// ============================================================================ // Imara — shared UI primitives, theme store, helpers // ============================================================================ const { useState, useEffect, useRef, useMemo, useCallback, createContext, useContext } = React; // ---- Icon (Lucide UMD) ------------------------------------------------------ function Icon({ name, size = 16, stroke = 1.75, className, style }) { const data = window.lucide && (window.lucide[name] || (window.lucide.icons && window.lucide.icons[name])); // lucide IconNode = ["svg", attrs, [ [tag, attrs], ... ] ]; children at [2] const kids = (data && Array.isArray(data[2]) ? data[2] : []); return ( ); } // ---- formatting helpers ----------------------------------------------------- const fmt = (n) => (n == null ? "-" : n.toLocaleString()); // Ages on the Imara server's clock (terms.js serverNow), not the browser's. function ago(iso) { const now = window.ImaraTerms && window.ImaraTerms.serverNow ? window.ImaraTerms.serverNow() : Date.now(); const s = Math.max(0, Math.round((now - new Date(iso).getTime()) / 1000)); if (s < 60) return `${s}s`; const m = Math.round(s / 60); if (m < 60) return `${m}m`; const h = Math.round(m / 60); if (h < 24) return `${h}h`; return `${Math.round(h / 24)}d`; } function agoLong(iso) { return ago(iso) + " ago"; } const cx = (...a) => a.filter(Boolean).join(" "); // ---- theme store ------------------------------------------------------------ // Only the light / dark choice is kept (an older saved accent or density is // ignored). function loadStore() { try { return { theme: JSON.parse(localStorage.getItem("imara.ui") || "{}").theme === "light" ? "light" : "dark" }; } catch { return { theme: "dark" }; } } const StoreCtx = createContext(null); function useStore() { return useContext(StoreCtx); } function StoreProvider({ children }) { const [s, setS] = useState(loadStore); useEffect(() => { const r = document.documentElement; r.setAttribute("data-theme", s.theme); try { localStorage.setItem("imara.ui", JSON.stringify(s)); } catch (e) { /* storage blocked: the choice lasts this visit */ } }, [s]); const set = useCallback((patch) => setS((p) => ({ ...p, ...patch })), []); return React.createElement(StoreCtx.Provider, { value: { ...s, set } }, children); } // ---- small components ------------------------------------------------------- function Badge({ tone = "neutral", sq, dot, children, style }) { return {dot && }{children}; } function Meter({ value, max = 100, tone }) { const pct = max > 0 ? Math.min(100, (value / max) * 100) : 0; const color = tone === "ok" ? "var(--ok)" : tone === "warn" ? "var(--warn)" : tone === "bad" ? "var(--bad)" : "var(--accent)"; return