/* RADAR1 — shared primitives: clock hooks, framed panels, wipes */ const { useState, useEffect, useMemo, useRef, useCallback } = React; /* Ticking clock */ function useNow(interval = 1000) { const [now, setNow] = useState(() => new Date()); useEffect(() => { const id = setInterval(() => setNow(new Date()), interval); return () => clearInterval(id); }, [interval]); return now; } function fmtTime(date, tz, h24, seconds = false) { try { return new Intl.DateTimeFormat("en-GB", { hour: "2-digit", minute: "2-digit", ...(seconds ? { second: "2-digit" } : {}), hour12: !h24, timeZone: tz, }).format(date).toUpperCase(); } catch (e) { return "--:--"; } } function fmtDate(date, tz) { try { return new Intl.DateTimeFormat("en-US", { weekday: "short", month: "short", day: "numeric", timeZone: tz, }).format(date).toUpperCase(); } catch (e) { return ""; } } /* Interval-driven index cycling with fade key */ function useCycle(length, ms, paused = false) { const [i, setI] = useState(0); useEffect(() => { if (paused || length < 2) return; const id = setInterval(() => setI((v) => (v + 1) % length), ms); return () => clearInterval(id); }, [length, ms, paused]); return i; } /* Caps eyebrow label */ function Eyebrow({ children, color = "var(--text-muted)", size = "var(--fs-caption)", style }) { return (
{children}
); } /* Framed rail panel — hairline frame, sweep corner, accent spine */ function RailPanel({ accent = "var(--signal-cyan)", title, right = null, children, flush = false, style }) { return (
{title && (
{title} {right}
)}
{children}
); } /* Pulsing live dot */ function LiveDot({ color = "var(--radar-red)", size = 8 }) { return ( ); } /* Delta value (markets) */ function Delta({ v }) { const up = v >= 0; return ( {up ? "▲" : "▼"} {Math.abs(v).toFixed(2)}% ); } /* Full-stage sweep wipe: red diagonal band crosses the frame; fires on `signal` change */ function SweepWipe({ signal }) { const [active, setActive] = useState(false); const first = useRef(true); useEffect(() => { if (first.current) { first.current = false; return; } setActive(true); const id = setTimeout(() => setActive(false), 950); return () => clearTimeout(id); }, [signal]); if (!active) return null; return (
RADAR1
); } /* Simple keyed fade/slide-in wrapper (re-mounts on key change upstream) */ function Enter({ dir = "up", dist = 14, dur = 360, delay = 0, children, style }) { return (
{children}
); } /* Broadcast armor: a region that crashes renders nothing and quietly retries. The stage itself must never unmount — a bad payload costs one region for 15 seconds, not the broadcast. */ class R1Boundary extends React.Component { constructor(props) { super(props); this.state = { broken: false }; this.timer = null; } static getDerivedStateFromError() { return { broken: true }; } componentDidCatch(err) { try { console.error("R1Boundary[" + (this.props.name || "?") + "]", err); } catch (e) {} } componentDidUpdate() { if (this.state.broken && !this.timer) { this.timer = setTimeout(() => { this.timer = null; this.setState({ broken: false }); }, 15000); } } componentWillUnmount() { if (this.timer) clearTimeout(this.timer); } render() { return this.state.broken ? null : this.props.children; } } /* Lucide refresh helper — call after any render that adds data-lucide nodes */ function useLucide(deps) { useEffect(() => { const id = requestAnimationFrame(() => { window.lucide && window.lucide.createIcons(); }); return () => cancelAnimationFrame(id); }, deps); } Object.assign(window, { useNow, fmtTime, fmtDate, useCycle, Eyebrow, RailPanel, LiveDot, Delta, SweepWipe, Enter, useLucide, R1Boundary, });