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,4 @@
.fl-counter-rollup {
display: inline-block;
font-variant-numeric: tabular-nums;
}

View File

@@ -0,0 +1,99 @@
import {
Component, ChangeDetectionStrategy, input, inject,
afterNextRender, DestroyRef, signal, effect,
} from '@angular/core';
import { ReducedMotionService } from '../../services/reduced-motion.service';
@Component({
selector: 'fl-counter-rollup',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `{{ prefix() }}{{ formattedValue() }}{{ suffix() }}`,
styleUrl: './fl-counter-rollup.component.scss',
host: { 'class': 'fl-counter-rollup' },
})
export class FlCounterRollupComponent {
private reducedMotion = inject(ReducedMotionService);
private destroyRef = inject(DestroyRef);
// Inputs
readonly value = input.required<number>();
readonly duration = input(2000);
readonly prefix = input('');
readonly suffix = input('');
readonly decimals = input(0);
readonly separator = input(',');
// Internal
readonly formattedValue = signal('0');
private currentValue = 0;
private startValue = 0;
private targetValue = 0;
private startTime = 0;
private animationFrame: number | null = null;
constructor() {
afterNextRender(() => {
this.startAnimation();
});
effect(() => {
// React to value changes
const newValue = this.value();
this.startValue = this.currentValue;
this.targetValue = newValue;
this.startAnimation();
});
this.destroyRef.onDestroy(() => {
if (this.animationFrame !== null) {
cancelAnimationFrame(this.animationFrame);
}
});
}
private startAnimation(): void {
if (this.reducedMotion.reduced()) {
this.currentValue = this.targetValue;
this.formattedValue.set(this.formatNumber(this.targetValue));
return;
}
this.startTime = performance.now();
this.animate();
}
private animate = (): void => {
const now = performance.now();
const elapsed = now - this.startTime;
const duration = this.duration();
const progress = Math.min(elapsed / duration, 1);
// Ease out cubic
const eased = 1 - Math.pow(1 - progress, 3);
this.currentValue = this.startValue + (this.targetValue - this.startValue) * eased;
this.formattedValue.set(this.formatNumber(this.currentValue));
if (progress < 1) {
this.animationFrame = requestAnimationFrame(this.animate);
} else {
this.currentValue = this.targetValue;
this.formattedValue.set(this.formatNumber(this.targetValue));
}
};
private formatNumber(value: number): string {
const decimals = this.decimals();
const separator = this.separator();
const fixed = value.toFixed(decimals);
const parts = fixed.split('.');
// Add thousands separator
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, separator);
return parts.join('.');
}
}

View File

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