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,12 @@
:host {
display: block;
width: 100%;
height: 100%;
background: #0a0a0a;
}
.fl-circuit-board__svg {
width: 100%;
height: 100%;
display: block;
}

View File

@@ -0,0 +1,157 @@
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';
interface CircuitPath {
d: string;
pulseOffset: number;
}
@Component({
selector: 'fl-circuit-board',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<svg
class="fl-circuit-board__svg"
viewBox="0 0 100 100"
preserveAspectRatio="xMidYMid slice"
>
<defs>
<linearGradient [id]="pulseGradientId">
<stop offset="0%" [attr.stop-color]="color()" stop-opacity="0" />
<stop offset="50%" [attr.stop-color]="pulseColor()" stop-opacity="1" />
<stop offset="100%" [attr.stop-color]="color()" stop-opacity="0" />
</linearGradient>
</defs>
<!-- Circuit traces -->
@for (circuit of circuits(); track $index) {
<path
[attr.d]="circuit.d"
fill="none"
[attr.stroke]="color()"
stroke-width="0.5"
stroke-linecap="round"
/>
}
<!-- Animated pulses -->
@if (!reducedMotion.reduced()) {
@for (circuit of circuits(); track $index) {
<path
[attr.d]="circuit.d"
fill="none"
[attr.stroke]="pulseGradientFill"
stroke-width="1"
stroke-linecap="round"
[style.stroke-dasharray]="10"
[style.stroke-dashoffset]="getPulseOffset($index, circuit.pulseOffset)"
/>
}
}
<!-- Connection nodes -->
@for (node of nodes(); track $index) {
<circle
[attr.cx]="node.x"
[attr.cy]="node.y"
r="0.8"
[attr.fill]="pulseColor()"
/>
}
</svg>
`,
styleUrl: './fl-circuit-board.component.scss',
host: { 'class': 'fl-circuit-board' },
})
export class FlCircuitBoardComponent {
protected reducedMotion = inject(ReducedMotionService);
private animationLoop = inject(AnimationLoopService);
private destroyRef = inject(DestroyRef);
// Inputs
readonly color = input('#00ff88');
readonly pulseColor = input('#00ffff');
readonly speed = input(1);
readonly density = input(0.5);
protected readonly circuits = signal<CircuitPath[]>([]);
protected readonly nodes = signal<{ x: number; y: number }[]>([]);
protected readonly pulseGradientId = `circuit-pulse-${Math.random().toString(36).slice(2)}`;
private loopCleanup: (() => void) | null = null;
private time = 0;
protected get pulseGradientFill(): string {
return `url(#${this.pulseGradientId})`;
}
constructor() {
afterNextRender(() => {
this.generateCircuits();
if (!this.reducedMotion.reduced()) {
const id = `circuit-board-${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();
}
protected getPulseOffset(index: number, baseOffset: number): number {
return (this.time * 50 + baseOffset) % 100;
}
private generateCircuits(): void {
const circuitPaths: CircuitPath[] = [];
const nodeList: { x: number; y: number }[] = [];
const pathCount = Math.floor(10 * this.density());
// Generate grid-based circuit paths
for (let i = 0; i < pathCount; i++) {
const startX = Math.random() * 100;
const startY = Math.random() * 100;
const path: string[] = [`M ${startX} ${startY}`];
let currentX = startX;
let currentY = startY;
const segments = 3 + Math.floor(Math.random() * 4);
for (let j = 0; j < segments; j++) {
const horizontal = Math.random() > 0.5;
const distance = 10 + Math.random() * 20;
if (horizontal) {
currentX += (Math.random() > 0.5 ? 1 : -1) * distance;
currentX = Math.max(0, Math.min(100, currentX));
} else {
currentY += (Math.random() > 0.5 ? 1 : -1) * distance;
currentY = Math.max(0, Math.min(100, currentY));
}
path.push(`L ${currentX} ${currentY}`);
nodeList.push({ x: currentX, y: currentY });
}
circuitPaths.push({
d: path.join(' '),
pulseOffset: Math.random() * 100,
});
}
this.circuits.set(circuitPaths);
this.nodes.set(nodeList);
}
}

View File

@@ -0,0 +1 @@
export * from './fl-circuit-board.component';