// Shared UI primitives used across screens
const { useState, useEffect, useMemo, useRef } = React;
const Avatar = ({ user, size = 28 }) => (
{user.avatar}
);
const Pill = ({ status, children, className = "" }) => {
const c = status ? status.toLowerCase() : "";
return {children || status};
};
const KPI = ({ label, value, delta, deltaDir, sub }) => (
{label}
{value}
{delta &&
{delta}
}
{sub &&
{sub}
}
);
const PageHeader = ({ title, sub, right }) => (
);
const Card = ({ title, sub, right, children, foot, style }) => (
{(title || right) && (
{title &&
{title}
}
{sub &&
{sub}
}
{right &&
{right}
}
)}
{children}
{foot &&
{foot}
}
);
const FilterChip = ({ keyLabel, value, set, onClear, onClick }) => (
);
const Stepper = ({ steps, current }) => (
{steps.map((s, i) => (
{i < current ? "✓" : i + 1}
{s}
{i < steps.length - 1 && }
))}
);
const Tabs = ({ tabs, current, onChange }) => (
{tabs.map((t) => (
))}
);
const SlideOver = ({ open, onClose, title, sub, children, foot, wide }) => {
if (!open) return null;
return (
<>
{children}
{foot &&
{foot}
}
>
);
};
const Toast = ({ msg, onClose }) => {
useEffect(() => {
if (!msg) return;
const t = setTimeout(onClose, 3000);
return () => clearTimeout(t);
}, [msg]);
if (!msg) return null;
return (
{msg}
);
};
const MiniBars = ({ data, highlight }) => (
{data.map((v, i) => (
))}
);
// Field with label
const Field = ({ label, required, hint, children, span }) => (
{children}
{hint &&
{hint}
}
);
// Format helpers
const fmt = {
num(n, d = 2) {
if (n == null || isNaN(n)) return "—";
return Number(n).toLocaleString("en-US", { minimumFractionDigits: d, maximumFractionDigits: d });
},
usd(n) {
return "$" + fmt.num(n, 2);
},
date(s) {
if (!s) return "—";
return s.length > 10 ? s : s;
},
};
// Sparkline SVG (rolling time series)
const Sparkline = ({ data, w = 80, h = 22, color = "var(--teal)", fill = false, strokeWidth = 1.4 }) => {
if (!data || data.length === 0) return null;
const min = Math.min(...data), max = Math.max(...data);
const range = max - min || 1;
const points = data.map((d, i) => `${(i / (data.length - 1)) * w},${h - ((d - min) / range) * (h - 2) - 1}`).join(" ");
return (
);
};
// Horizontal pace gauge — value vs target
const PaceGauge = ({ value, target, width = 160 }) => {
const pct = Math.max(0, Math.min(1.2, value / target));
const color = pct < 0.95 ? "var(--red)" : pct < 1.05 ? "var(--amber)" : "var(--green)";
return (
{(pct * 100).toFixed(0)}%
target {fmt.num(target / 1000, 0)}k
);
};
// Donut / progress ring
const Ring = ({ pct, size = 56, stroke = 6, color = "var(--teal)", track = "var(--paper-2)", label }) => {
const r = (size - stroke) / 2;
const c = 2 * Math.PI * r;
const off = c * (1 - Math.max(0, Math.min(1, pct / 100)));
return (
48 ? 12 : 10 }}>{label || `${Math.round(pct)}%`}
);
};
Object.assign(window, { Avatar, Pill, KPI, PageHeader, Card, FilterChip, Stepper, Tabs, SlideOver, Toast, MiniBars, Field, fmt, Sparkline, PaceGauge, Ring });