mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-08-03 13:25:13 +00:00
- one gate (motionEnabled): off under reduced-motion, missing matchMedia, and vitest; every path checks it first - mount/filter-change only: count-up and bar grow-in key off the period|provider|range key, so 30s poll refreshes snap values silently instead of re-animating - first-load skeleton shimmer replaces bare scanning text (kept sr-only for screen readers); slide-in toast host for Settings/export feedback (validation errors stay inline); CSS hover-lift + press micro- interactions, all with a reduced-motion escape hatch - gsap 3.15.0 + @gsap/react 2.1.2 (+74KB raw JS; G4 flame work shares it) 244/244, typecheck + build green.
76 lines
1.8 KiB
TypeScript
76 lines
1.8 KiB
TypeScript
export type ToastKind = 'ok' | 'error'
|
|
export type Toast = { id: number; text: string; kind: ToastKind }
|
|
|
|
let current: Toast | null = null
|
|
let seq = 0
|
|
let timer: ReturnType<typeof setTimeout> | null = null
|
|
const hosts: number[] = []
|
|
let hostSeq = 0
|
|
const listeners = new Set<() => void>()
|
|
|
|
function emit(): void {
|
|
for (const listener of listeners) listener()
|
|
}
|
|
|
|
function clearTimer(): void {
|
|
if (timer) {
|
|
clearTimeout(timer)
|
|
timer = null
|
|
}
|
|
}
|
|
|
|
/** Show a toast, replacing any current one (only ever one at a time), and start
|
|
* its auto-dismiss timer. */
|
|
export function showToast(text: string, kind: ToastKind = 'ok', durationMs = 3000): void {
|
|
seq += 1
|
|
current = { id: seq, text, kind }
|
|
clearTimer()
|
|
timer = setTimeout(() => {
|
|
current = null
|
|
timer = null
|
|
emit()
|
|
}, durationMs)
|
|
emit()
|
|
}
|
|
|
|
export function dismissToast(): void {
|
|
clearTimer()
|
|
current = null
|
|
emit()
|
|
}
|
|
|
|
export function getToast(): Toast | null {
|
|
return current
|
|
}
|
|
|
|
export function subscribeToasts(listener: () => void): () => void {
|
|
listeners.add(listener)
|
|
return () => {
|
|
listeners.delete(listener)
|
|
}
|
|
}
|
|
|
|
/** Register a Toast host. Only the first-registered host renders (so App and a
|
|
* standalone-tested Settings can both mount one without doubling the toast). The
|
|
* store resets when the last host unmounts, keeping tests isolated. */
|
|
export function registerToastHost(): { id: number; release: () => void } {
|
|
const id = ++hostSeq
|
|
hosts.push(id)
|
|
emit()
|
|
return {
|
|
id,
|
|
release: () => {
|
|
const index = hosts.indexOf(id)
|
|
if (index >= 0) hosts.splice(index, 1)
|
|
if (hosts.length === 0) {
|
|
clearTimer()
|
|
current = null
|
|
}
|
|
emit()
|
|
},
|
|
}
|
|
}
|
|
|
|
export function isPrimaryHost(id: number): boolean {
|
|
return hosts.length > 0 && hosts[0] === id
|
|
}
|