mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-08-08 08:04:32 +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.
36 lines
1.1 KiB
TypeScript
36 lines
1.1 KiB
TypeScript
import { useEffect, useReducer, useRef } from 'react'
|
|
import { createPortal } from 'react-dom'
|
|
|
|
import { getToast, isPrimaryHost, registerToastHost, subscribeToasts } from '../lib/toast'
|
|
import { motionClass } from '../lib/motion'
|
|
|
|
/** Bottom-right toast surface for action feedback. One at a time, role=status,
|
|
* slide-in when motion is on, auto-dismissed by the store. Ref-counted so only
|
|
* the primary host paints even if more than one is mounted. */
|
|
export function ToastHost() {
|
|
const idRef = useRef(0)
|
|
const [, force] = useReducer((n: number) => n + 1, 0)
|
|
|
|
useEffect(() => {
|
|
const { id, release } = registerToastHost()
|
|
idRef.current = id
|
|
const unsubscribe = subscribeToasts(force)
|
|
force()
|
|
return () => {
|
|
release()
|
|
unsubscribe()
|
|
}
|
|
}, [])
|
|
|
|
const toast = getToast()
|
|
if (typeof document === 'undefined' || !isPrimaryHost(idRef.current) || !toast) return null
|
|
|
|
return createPortal(
|
|
<div className="toast-host" aria-live="polite">
|
|
<div key={toast.id} className={motionClass(`toast toast-${toast.kind}`, 'toast-in')} role="status">
|
|
{toast.text}
|
|
</div>
|
|
</div>,
|
|
document.body,
|
|
)
|
|
}
|