// DemoKit.jsx — shared helpers for the homepage feature demos. // Timeline-driven, auto-looping mini-UIs. Everything is keyed off a single // looping clock (useDemoLoop) so demos are deterministic and seekable // (set window.__DEMO_FREEZE = to freeze every demo at a timestamp). (function () { const { useState, useEffect, useMemo, useRef } = React; // ── clock ────────────────────────────────────────────────────────── function useDemoLoop(duration, stillAt) { const reduced = useMemo( () => window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches, [] ); const [t, setT] = useState(reduced ? (stillAt != null ? stillAt : duration - 50) : 0); useEffect(() => { if (reduced) return; let raf; const start = performance.now(); let last = -1000; const update = (now) => { const f = window.__DEMO_FREEZE; const tt = f != null ? Math.min(f, duration - 1) : (now - start) % duration; if (Math.abs(tt - last) > 28 || tt < last) { last = tt; setT(tt); } }; const tick = (now) => { update(now); raf = requestAnimationFrame(tick); }; raf = requestAnimationFrame(tick); // fallback: keeps the clock moving when rAF is throttled (hidden/embedded views) const iv = setInterval(() => update(performance.now()), 200); return () => { cancelAnimationFrame(raf); clearInterval(iv); }; }, [duration, reduced]); return t; } // ── timeline math ────────────────────────────────────────────────── function dkProg(t, a, b) { return Math.max(0, Math.min(1, (t - a) / (b - a))); } function dkEase(p) { return p < 0.5 ? 2 * p * p : 1 - Math.pow(-2 * p + 2, 2) / 2; } function dkTyped(text, t, a, b) { const p = dkProg(t, a, b); return text.slice(0, Math.round(p * text.length)); } // fade the stage out at the end of the loop so the restart feels soft // (no fade-in: t=0 must render fully visible even if the clock is paused) function dkFade(t, d) { const f = 320; if (t > d - f) return Math.max(0, (d - t) / f); return 1; } // cursor state from waypoints [{at,x,y,hidden}] + click times [ms] function dkCursor(t, waypoints, clicks = []) { let pos = waypoints[0]; for (const w of waypoints) if (t >= w.at) pos = w; const down = clicks.some((c) => t >= c && t < c + 180); const fired = clicks.filter((c) => t >= c).length; return { x: pos.x, y: pos.y, hidden: !!pos.hidden, down, fired }; } // € 1.234,56 (PT format per README) function dkEUR(v) { const [int, dec] = v.toFixed(2).split('.'); return '€ ' + int.replace(/\B(?=(\d{3})+(?!\d))/g, '.') + ',' + dec; } // ── atoms ────────────────────────────────────────────────────────── function DemoCaret({ show = true, dark }) { if (!show) return null; return ( ); } function DemoCursor({ s }) { return (
{s.fired > 0 && }
); } function DemoToast({ show, text, icon = 'check-circle-2', dark }) { return (
{text}
); } function MiniWindow({ title, icon, right, dark, width = 480, children }) { return (
{title}
{right}
{children}
); } // Self-scaling 560×400 stage: fills its container's width, keeps ratio. function DemoStage({ width = 560, height = 400, dark, t, dur, children }) { const ref = useRef(null); const [scale, setScale] = useState(1); useEffect(() => { const el = ref.current; if (!el) return; const upd = () => setScale(el.clientWidth / width); upd(); const ro = new ResizeObserver(upd); ro.observe(el); return () => ro.disconnect(); }, [width]); const fade = t != null && dur ? dkFade(t, dur) : 1; return (
{children}
); } // Deterministic fake-QR placeholder (squares only). function FakeQR({ size = 52 }) { const cells = useMemo(() => { const a = []; let s = 5; for (let i = 0; i < 100; i++) { s = (s * 37 + 11) % 53; a.push(s % 2 === 0); } return a; }, []); return (
{cells.map((on, i) => ( ))}
); } Object.assign(window, { useDemoLoop, dkProg, dkEase, dkTyped, dkFade, dkCursor, dkEUR, DemoCaret, DemoCursor, DemoToast, MiniWindow, DemoStage, FakeQR, }); })();