diff --git a/components/ui-kit.jsx b/components/ui-kit.jsx
new file mode 100644
index 0000000..92f9e76
--- /dev/null
+++ b/components/ui-kit.jsx
@@ -0,0 +1,656 @@
+/* ============================================================
+ ui-kit.jsx
+ Composants haute-fid Gruvbox Seventies.
+ Tout est purement décoratif/interactif côté composant.
+ Effets : transparence (glass), hover glow, click 3D, tooltips.
+ ============================================================ */
+
+const { useState, useRef, useEffect } = React;
+
+/* ============================================================
+ Icônes — Font Awesome 6 Free.
+ Mapping nom logique → classe FA. Le CSS de FA est chargé en CDN
+ dans le
. Le composant garde la MÊME API qu'avant (name,
+ size, style) pour ne rien casser ailleurs.
+ ============================================================ */
+const ICON_MAP = {
+ cpu: 'microchip',
+ memory: 'memory',
+ disk: 'hard-drive',
+ network: 'network-wired',
+ clock: 'clock',
+ grid: 'table-cells',
+ list: 'list',
+ cog: 'gear',
+ alert: 'triangle-exclamation',
+ bell: 'bell',
+ server: 'server',
+ chart: 'chart-line',
+ bars: 'chart-simple',
+ terminal: 'terminal',
+ refresh: 'arrows-rotate',
+ play: 'play',
+ pause: 'pause',
+ power: 'power-off',
+ sun: 'sun',
+ moon: 'moon',
+ search: 'magnifying-glass',
+ close: 'xmark',
+ chevR: 'chevron-right',
+ chevL: 'chevron-left',
+ chevD: 'chevron-down',
+ chevU: 'chevron-up',
+ plus: 'plus',
+ filter: 'filter',
+ download: 'download',
+ folder: 'folder',
+ node: 'circle-nodes',
+ user: 'user',
+};
+
+const Icon = ({ name, size = 16, style }) => {
+ const fa = ICON_MAP[name] || 'circle-question';
+ return (
+
+ );
+};
+
+/* ============================================================
+ Tooltip — apparaît au hover après 300ms, position auto.
+ ============================================================ */
+function Tooltip({ children, label, side = 'top' }) {
+ const [show, setShow] = useState(false);
+ const t = useRef();
+ const onEnter = () => { t.current = setTimeout(() => setShow(true), 280); };
+ const onLeave = () => { clearTimeout(t.current); setShow(false); };
+ const sides = {
+ top: { bottom: 'calc(100% + 8px)', left: '50%', transform: 'translateX(-50%)' },
+ bottom: { top: 'calc(100% + 8px)', left: '50%', transform: 'translateX(-50%)' },
+ left: { right: 'calc(100% + 8px)', top: '50%', transform: 'translateY(-50%)' },
+ right: { left: 'calc(100% + 8px)', top: '50%', transform: 'translateY(-50%)' },
+ };
+ return (
+
+ {children}
+ {show && (
+ {label}
+ )}
+
+ );
+}
+
+/* ============================================================
+ IconButton — bouton icône seul + tooltip obligatoire.
+ ============================================================ */
+function IconButton({ icon, label, onClick, active, danger, size = 34, primary }) {
+ const bg = active ? 'var(--accent-tint)'
+ : primary ? 'var(--accent)'
+ : 'var(--bg-3)';
+ const fg = active ? 'var(--accent)'
+ : primary ? 'var(--bg-1)'
+ : danger ? 'var(--err)'
+ : 'var(--ink-2)';
+ const bd = active ? 'var(--accent-soft)' : 'var(--border-2)';
+ return (
+
+
+
+ );
+}
+
+/* ============================================================
+ Toggle on/off — switch tactile avec glow accent quand ON
+ ============================================================ */
+function Toggle({ on, onChange, label, icon }) {
+ return (
+
+ {icon && }
+ {label && {label}}
+
+
+ );
+}
+
+/* ============================================================
+ Status LED — pastille pulsante (effet halo si critique)
+ ============================================================ */
+function StatusLed({ status = 'ok', size = 10, pulse }) {
+ const map = {
+ ok: { c: 'var(--ok)', g: 'var(--ok-glow)' },
+ warn: { c: 'var(--warn)', g: 'var(--warn-glow)' },
+ err: { c: 'var(--err)', g: 'var(--err-glow)' },
+ off: { c: 'var(--ink-4)', g: 'transparent' },
+ info: { c: 'var(--info)', g: 'var(--info-glow)' },
+ };
+ const { c, g } = map[status];
+ const id = `pulse-${status}-${size}`;
+ return (
+ <>
+ {pulse && (
+
+ )}
+
+ >
+ );
+}
+
+/* ============================================================
+ BatteryGauge — jauge horizontale style batterie
+ - Pas de bandes (couleur unie + léger gloss interne)
+ - Pas de graduations verticales
+ - Hover : glow lumineux dans la couleur de la jauge
+ - Mode compact : label [bar] valeur sur une seule ligne
+ ============================================================ */
+function BatteryGauge({ value = 60, label, max = 100, unit = '%', warnAt = 70, errAt = 90, height = 22, compact = false, color: colorOverride, icon }) {
+ const pct = Math.max(0, Math.min(100, (value / max) * 100));
+ const color = colorOverride
+ || (pct >= errAt ? 'var(--err)' : pct >= warnAt ? 'var(--warn)' : 'var(--ok)');
+ const glowVar = pct >= errAt ? 'var(--err-glow)'
+ : pct >= warnAt ? 'var(--warn-glow)'
+ : 'var(--ok-glow)';
+
+ // Variante compacte : label [bar] valeur sur une seule ligne
+ if (compact) {
+ return (
+
+ {(icon || label) && (
+
+ {icon && }
+ {label && {label}}
+
+ )}
+
+
+ {value}{unit}
+
+
+ );
+ }
+
+ return (
+
+ {label && (
+
+ {label}
+
+ {value}{unit}
+
+
+ )}
+
+
+ {/* Gloss interne très léger (un seul highlight haut, pas de bande inférieure) */}
+
+
+
+ );
+}
+
+/* ============================================================
+ RadialGauge — jauge ronde, version épurée
+ ============================================================ */
+function RadialGauge({ value = 64, label, size = 120, warnAt = 70, errAt = 90 }) {
+ const pct = Math.max(0, Math.min(100, value));
+ const color = pct >= errAt ? 'var(--err)' : pct >= warnAt ? 'var(--warn)' : 'var(--ok)';
+ const glow = pct >= errAt ? 'var(--err-glow)' : pct >= warnAt ? 'var(--warn-glow)' : 'var(--ok-glow)';
+ const r = size / 2 - 10;
+ const cx = size / 2;
+ const cy = size / 2 + 6;
+ const circ = Math.PI * r;
+ const offset = circ - (pct / 100) * circ;
+ return (
+
+
+
+
+ {value}%
+
+ {label &&
{label}
}
+
+
+ );
+}
+
+/* ============================================================
+ BigRadialGauge — la grande jauge cockpit "santé système"
+ ============================================================ */
+function BigRadialGauge({ value = 87, label = 'score santé · stable' }) {
+ const size = 320;
+ const r = 130;
+ const cx = size / 2;
+ const cy = size / 2 + 30;
+ const circ = Math.PI * r;
+ const offset = circ - (value / 100) * circ;
+ const color = value >= 80 ? 'var(--ok)' : value >= 50 ? 'var(--warn)' : 'var(--err)';
+ return (
+
+
+
+
+ );
+}
+
+/* ============================================================
+ Popup — modale glassmorphism centrée + bouton fermer
+ ============================================================ */
+function Popup({ open, onClose, title, children, footer, width = 460 }) {
+ if (!open) return null;
+ return (
+
+
+
e.stopPropagation()} style={{
+ width, maxWidth: '90%',
+ borderRadius: 12,
+ boxShadow: 'var(--shadow-3)',
+ animation: 'popin .25s cubic-bezier(.3,.7,.3,1.2)',
+ overflow: 'hidden',
+ }}>
+
+
{children}
+ {footer && (
+
{footer}
+ )}
+
+
+ );
+}
+
+/* ============================================================
+ Button — bouton classique avec variantes
+ ============================================================ */
+function Button({ children, icon, onClick, variant = 'default', size = 'md' }) {
+ const sizes = {
+ sm: { padding: '5px 10px', fontSize: 12, h: 28 },
+ md: { padding: '7px 14px', fontSize: 13, h: 34 },
+ lg: { padding: '10px 18px', fontSize: 14, h: 40 },
+ }[size];
+ const variants = {
+ default: { bg: 'var(--bg-3)', fg: 'var(--ink-1)', bd: 'var(--border-2)' },
+ primary: { bg: 'var(--accent)', fg: 'var(--bg-1)', bd: 'var(--accent-soft)' },
+ ghost: { bg: 'transparent', fg: 'var(--ink-2)', bd: 'var(--border-2)' },
+ danger: { bg: 'var(--bg-3)', fg: 'var(--err)', bd: 'var(--err)' },
+ }[variant];
+ return (
+
+ );
+}
+
+/* ============================================================
+ TreeNav — arbre dépliable avec icône en tête (style B)
+ ============================================================ */
+function TreeNav({ groups, activeId, onSelect }) {
+ const [open, setOpen] = useState(() =>
+ Object.fromEntries(groups.map(g => [g.id, g.open !== false]))
+ );
+ return (
+
+ {groups.map(g => (
+
+
setOpen({ ...open, [g.id]: !open[g.id] })}
+ style={{
+ display: 'flex', alignItems: 'center', gap: 8,
+ padding: '7px 8px', borderRadius: 6,
+ color: 'var(--ink-2)',
+ background: 'transparent',
+ border: '1px solid transparent',
+ cursor: 'pointer',
+ }}>
+
+
+ {g.label}
+ {g.count != null && (
+
+ {g.count}
+
+ )}
+
+ {open[g.id] && (
+
+ {g.children.map(c => {
+ const active = c.id === activeId;
+ return (
+
onSelect && onSelect(c.id)}
+ style={{
+ display: 'flex', alignItems: 'center', gap: 8,
+ padding: '6px 10px', borderRadius: 6,
+ background: active ? 'var(--accent-tint)' : 'transparent',
+ color: active ? 'var(--ink-1)' : 'var(--ink-2)',
+ borderLeft: active ? '2px solid var(--accent)' : '2px solid transparent',
+ marginLeft: active ? 0 : 2,
+ fontSize: 12.5,
+ }}>
+
+ {c.label}
+ {c.meta && {c.meta}}
+
+ );
+ })}
+
+ )}
+
+ ))}
+
+ );
+}
+
+/* ============================================================
+ Sparkline pour les KPI
+ ============================================================ */
+function Sparkline({ points = [], color = 'var(--accent)', h = 28 }) {
+ const w = 100;
+ const max = Math.max(...points);
+ const min = Math.min(...points);
+ const range = max - min || 1;
+ const step = w / (points.length - 1);
+ const path = points.map((p, i) =>
+ `${i === 0 ? 'M' : 'L'} ${(i * step).toFixed(1)} ${(h - 2 - ((p - min) / range) * (h - 4)).toFixed(1)}`
+ ).join(' ');
+ const area = path + ` L ${w} ${h} L 0 ${h} Z`;
+ return (
+
+ );
+}
+
+/* ============================================================
+ LineChart — grand graph multi-séries
+ ============================================================ */
+function LineChart({ series, h = 200, labels }) {
+ const w = 600;
+ const padding = { l: 36, r: 12, t: 12, b: 24 };
+ const innerW = w - padding.l - padding.r;
+ const innerH = h - padding.t - padding.b;
+ const all = series.flatMap(s => s.points);
+ const max = Math.max(...all) * 1.1;
+ const min = 0;
+ const range = max - min;
+ const ptsCount = series[0].points.length;
+ const step = innerW / (ptsCount - 1);
+ return (
+
+ );
+}
+
+/* Expose */
+Object.assign(window, {
+ Icon, Tooltip, IconButton, Toggle, StatusLed,
+ BatteryGauge, RadialGauge, BigRadialGauge,
+ Popup, Button, TreeNav, Sparkline, LineChart,
+});
+
+/* Effets hover sur les jauges (sans effet au clic) */
+(function injectGaugeHoverStyles() {
+ if (document.getElementById('gauge-hover-styles')) return;
+ const s = document.createElement('style');
+ s.id = 'gauge-hover-styles';
+ s.textContent = `
+ .bg-hover:hover .bg-bar {
+ border-color: color-mix(in oklch, var(--accent) 60%, var(--border-3));
+ }
+ .bg-hover:hover .bg-fill {
+ box-shadow: 0 0 14px var(--bg-glow, var(--accent-glow));
+ filter: brightness(1.15);
+ }
+ .gauge-hover { transition: filter .2s; }
+ .gauge-hover:hover { filter: drop-shadow(0 0 8px var(--accent-glow)) brightness(1.08); }
+ `;
+ document.head.appendChild(s);
+})();