feat: add @sda/flair-elements-ui library

Angular 19+ standalone library with 61 components, 15 directives,
and 8 services for decorative visual effects, animations, and
micro-interactions.

Components include particle systems, flow fields, aurora effects,
tilt/holographic/spotlight cards, text effects, dividers, scroll
animations, generative art, celebrations, image treatments,
morphing shapes, terminal output, and pattern textures.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Giuliano Silvestro
2026-03-09 10:24:52 +10:00
commit daf6182e94
230 changed files with 10642 additions and 0 deletions

View File

@@ -0,0 +1,19 @@
:host {
display: block;
position: relative;
width: 100%;
height: 100%;
}
.fl-fluid-container__svg {
position: absolute;
width: 0;
height: 0;
pointer-events: none;
}
.fl-fluid-container__content {
width: 100%;
height: 100%;
box-sizing: border-box;
}

View File

@@ -0,0 +1,125 @@
import {
Component, ChangeDetectionStrategy, input, inject,
DestroyRef, afterNextRender, signal,
} from '@angular/core';
import { ReducedMotionService } from '../../services/reduced-motion.service';
import { AnimationLoopService } from '../../services/animation-loop.service';
@Component({
selector: 'fl-fluid-container',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<svg
class="fl-fluid-container__svg"
preserveAspectRatio="none"
[attr.viewBox]="viewBox"
>
<defs>
<clipPath [id]="clipId">
<path [attr.d]="clipPath()" />
</clipPath>
</defs>
</svg>
<div
class="fl-fluid-container__content"
[style.clip-path]="clipPathValue"
[style.border]="borderStyle"
>
<ng-content />
</div>
`,
styleUrl: './fl-fluid-container.component.scss',
host: { 'class': 'fl-fluid-container' },
})
export class FlFluidContainerComponent {
private reducedMotion = inject(ReducedMotionService);
private animationLoop = inject(AnimationLoopService);
private destroyRef = inject(DestroyRef);
// Inputs
readonly borderColor = input('#ff6b6b');
readonly speed = input(1);
readonly amplitude = input(10);
readonly borderWidth = input(2);
protected readonly clipPath = signal('');
protected readonly clipId = `fluid-${Math.random().toString(36).slice(2)}`;
protected readonly viewBox = '0 0 100 100';
private loopCleanup: (() => void) | null = null;
private time = 0;
protected get clipPathValue(): string {
return this.reducedMotion.reduced() ? 'none' : `url(#${this.clipId})`;
}
protected get borderStyle(): string {
return `${this.borderWidth()}px solid ${this.borderColor()}`;
}
constructor() {
afterNextRender(() => {
this.updatePath();
if (!this.reducedMotion.reduced()) {
const id = `fluid-container-${Math.random().toString(36).slice(2)}`;
this.loopCleanup = this.animationLoop.register(id, (delta) => this.tick(delta));
}
});
this.destroyRef.onDestroy(() => {
this.loopCleanup?.();
});
}
private tick(delta: number): void {
this.time += delta * this.speed();
this.updatePath();
}
private updatePath(): void {
const amp = this.amplitude() / 10;
const t = this.time;
// Create undulating border effect
const topWave = this.createWave(0, 100, t, amp, 0);
const rightWave = this.createWave(100, 100, t, amp, Math.PI / 2, true);
const bottomWave = this.createWave(100, 0, t, amp, Math.PI);
const leftWave = this.createWave(0, 0, t, amp, 3 * Math.PI / 2, true);
this.clipPath.set(`
M ${topWave}
L ${rightWave}
L ${bottomWave}
L ${leftWave}
Z
`);
}
private createWave(
start: number,
end: number,
time: number,
amplitude: number,
phase: number,
vertical = false
): string {
const points: string[] = [];
const steps = 20;
for (let i = 0; i <= steps; i++) {
const t = i / steps;
const pos = start + (end - start) * t;
const wave = Math.sin(t * Math.PI * 4 + time + phase) * amplitude;
if (vertical) {
points.push(`${pos + wave},${start + (end - start) * t}`);
} else {
points.push(`${start + (end - start) * t},${pos + wave}`);
}
}
return points.join(' L ');
}
}

View File

@@ -0,0 +1 @@
export * from './fl-fluid-container.component';