// Schaffner OS — shared UI primitives const { useState, useEffect, useMemo } = React; function fmtMoney(n) { return '$' + Math.round(n).toLocaleString('en-US'); } function fmtK(n) { return n >= 1000 ? '$' + (n / 1000).toFixed(1).replace(/\.0$/, '') + 'k' : fmtMoney(n); } function pct(x, d = 1) { return (x * 100).toFixed(d) + '%'; } function Delta({ value }) { const up = value >= 0; return ( {up ? '▲' : '▼'} {Math.abs(value).toFixed(1)}% ); } // Simple sparkline — pure data viz function Spark({ data, color = 'var(--moss)', w = 120, h = 34, fill = true }) { const min = Math.min(...data), max = Math.max(...data); const range = max - min || 1; const pts = data.map((v, i) => [ (i / (data.length - 1)) * (w - 2) + 1, h - 3 - ((v - min) / range) * (h - 6), ]); const line = pts.map(p => p.join(',')).join(' '); const area = `1,${h} ` + line + ` ${w - 1},${h}`; return ( ); } // Multi-series line chart for revenue trend. // fmt(v) optionally formats axis + end labels (default money: "$k"). // A series may set { dash: true } for a dashed trend line and name '' to skip its label. function TrendChart({ series, labels, height = 190, fmt }) { const w = 760, h = height, padL = 44, padR = 96, padT = 14, padB = 24; const axisFmt = fmt || (v => '$' + Math.round(v) + 'k'); const labelFmt = fmt || (v => '$' + v + 'k'); const all = series.flatMap(s => s.data); const lo = Math.min(...all), hi = Math.max(...all); const min = lo > 0 ? lo * 0.96 : lo, max = hi * 1.03; const x = i => padL + (i / (series[0].data.length - 1)) * (w - padL - padR); const y = v => padT + (1 - (v - min) / (max - min)) * (h - padT - padB); const gridVals = [min, (min + max) / 2, max]; return ( ); } function SectionHead({ title, action, onAction }) { return (

{title}

{action ? : null}
); } function StatusBadge({ status }) { const map = { ok: ['ok', 'In stock'], low: ['warn', 'Low'], reorder: ['bad', 'Reorder now'], watch: ['neutral', 'Count due'], }; const [tone, label] = map[status] || ['neutral', status]; return {label}; } function Toast({ msg }) { if (!msg) return null; return
{msg}
; } function useToast() { const [msg, setMsg] = useState(null); const show = (m) => { setMsg(m); window.clearTimeout(show._t); show._t = window.setTimeout(() => setMsg(null), 2200); }; return [msg, show]; } Object.assign(window, { fmtMoney, fmtK, pct, Delta, Spark, TrendChart, SectionHead, StatusBadge, Toast, useToast, });