// 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 && (
)}
{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 (
);
};
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 });