/* Eno.Health Console — shared UI primitives. Exposes components on window. */ const { useState, useEffect, useRef, useMemo, createElement: h } = React; /* ---- Icons (simple stroke set) ------------------------------------------ */ const ICON_PATHS = { overview: "M3 3h7v7H3zM14 3h7v4h-7zM14 10h7v11h-7zM3 14h7v7H3z", documents: "M6 2h8l4 4v16H6zM14 2v4h4", runs: "M3 12h4l3 8 4-16 3 8h4", activity: "M3 12h3.5l2-7 4.5 14 2.5-9 1.5 2h4", upload: "M12 16V4M7 9l5-5 5 5M5 20h14", policies: "M12 2l8 4v6c0 5-3.5 8-8 10-4.5-2-8-5-8-10V6z", retention: "M3 6h18M8 6V4h8v2M6 6l1 14h10l1-14", webhooks: "M9 7a4 4 0 1 1 5 5M15 17a4 4 0 1 1-5-5M7 12l-3 4M17 12l3 4", tenants: "M9 11a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM3 20c0-3 3-5 6-5s6 2 6 5M17 11a3 3 0 0 0 0-6M21 20c0-2-1.5-3.5-3.5-4.2", search: "M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16zM21 21l-4.3-4.3", bell: "M18 8a6 6 0 1 0-12 0c0 7-3 9-3 9h18s-3-2-3-9M13.7 21a2 2 0 0 1-3.4 0", chevron: "M6 9l6 6 6-6", chevronL: "M15 6l-6 6 6 6", chevronR: "M9 6l6 6-6 6", check: "M20 6L9 17l-5-5", x: "M18 6L6 18M6 6l12 12", alert: "M12 9v4M12 17h.01M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z", clock: "M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20zM12 6v6l4 2", dot: "M12 12h.01", external: "M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6M15 3h6v6M10 14L21 3", refresh: "M21 12a9 9 0 1 1-3-6.7L21 8M21 3v5h-5", shield: "M12 2l8 4v6c0 5-3.5 8-8 10-4.5-2-8-5-8-10V6z", play: "M5 3l14 9-14 9z", pause: "M6 4h4v16H6zM14 4h4v16h-4z", filter: "M3 4h18l-7 8v6l-4 2v-8z", sort: "M7 4v16M4 7l3-3 3 3M17 20V4M14 17l3 3 3-3", copy: "M9 9h11v11H9zM5 15H4V4h11v1", arrowRight: "M5 12h14M13 6l6 6-6 6", logout: "M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4M16 17l5-5-5-5M21 12H9", trash: "M3 6h18M8 6V4h8v2M6 6l1 14h10l1-14", plus: "M12 5v14M5 12h14", database: "M12 8c4.4 0 8-1.3 8-3s-3.6-3-8-3-8 1.3-8 3 3.6 3 8 3zM4 5v14c0 1.7 3.6 3 8 3s8-1.3 8-3V5M4 12c0 1.7 3.6 3 8 3s8-1.3 8-3", scan: "M3 7V5a2 2 0 0 1 2-2h2M17 3h2a2 2 0 0 1 2 2v2M21 17v2a2 2 0 0 1-2 2h-2M7 21H5a2 2 0 0 1-2-2v-2M3 12h18", lock: "M5 11h14v10H5zM8 11V7a4 4 0 1 1 8 0v4", spinner: "M12 2v4M12 18v4M4.9 4.9l2.8 2.8M16.3 16.3l2.8 2.8M2 12h4M18 12h4M4.9 19.1l2.8-2.8M16.3 7.7l2.8-2.8", beaker: "M9 3h6M10 3v6l-5.4 9.3A1.8 1.8 0 0 0 6.2 21h11.6a1.8 1.8 0 0 0 1.6-2.7L14 9V3M8 14h8", hash: "M4 9h16M4 15h16M10 3 8 21M16 3l-2 18", link: "M9 7a4 4 0 0 1 0 8h-1M15 17a4 4 0 0 1 0-8h1M8 12h8", sparkles: "M12 3l1.8 4.8L18.6 9l-4.8 1.2L12 15l-1.8-4.8L5.4 9l4.8-1.2zM19 14l.9 2.4 2.4.9-2.4.9L19 21l-.9-2.8-2.4-.9 2.4-.9z", download: "M12 3v12M7 10l5 5 5-5M5 21h14", sheet: "M4 3h16v18H4zM4 9h16M4 15h16M10 3v18", }; function Icon({ name, size = 18, className = "", style = {} }) { const d = ICON_PATHS[name] || ICON_PATHS.dot; return h("svg", { className: "icon " + className, width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.8, strokeLinecap: "round", strokeLinejoin: "round", style, }, h("path", { d })); } /* ---- Status pill --------------------------------------------------------- */ function Pill({ tone = "neutral", children, dot = true, soft = true }) { return h("span", { className: `pill pill-${tone}${soft ? " pill-soft" : ""}` }, dot && h("span", { className: "pill-dot" }), children); } function StatusPill({ status }) { return h(Pill, { tone: status.tone }, status.label); } /* ---- Buttons ------------------------------------------------------------- */ function Button({ variant = "default", size = "md", icon, iconRight, children, className = "", ...rest }) { return h("button", { className: `btn btn-${variant} btn-${size} ${className}`, ...rest }, icon && h(Icon, { name: icon, size: size === "sm" ? 15 : 16 }), children && h("span", null, children), iconRight && h(Icon, { name: iconRight, size: size === "sm" ? 15 : 16 })); } /* ---- Card ---------------------------------------------------------------- */ function Card({ title, subtitle, actions, children, pad = true, className = "" }) { return h("section", { className: "card " + className }, (title || actions) && h("div", { className: "card-head" }, h("div", null, title && h("h3", { className: "card-title" }, title), subtitle && h("p", { className: "card-sub" }, subtitle)), actions && h("div", { className: "card-actions" }, actions)), h("div", { className: pad ? "card-body" : "" }, children)); } /* ---- Stat tile ----------------------------------------------------------- */ function Stat({ label, value, unit, delta, tone, spark, icon }) { return h("div", { className: "stat" }, h("div", { className: "stat-top" }, h("span", { className: "stat-label" }, label), icon && h(Icon, { name: icon, size: 16, className: "stat-icon" })), h("div", { className: "stat-value" }, value, unit && h("span", { className: "stat-unit" }, unit)), delta && h("div", { className: `stat-delta ${tone || ""}` }, delta), spark && h(Sparkline, { data: spark, tone })); } /* ---- Sparkline + bars ---------------------------------------------------- */ function Sparkline({ data, tone = "accent", height = 32, fill = true }) { const w = 120, h2 = height, max = Math.max(...data, 1), min = Math.min(...data, 0); const rng = max - min || 1; const pts = data.map((v, i) => [i / (data.length - 1) * w, h2 - ((v - min) / rng) * (h2 - 4) - 2]); const line = pts.map((p, i) => (i ? "L" : "M") + p[0].toFixed(1) + " " + p[1].toFixed(1)).join(" "); const area = line + ` L${w} ${h2} L0 ${h2} Z`; const col = `var(--${tone})`; return h("svg", { className: "spark", viewBox: `0 0 ${w} ${h2}`, preserveAspectRatio: "none", width: "100%", height: h2 }, fill && h("path", { d: area, fill: col, opacity: 0.1 }), h("path", { d: line, fill: "none", stroke: col, strokeWidth: 1.8 })); } function MiniBars({ data, tone = "accent", height = 36 }) { const max = Math.max(...data, 1); return h("div", { className: "minibars", style: { height } }, data.map((v, i) => h("span", { key: i, className: "minibar", style: { height: `${(v / max) * 100}%`, background: `var(--${tone})` }, }))); } /* ---- Progress ------------------------------------------------------------ */ function Bar({ value, max = 100, tone = "accent", height = 7, segments }) { if (segments) { const total = segments.reduce((a, s) => a + s.value, 0) || 1; return h("div", { className: "bar", style: { height } }, segments.map((s, i) => h("span", { key: i, className: "bar-fill", style: { width: `${(s.value / total) * 100}%`, background: `var(--${s.tone})` }, title: `${s.label}: ${s.value}`, }))); } return h("div", { className: "bar", style: { height } }, h("span", { className: "bar-fill", style: { width: `${Math.min(100, (value / max) * 100)}%`, background: `var(--${tone})` } })); } /* ---- Tabs ---------------------------------------------------------------- */ function Tabs({ tabs, active, onChange }) { return h("div", { className: "tabs" }, tabs.map((t) => h("button", { key: t.key, className: "tab" + (active === t.key ? " tab-on" : ""), onClick: () => onChange(t.key), }, t.label, t.count != null && h("span", { className: "tab-count" }, t.count)))); } /* ---- Toggle -------------------------------------------------------------- */ function Toggle({ on, onChange, label }) { return h("button", { className: "toggle" + (on ? " toggle-on" : ""), onClick: () => onChange(!on), type: "button", "aria-label": label || "toggle" }, h("span", { className: "toggle-knob" })); } /* ---- Avatar -------------------------------------------------------------- */ function Avatar({ initials, size = 30, tone = "accent" }) { return h("span", { className: "avatar", style: { width: size, height: size, fontSize: size * 0.4, background: `var(--${tone}-soft)`, color: `var(--${tone}-ink)` } }, initials); } /* ---- Code chip / mono ---------------------------------------------------- */ function Mono({ children, copy, className = "" }) { const [done, setDone] = useState(false); return h("span", { className: "mono " + className }, h("span", null, children), copy && h("button", { className: "mono-copy", title: "Copy", onClick: (e) => { e.stopPropagation(); navigator.clipboard?.writeText(String(children)); setDone(true); setTimeout(() => setDone(false), 1000); }, }, h(Icon, { name: done ? "check" : "copy", size: 12 }))); } /* ---- Empty state --------------------------------------------------------- */ function Empty({ icon = "search", title, sub }) { return h("div", { className: "empty" }, h(Icon, { name: icon, size: 28 }), h("p", { className: "empty-title" }, title), sub && h("p", { className: "empty-sub" }, sub)); } /* ---- Modal / Drawer ------------------------------------------------------ */ function Drawer({ open, onClose, title, sub, children, width = 720, footer }) { useEffect(() => { const f = (e) => e.key === "Escape" && onClose(); if (open) window.addEventListener("keydown", f); return () => window.removeEventListener("keydown", f); }, [open]); if (!open) return null; return h("div", { className: "drawer-scrim", onClick: onClose }, h("div", { className: "drawer", style: { width }, onClick: (e) => e.stopPropagation() }, h("div", { className: "drawer-head" }, h("div", null, h("h2", { className: "drawer-title" }, title), sub && h("div", { className: "drawer-sub" }, sub)), h("button", { className: "icon-btn", onClick: onClose }, h(Icon, { name: "x", size: 18 }))), h("div", { className: "drawer-body" }, children), footer && h("div", { className: "drawer-foot" }, footer))); } function Modal({ open, onClose, title, children, footer, width = 480 }) { if (!open) return null; return h("div", { className: "modal-scrim", onClick: onClose }, h("div", { className: "modal", style: { width }, onClick: (e) => e.stopPropagation() }, h("div", { className: "modal-head" }, h("h2", { className: "modal-title" }, title), h("button", { className: "icon-btn", onClick: onClose }, h(Icon, { name: "x", size: 18 }))), h("div", { className: "modal-body" }, children), footer && h("div", { className: "modal-foot" }, footer))); } /* ---- Field --------------------------------------------------------------- */ function Field({ label, hint, children }) { return h("label", { className: "field" }, label && h("span", { className: "field-label" }, label), children, hint && h("span", { className: "field-hint" }, hint)); } function Select({ value, onChange, options, ...rest }) { return h("select", { className: "input", value, onChange: (e) => onChange(e.target.value), ...rest }, options.map((o) => h("option", { key: o.value ?? o, value: o.value ?? o }, o.label ?? o))); } /* ---- Toast host ---------------------------------------------------------- */ const ToastCtx = React.createContext(() => {}); function ToastHost({ children }) { const [items, setItems] = useState([]); const push = (msg, tone = "ok") => { const id = Math.random().toString(36).slice(2); setItems((x) => [...x, { id, msg, tone }]); setTimeout(() => setItems((x) => x.filter((t) => t.id !== id)), 3200); }; return h(ToastCtx.Provider, { value: push }, children, h("div", { className: "toast-host" }, items.map((t) => h("div", { key: t.id, className: `toast toast-${t.tone}` }, h(Icon, { name: t.tone === "bad" ? "alert" : "check", size: 16 }), h("span", null, t.msg))))); } const useToast = () => React.useContext(ToastCtx); /* ---- Helpers ------------------------------------------------------------- */ function fmtBytes(n) { if (n < 1024) return n + " B"; if (n < 1048576) return (n / 1024).toFixed(0) + " KB"; if (n < 1073741824) return (n / 1048576).toFixed(1) + " MB"; return (n / 1073741824).toFixed(2) + " GB"; } function fmtAgo(date) { const s = Math.round((window.ENO.NOW - date.getTime()) / 1000); if (s < 60) return s + "s ago"; if (s < 3600) return Math.round(s / 60) + "m ago"; if (s < 86400) return Math.round(s / 3600) + "h ago"; return Math.round(s / 86400) + "d ago"; } function fmtTime(date) { return date.toISOString().slice(11, 19) + " UTC"; } function fmtDate(date) { return date.toISOString().slice(0, 10); } function fmtNum(n) { return n.toLocaleString("en-US"); } Object.assign(window, { Icon, Pill, StatusPill, Button, Card, Stat, Sparkline, MiniBars, Bar, Tabs, Toggle, Avatar, Mono, Empty, Drawer, Modal, Field, Select, ToastHost, useToast, fmtBytes, fmtAgo, fmtTime, fmtDate, fmtNum, });