// Shared UI atoms for app + admin const { useState: useStateU, useEffect: useEffectU } = React; const Dot = ({ c = "var(--ink-3)", size = 6 }) => ( ); const Pill = ({ children, tone = "ink", small }) => { const tones = { ink: { bg: "var(--bg-2)", fg: "var(--ink-2)", bd: "var(--line)" }, accent: { bg: "var(--accent-soft)", fg: "var(--accent)", bd: "transparent" }, warn: { bg: "var(--warn-soft)", fg: "var(--warn)", bd: "transparent" }, bad: { bg: "color-mix(in oklab, var(--bad) 14%, var(--bg))", fg: "var(--bad)", bd: "transparent" }, ghost: { bg: "transparent", fg: "var(--ink-2)", bd: "var(--line)" }, }[tone]; return ( {children} ); }; const Btn = ({ children, variant = "primary", onClick, small, disabled, icon, title }) => { const pad = small ? "6px 11px" : "8px 14px"; const fs = small ? 12 : 13; const variants = { primary: { background: "var(--ink)", color: "var(--bg)", border: "1px solid var(--ink)" }, ghost: { background: "transparent", color: "var(--ink)", border: "1px solid var(--line)" }, soft: { background: "var(--bg-2)", color: "var(--ink)", border: "1px solid var(--line-2)" }, danger: { background: "transparent", color: "var(--bad)", border: "1px solid var(--bad)" }, }; return ( ); }; const Card = ({ children, pad = 18, title, action, compact }) => (
{title && (
{title}
{action}
)}
{children}
); const Stat = ({ value, label, sub, tone }) => (
{label}
{value}
{sub &&
{sub}
}
); // WideWired mark — inline SVG: two brackets flanking a filled square. // The whole mark scales with `size` (height in px). const LogoMark = ({ size = 28, showWord = true }) => { const [src, setSrc] = React.useState(() => { const t = typeof document !== "undefined" ? document.documentElement.dataset.theme : "light"; return t === "dark" ? "assets/widewired-logo-white.svg" : "assets/widewired-logo-ink.svg"; }); React.useEffect(() => { const update = () => { const t = document.documentElement.dataset.theme; setSrc(t === "dark" ? "assets/widewired-logo-white.svg" : "assets/widewired-logo-ink.svg"); }; update(); const obs = new MutationObserver(update); obs.observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme"] }); return () => obs.disconnect(); }, []); return ( WideWired ); }; const Segmented = ({ value, options, onChange, small }) => (
{options.map(([k, l]) => ( ))}
); // Role → permission matrix (from docs/19) const CAN = { edit: { owner: true, editor: true, reader: false }, delete: { owner: true, editor: false, reader: false }, invite: { owner: true, editor: false, reader: false }, billing: { owner: true, editor: false, reader: false }, }; function can(role, op) { return !!CAN[op]?.[role]; } // Avatar const Avatar = ({ name, size = 28, tone = "ink" }) => { const initial = (name || "?").trim().charAt(0).toUpperCase(); const bg = tone === "accent" ? "var(--accent-soft)" : "var(--bg-3)"; const fg = tone === "accent" ? "var(--accent)" : "var(--ink-2)"; return (
= 28 ? 8 : 6, background: bg, color: fg, display: "inline-flex", alignItems: "center", justifyContent: "center", fontSize: size * 0.42, fontWeight: 600, border: "1px solid var(--line)", }}>{initial}
); }; // Small icon (monospace symbol-based to avoid emoji / SVG complexity) const Ic = ({ c, color }) => ( {c} ); Object.assign(window, { Dot, Pill, Btn, Card, Stat, LogoMark, Segmented, Avatar, Ic, can });