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