Files
gacha-event-tracker/src/client/components/Fireworks.tsx
T
Lucas WintherandClaude Opus 5 c3e9b9064d feat(daily): a small burst when the last daily lands
The one moment in this app that is unambiguously good news, and it read
the same as any other tick.

Fires only on a tick that completes the set — the list also shortens when
the reader filters, and a burst there would be congratulating them for
filtering. Nothing on mount either, so arriving at an already-finished
day is not treated as having just finished it.

Decorative, aria-hidden and under a second; the "All done" text beside it
is what a screen reader gets. Fully off under prefers-reduced-motion
rather than merely reduced — nothing here carries meaning, so the honest
reduced version is nothing at all.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-16 20:39:09 +02:00

62 lines
2.0 KiB
TypeScript

/**
* A small burst, for the one moment in this app that is unambiguously good
* news: every daily ticked off before the reset.
*
* Deliberately tiny and deliberately silent — decorative only, `aria-hidden`,
* and gone in under a second. The text beside it already says "All done", which
* is what a screen reader gets; this is the same sentence said in colour.
*
* Fully off under `prefers-reduced-motion` (see styles.css). A celebration
* nobody asked for is exactly the kind of motion that setting exists to stop,
* and the state it celebrates is legible without it.
*/
/**
* Fixed spark geometry rather than random: the burst looks the same every time,
* which makes it a piece of the interface rather than a surprise, and keeps it
* out of the "renders differently on every paint" category.
*/
const BURSTS: Array<{ x: number; y: number; delay: number }> = [
{ x: 18, y: 40, delay: 0 },
{ x: 52, y: 22, delay: 140 },
{ x: 82, y: 52, delay: 260 },
];
const SPARKS = 9;
/** The heat ramp, reused: the palette already means "this app", so it stays. */
const COLORS = [
"var(--color-near)",
"var(--color-soon)",
"var(--color-critical)",
"var(--color-ink)",
];
export function Fireworks() {
return (
<div aria-hidden className="fireworks">
{BURSTS.map((burst, b) => (
<span
key={b}
className="firework"
style={{ left: `${burst.x}%`, top: `${burst.y}%` }}
>
{Array.from({ length: SPARKS }, (_, i) => (
<span
key={i}
className="spark"
style={{
["--a" as string]: `${(360 / SPARKS) * i + b * 13}deg`,
// Uneven reach, so it reads as a burst rather than a wheel.
["--d" as string]: `${16 + ((i * 7) % 13)}px`,
["--delay" as string]: `${burst.delay}ms`,
background: COLORS[(i + b) % COLORS.length] ?? "var(--color-ink)",
}}
/>
))}
</span>
))}
</div>
);
}