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,20 @@
:host {
display: block;
position: relative;
overflow: hidden;
width: 100%;
height: 100%;
svg {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
::ng-deep > :not(svg) {
position: relative;
z-index: 1;
}
}

View File

@@ -0,0 +1,157 @@
import {
Component, ChangeDetectionStrategy, input, output, inject,
viewChild, ElementRef, afterNextRender, DestroyRef,
} from '@angular/core';
import { ReducedMotionService } from '../../services/reduced-motion.service';
import { AnimationLoopService } from '../../services/animation-loop.service';
import { createSvgElement, generateWavePath } from '../../utils/svg.utils';
@Component({
selector: 'fl-aurora',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<svg #svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="none">
<defs>
<filter id="aurora-blur" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="40" />
</filter>
</defs>
</svg>
<ng-content />
`,
styleUrl: './fl-aurora.component.scss',
host: { 'class': 'fl-aurora' },
})
export class FlAuroraComponent {
private reducedMotion = inject(ReducedMotionService);
private animationLoop = inject(AnimationLoopService);
private destroyRef = inject(DestroyRef);
protected svgRef = viewChild.required<ElementRef<SVGElement>>('svg');
// Inputs
readonly colors = input<string[]>(['#22c55e', '#06b6d4', '#8b5cf6', '#6366f1']);
readonly layers = input(4);
readonly speed = input(1);
readonly blur = input(60);
readonly opacity = input(0.6);
readonly responsive = input(true);
// Outputs
readonly ready = output<void>();
private paths: SVGPathElement[] = [];
private width = 0;
private height = 0;
private loopCleanup: (() => void) | null = null;
private resizeObserver: ResizeObserver | null = null;
constructor() {
afterNextRender(() => {
this.initialize();
});
this.destroyRef.onDestroy(() => {
this.loopCleanup?.();
this.resizeObserver?.disconnect();
});
}
private initialize(): void {
const svg = this.svgRef().nativeElement;
const parent = svg.parentElement;
if (!parent) return;
this.resize();
if (this.responsive()) {
this.resizeObserver = new ResizeObserver(() => this.resize());
this.resizeObserver.observe(parent);
}
// Update blur
const blurFilter = svg.querySelector('feGaussianBlur');
if (blurFilter) {
blurFilter.setAttribute('stdDeviation', `${this.blur()}`);
}
this.createLayers();
if (this.reducedMotion.reduced()) {
this.renderStaticState();
} else {
const id = `aurora-${Math.random().toString(36).slice(2)}`;
this.loopCleanup = this.animationLoop.register(id, (_, elapsed) => this.tick(elapsed));
}
this.ready.emit();
}
private resize(): void {
const svg = this.svgRef().nativeElement;
const parent = svg.parentElement;
if (!parent) return;
this.width = parent.clientWidth;
this.height = parent.clientHeight;
svg.setAttribute('viewBox', `0 0 ${this.width} ${this.height}`);
svg.setAttribute('width', `${this.width}`);
svg.setAttribute('height', `${this.height}`);
}
private createLayers(): void {
const svg = this.svgRef().nativeElement;
const colors = this.colors();
const layerCount = this.layers();
const opacity = this.opacity();
// Remove old paths
for (const p of this.paths) p.remove();
this.paths = [];
for (let i = 0; i < layerCount; i++) {
const path = createSvgElement('path', {
fill: colors[i % colors.length],
opacity: `${opacity * (1 - i * 0.15)}`,
filter: 'url(#aurora-blur)',
});
svg.appendChild(path);
this.paths.push(path);
}
}
private tick(elapsed: number): void {
const w = this.width;
const h = this.height;
const speed = this.speed();
const layerCount = this.layers();
for (let i = 0; i < this.paths.length; i++) {
const layerOffset = i / layerCount;
const amplitude = h * 0.15 * (1 + layerOffset * 0.5);
const frequency = 1.5 + layerOffset;
const phase = elapsed * speed * (0.3 + layerOffset * 0.2);
const yOffset = h * (0.2 + layerOffset * 0.15);
const d = generateWavePath(w, h, amplitude, frequency, phase, yOffset);
this.paths[i].setAttribute('d', d);
}
}
private renderStaticState(): void {
const w = this.width;
const h = this.height;
const layerCount = this.layers();
for (let i = 0; i < this.paths.length; i++) {
const layerOffset = i / layerCount;
const amplitude = h * 0.15 * (1 + layerOffset * 0.5);
const frequency = 1.5 + layerOffset;
const yOffset = h * (0.2 + layerOffset * 0.15);
const d = generateWavePath(w, h, amplitude, frequency, 0, yOffset);
this.paths[i].setAttribute('d', d);
}
}
}

View File

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