"use client"; /* Forge shared UI primitives - ported from the design handoff (primitives.jsx). */ import { CSSProperties, ReactNode, useEffect, useMemo, useRef, useState } from "react"; import { createPortal } from "react-dom"; import { Icon } from "./icons"; /* ---------------- Sparkline ---------------- */ export function Sparkline({ data, w = 80, h = 24, color = "var(--accent)", fill = true, strokeW = 1.5, }: { data: number[]; w?: number; h?: number; color?: string; fill?: boolean; strokeW?: number }) { const 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, h - 2 - ((v - min) / rng) * (h - 4)]); const d = pts.map((p, i) => (i ? "L" : "M") + p[0].toFixed(1) + " " + p[1].toFixed(1)).join(" "); const area = d + ` L${w} ${h} L0 ${h} Z`; const gid = useMemo(() => "sg" + Math.random().toString(36).slice(2, 7), []); return ( {fill && ( )} {fill && } ); } /* ---------------- Donut ---------------- */ export function Donut({ segments, size = 120, thickness = 16, center, }: { segments: { value: number; color: string }[]; size?: number; thickness?: number; center?: ReactNode }) { const total = segments.reduce((a, s) => a + s.value, 0) || 1; const R = (size - thickness) / 2, C = 2 * Math.PI * R; let off = 0; return (
{segments.map((s, i) => { const len = (s.value / total) * C; const el = ( ); off += len; return el; })} {center && (
{center}
)}
); } /* ---------------- TokenMeter (signature) ---------------- */ export function TokenMeter({ raw, projected, max, compact = false, animateKey, }: { raw: number; projected: number; max?: number; compact?: boolean; animateKey?: any }) { const cap = max || Math.max(raw, projected, 1) * 1.1; const [shown, setShown] = useState<"raw" | "proj">("raw"); useEffect(() => { setShown("raw"); const t = setTimeout(() => setShown("proj"), 420); return () => clearTimeout(t); }, [animateKey]); const pctRaw = Math.min(100, (raw / cap) * 100); const pctProj = Math.min(100, (projected / cap) * 100); const saved = raw > 0 ? Math.round((1 - projected / raw) * 100) : 0; if (compact) { return (
{projected} {saved > 0 && {"−" + saved + "%"}}
); } return (
Context cost {(shown === "proj" ? projected : raw).toLocaleString()} tok
raw {raw.toLocaleString()} → projected {projected.toLocaleString()} {saved > 0 && {saved + "% saved"}}
); } /* ---------------- StatusPill ---------------- */ export function StatusPill({ status, label }: { status: string; label?: string }) { // Minimal, professional statuses: neutral pills with no colour dots. Only problem // states (error / fail / interrupted) keep a subtle tint so they still stand out. const map: Record = { done: ["pill-muted", "Done"], pass: ["pill-muted", "Passing"], active: ["pill-muted", "Active"], ready: ["pill-muted", "Ready"], running: ["pill-muted", "Running"], processing: ["pill-muted", "Processing"], draft: ["pill-muted", "Draft"], untested: ["pill-muted", "Untested"], error: ["pill-err", "Error"], fail: ["pill-err", "Failing"], interrupted: ["pill-warn", "Interrupted"], }; const [cls, def] = map[status] || ["pill-muted", status]; return {label || def}; } /* ---------------- Avatar ---------------- */ export function Avatar({ name, size = 26, color }: { name: string; size?: number; color?: string }) { const init = (name || "?").split(/\s|_|-/).filter(Boolean).slice(0, 2).map((s) => s[0]).join("").toUpperCase(); const hue = useMemo(() => { let h = 0; for (const c of name || "") h = (h * 31 + c.charCodeAt(0)) % 360; return h; }, [name]); return (
{init}
); } /* ---------------- Toggle ---------------- */ export function Toggle({ on, onChange, signal }: { on: boolean; onChange?: (v: boolean) => void; signal?: boolean }) { return ( ); })}
); } /* ---------------- Modal ---------------- */ export function Modal({ open, onClose, children, width = 520, title, footer }: { open: boolean; onClose: () => void; children: ReactNode; width?: number; title?: string; footer?: ReactNode }) { // Portal to so the fixed overlay escapes any transformed ancestor (e.g. the // `.fade-up` screen wrapper). A transformed ancestor becomes the containing block for // `position:fixed`, which otherwise traps the modal inside the content column and clips // its header. `mounted` avoids touching `document` during SSR / first hydration. const [mounted, setMounted] = useState(false); useEffect(() => setMounted(true), []); useEffect(() => { if (!open) return; const h = (e: KeyboardEvent) => e.key === "Escape" && onClose(); window.addEventListener("keydown", h); return () => window.removeEventListener("keydown", h); }, [open, onClose]); if (!open || !mounted) return null; return createPortal(
e.stopPropagation()}> {title && (
{title}
)}
{children}
{footer &&
{footer}
}
, document.body, ); } /* ---------------- Drawer ---------------- */ export function Drawer({ open, onClose, children, width = 440, title, sub }: { open: boolean; onClose: () => void; children: ReactNode; width?: number; title?: string; sub?: string }) { return (
{title && (
{title}
{sub &&
{sub}
}
)}
{children}
); } /* ---------------- EmptyState ---------------- */ export function EmptyState({ icon, title, sub, action }: { icon: string; title: string; sub?: string; action?: ReactNode }) { return (
{title}
{sub &&
{sub}
} {action}
); } /* ---------------- Field ---------------- */ export function Field({ label, help, children, required }: { label?: string; help?: string; children: ReactNode; required?: boolean }) { return (
{label && } {children} {help &&
{help}
}
); } /* ---------------- Tabs ---------------- */ export function Tabs({ tabs, value, onChange, equal }: { tabs: (string | { value: string; label: string; count?: number })[]; value: string; onChange: (v: string) => void; equal?: boolean }) { return (
{tabs.map((t) => { const val = typeof t === "string" ? t : t.value; const lab = typeof t === "string" ? t : t.label; const active = value === val; return ( ); })}
); } /* ---------------- CodeBlock ---------------- */ export function CodeBlock({ code, copyable = true, maxHeight }: { code: string; lang?: string; copyable?: boolean; maxHeight?: number }) { const [copied, setCopied] = useState(false); return (
{copyable && ( )}
        {code}
      
); } /* ---------------- Tile ---------------- */ export function Tile({ icon, color = "var(--accent)", size = 36 }: { icon: string; color?: string; size?: number; glow?: boolean }) { // Bare icon - no background, no border (minimal look, applied app-wide). return (
); } /* ---------------- Menu ---------------- */ export function Menu({ trigger, items, align = "right" }: { trigger: ReactNode; items: { label?: string; icon?: string; onClick?: () => void; danger?: boolean; divider?: boolean }[]; align?: "left" | "right" }) { const [open, setOpen] = useState(false); const ref = useRef(null); useEffect(() => { if (!open) return; const h = (e: MouseEvent) => { if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false); }; window.addEventListener("mousedown", h); return () => window.removeEventListener("mousedown", h); }, [open]); const alignStyle: CSSProperties = align === "right" ? { right: 0 } : { left: 0 }; return (
setOpen((o) => !o)}>{trigger}
{open && (
{items.map((it, i) => it.divider ? (
) : ( ), )}
)}
); }