/* ===========================================================
Forja 3D — shared UI components → window
=========================================================== */
const { useState, useEffect, useRef, useMemo } = React;
/* ---- simple line icons (geometric, stroke-based) ---- */
const ICONS = {
overview: 'M3 3h7v7H3zM14 3h7v7h-7zM14 14h7v7h-7zM3 14h7v7H3z',
calc: 'M5 3h14v18H5zM8 7h8M8 11h2M8 15h2M14 11v6',
spool: 'M12 3a9 9 0 100 18 9 9 0 000-18zM12 9a3 3 0 100 6 3 3 0 000-6zM12 3v3M12 18v3',
money: 'M3 6h18v12H3zM12 9a3 3 0 100 6 3 3 0 000-6M6 9v.01M18 15v.01',
sale: 'M4 13l4-9 8 0 4 9-8 7zM12 4v9',
debt: 'M12 3v18M7 7h7a3 3 0 010 6H7M7 13h8',
box: 'M12 3l8 4.5v9L12 21l-8-4.5v-9zM4 7.5l8 4.5 8-4.5M12 12v9',
history: 'M3 12a9 9 0 109-9 9 9 0 00-9 9zM3 12H1m11-5v5l3 2',
gear: 'M12 9a3 3 0 100 6 3 3 0 000-6zM19 12l2-1-1-3-2 .5-2-2 .5-2-3-1-1 2h-3l-1-2-3 1 .5 2-2 2-2-.5-1 3 2 1v3l-2 1 1 3 2-.5 2 2-.5 2 3 1 1-2h3l1 2 3-1-.5-2 2-2 2 .5 1-3-2-1z',
plus: 'M12 5v14M5 12h14',
close: 'M6 6l12 12M18 6L6 18',
trash: 'M4 7h16M9 7V4h6v3M6 7l1 13h10l1-13',
edit: 'M4 20h4L19 9l-4-4L4 16zM14 6l4 4',
search: 'M11 4a7 7 0 105 12 7 7 0 00-5-12zM21 21l-5-5',
pay: 'M12 5v14M19 12l-7 7-7-7',
bolt: 'M13 2L4 14h7l-1 8 9-12h-7z',
clock: 'M12 3a9 9 0 100 18 9 9 0 000-18zM12 7v5l3 2',
chevron: 'M9 6l6 6-6 6',
menu: 'M4 6h16M4 12h16M4 18h16',
check: 'M5 12l5 5L20 6',
};
function Icon({ name, size = 18, stroke = 1.7, fill = 'none', ...p }) {
return (
);
}
/* ---- Sparkline ---- */
function Sparkline({ data, color = 'var(--orange)' }) {
const max = Math.max(...data, 1);
return (
{data.map((v, i) => (
))}
);
}
/* ---- KPI card ---- */
function Kpi({ label, value, sub, dot = '', spark, sparkColor, delta, deltaDir }) {
return (
{label}
{value}
{delta &&
{delta}
}
{spark &&
}
{sub && !spark &&
{sub}
}
);
}
/* ---- Progress bar ---- */
function Bar({ value, max = 100, thin }) {
const pct = Math.max(0, Math.min(100, (value / max) * 100));
return
;
}
/* ---- Pill ---- */
function Pill({ tone = 'n', children }) { return {children}; }
/* ---- Button ---- */
function Button({ variant = 'ghost', size, icon, children, ...p }) {
return (
);
}
function IconBtn({ icon, danger, ...p }) {
return ;
}
/* ---- Modal (portal) ---- */
function Modal({ open, onClose, title, children, footer, wide }) {
useEffect(() => {
if (!open) return;
const h = (e) => e.key === 'Escape' && onClose();
window.addEventListener('keydown', h);
return () => window.removeEventListener('keydown', h);
}, [open, onClose]);
if (!open) return null;
return ReactDOM.createPortal(
e.target === e.currentTarget && onClose()}>
{title}
{children}
{footer &&
{footer}
}
, document.body);
}
/* ---- Form fields ---- */
function Field({ label, hint, children }) {
return (
{label &&
}
{children}
{hint &&
{hint}
}
);
}
function Text({ value, onChange, ...p }) {
return onChange(e.target.value)} {...p} />;
}
function Num({ value, onChange, prefix, suffix, step = 'any', ...p }) {
return (
{prefix && {prefix}}
onChange(e.target.value === '' ? '' : parseFloat(e.target.value))}
style={suffix ? { paddingRight: 46 } : null} {...p} />
{suffix && {suffix}}
);
}
function Select({ value, onChange, options, ...p }) {
return (
);
}
/* ---- Empty state ---- */
function Empty({ icon = 'box', title, children, action }) {
return (
{title}
{children}
{action}
);
}
/* ---- Page header ---- */
function PageHead({ title, sub, action }) {
return (
<>
>
);
}
/* ---- Confirm helper (window.confirm wrapper for clarity) ---- */
function confirmDel(msg) { return window.confirm(msg); }
Object.assign(window, { Icon, Sparkline, Kpi, Bar, Pill, Button, IconBtn, Modal, Field, Text, Num, Select, Empty, PageHead, confirmDel });