diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js
new file mode 100644
index 0000000..2e7af2b
--- /dev/null
+++ b/frontend/postcss.config.js
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/frontend/src/design-system/tokens.css b/frontend/src/design-system/tokens.css
new file mode 100644
index 0000000..348bfe2
--- /dev/null
+++ b/frontend/src/design-system/tokens.css
@@ -0,0 +1,204 @@
+/* ============================================================
+ ui-tokens.css
+ Design tokens Gruvbox Seventies — dark (par défaut) + light.
+ Sombre délavé (pas noir intense) / gris clair usé (pas blanc pur).
+ ============================================================ */
+
+:root,
+[data-theme="dark"] {
+ /* Couches de fond — sombre délavé, brun-gris chaud */
+ --bg-0: #221c17; /* niveau le plus profond (rare) */
+ --bg-1: #2a231d; /* fond app */
+ --bg-2: #322a23; /* panneaux */
+ --bg-3: #3c332a; /* cartes */
+ --bg-4: #4a4035; /* hover */
+ --bg-5: #5a4f43; /* press / actif */
+
+ /* Surfaces translucides */
+ --surf-glass: rgba(50, 42, 35, 0.72);
+ --surf-glass-strong: rgba(50, 42, 35, 0.92);
+ --surf-glass-soft: rgba(50, 42, 35, 0.42);
+
+ /* Bordures */
+ --border-1: rgba(168, 153, 132, 0.18);
+ --border-2: rgba(168, 153, 132, 0.32);
+ --border-3: rgba(168, 153, 132, 0.55);
+
+ /* Texte */
+ --ink-1: #f2e5c7; /* cream principal */
+ --ink-2: #d5c4a1; /* secondaire */
+ --ink-3: #a89984; /* labels / hints */
+ --ink-4: #7c6f64; /* désactivé */
+
+ /* Accent orange seventies */
+ --accent: #fe8019;
+ --accent-soft: #d65d0e;
+ --accent-glow: rgba(254, 128, 25, 0.35);
+ --accent-tint: rgba(254, 128, 25, 0.12);
+
+ /* Statuts */
+ --ok: #4dbb26;
+ --ok-glow: rgba(77, 187, 38, 0.45);
+ --warn: #fabd2f;
+ --warn-glow: rgba(250, 189, 47, 0.45);
+ --err: #fb4934;
+ --err-glow: rgba(251, 73, 52, 0.4);
+ --info: #83a598;
+ --info-glow: rgba(131, 165, 152, 0.4);
+
+ /* Couleurs additionnelles (datavis, badges, catégories) */
+ --blue: #3db0d1;
+ --blue-glow: rgba(61, 176, 209, 0.45);
+ --purple: #c882c8;
+ --purple-glow: rgba(200, 130, 200, 0.45);
+
+ /* Ombres */
+ --shadow-1: 0 1px 2px rgba(0,0,0,0.4);
+ --shadow-2: 0 4px 12px rgba(0,0,0,0.45);
+ --shadow-3: 0 12px 32px rgba(0,0,0,0.55);
+ --shadow-press: inset 0 2px 4px rgba(0,0,0,0.5);
+
+ /* Relief 3D pour tuiles : highlight haut + ombre bas + ombre portée */
+ --tile-3d:
+ inset 0 1px 0 rgba(255, 230, 180, 0.12),
+ inset 0 -1px 0 rgba(0, 0, 0, 0.45),
+ 0 1px 0 rgba(0, 0, 0, 0.35),
+ 0 2px 4px rgba(0, 0, 0, 0.4),
+ 0 8px 18px rgba(0, 0, 0, 0.5);
+ --tile-3d-strong:
+ inset 0 1px 0 rgba(255, 230, 180, 0.18),
+ inset 0 -2px 0 rgba(0, 0, 0, 0.55),
+ 0 1px 0 rgba(0, 0, 0, 0.4),
+ 0 4px 8px rgba(0, 0, 0, 0.5),
+ 0 14px 28px rgba(0, 0, 0, 0.55);
+
+ /* Polices */
+ --font-ui: 'Inter', system-ui, -apple-system, sans-serif;
+ --font-mono: 'JetBrains Mono', ui-monospace, monospace;
+ --font-terminal: 'Share Tech Mono', 'VT323', 'Courier New', monospace;
+}
+
+[data-theme="light"] {
+ /* Gris clair usé, légèrement chaud (pas blanc pur) */
+ --bg-0: #b8b2a3;
+ --bg-1: #d5d0c5;
+ --bg-2: #dcd7cc;
+ --bg-3: #e3ded3;
+ --bg-4: #ccc6b8;
+ --bg-5: #bdb6a7;
+
+ --surf-glass: rgba(220, 215, 204, 0.72);
+ --surf-glass-strong: rgba(220, 215, 204, 0.94);
+ --surf-glass-soft: rgba(220, 215, 204, 0.42);
+
+ --border-1: rgba(60, 56, 54, 0.15);
+ --border-2: rgba(60, 56, 54, 0.28);
+ --border-3: rgba(60, 56, 54, 0.5);
+
+ --ink-1: #28241f;
+ --ink-2: #3c3836;
+ --ink-3: #5a544c;
+ --ink-4: #8a8278;
+
+ --accent: #af3a03;
+ --accent-soft: #d65d0e;
+ --accent-glow: rgba(175, 58, 3, 0.28);
+ --accent-tint: rgba(175, 58, 3, 0.08);
+
+ --ok: #3c911c;
+ --ok-glow: rgba(60, 145, 28, 0.32);
+ --warn: #b57614;
+ --warn-glow: rgba(181, 118, 20, 0.35);
+ --err: #9d0006;
+ --err-glow: rgba(157, 0, 6, 0.3);
+ --info: #427b58;
+ --info-glow: rgba(66, 123, 88, 0.3);
+
+ /* Couleurs additionnelles (datavis, badges, catégories) */
+ --blue: #2d82a3;
+ --blue-glow: rgba(45, 130, 163, 0.32);
+ --purple: #8c468c;
+ --purple-glow: rgba(140, 70, 140, 0.32);
+
+ --shadow-1: 0 1px 2px rgba(40,30,20,0.12);
+ --shadow-2: 0 4px 12px rgba(40,30,20,0.15);
+ --shadow-3: 0 12px 32px rgba(40,30,20,0.2);
+ --shadow-press: inset 0 2px 4px rgba(40,30,20,0.18);
+
+ /* Relief light : highlight haut blanc cassé + ombre marquée */
+ --tile-3d:
+ inset 0 1px 0 rgba(255, 255, 255, 0.55),
+ inset 0 -1px 0 rgba(60, 50, 40, 0.18),
+ 0 1px 0 rgba(60, 50, 40, 0.1),
+ 0 2px 4px rgba(60, 50, 40, 0.12),
+ 0 8px 18px rgba(60, 50, 40, 0.18);
+ --tile-3d-strong:
+ inset 0 1px 0 rgba(255, 255, 255, 0.7),
+ inset 0 -2px 0 rgba(60, 50, 40, 0.22),
+ 0 1px 0 rgba(60, 50, 40, 0.15),
+ 0 4px 8px rgba(60, 50, 40, 0.18),
+ 0 14px 28px rgba(60, 50, 40, 0.22);
+}
+
+/* ============================================================
+ Reset minimal + base typo
+ ============================================================ */
+* { box-sizing: border-box; }
+html, body { margin: 0; padding: 0; }
+
+body {
+ font-family: var(--font-ui);
+ font-size: 14px;
+ color: var(--ink-1);
+ background: var(--bg-1);
+ -webkit-font-smoothing: antialiased;
+}
+
+.mono { font-family: var(--font-mono); }
+.terminal { font-family: var(--font-terminal); letter-spacing: 0.02em; }
+.label {
+ font-family: var(--font-mono);
+ font-size: 11px;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ color: var(--ink-3);
+}
+
+/* ============================================================
+ Surfaces — relief 3D marqué, AUCUN effet hover
+ ============================================================ */
+.glass {
+ background: var(--surf-glass);
+ backdrop-filter: blur(12px) saturate(140%);
+ -webkit-backdrop-filter: blur(12px) saturate(140%);
+ border: 1px solid var(--border-2);
+ box-shadow: var(--tile-3d);
+}
+.glass-strong {
+ background: var(--surf-glass-strong);
+ backdrop-filter: blur(16px) saturate(150%);
+ -webkit-backdrop-filter: blur(16px) saturate(150%);
+ border: 1px solid var(--border-3);
+ box-shadow: var(--tile-3d-strong);
+}
+
+/* Élément cliquable : pas de hover, mais réelle pression 3D au clic */
+.interactive {
+ cursor: pointer;
+ transition: transform .04s ease-out, box-shadow .04s, background .04s;
+ transform: translateY(0);
+}
+.interactive:active {
+ transform: translateY(1px);
+ box-shadow: var(--shadow-press) !important;
+ filter: brightness(0.92);
+}
+
+/* Scrollbar custom */
+*::-webkit-scrollbar { width: 8px; height: 8px; }
+*::-webkit-scrollbar-track { background: transparent; }
+*::-webkit-scrollbar-thumb {
+ background: var(--border-2);
+ border-radius: 4px;
+}
+*::-webkit-scrollbar-thumb:hover { background: var(--accent-soft); }
diff --git a/frontend/src/design-system/ui-kit.tsx b/frontend/src/design-system/ui-kit.tsx
new file mode 100644
index 0000000..d8b9747
--- /dev/null
+++ b/frontend/src/design-system/ui-kit.tsx
@@ -0,0 +1,669 @@
+// @ts-nocheck
+/* ============================================================
+ ui-kit.tsx
+ Composants haute-fid Gruvbox Seventies.
+ Tout est purement décoratif/interactif côté composant.
+ Effets : transparence (glass), hover glow, click 3D, tooltips.
+ Adapté de ui-kit.jsx (UMD → ESM TypeScript)
+ ============================================================ */
+
+import { useState, useRef, useEffect } from '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 (
+
+ );
+}
+
+/* Effets hover sur les jauges (sans effet au clic) */
+(function injectGaugeHoverStyles() {
+ if (typeof document === 'undefined') return;
+ 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);
+})();
+
+/* Exports nommés ESM */
+export {
+ Icon,
+ Tooltip,
+ IconButton,
+ Toggle,
+ StatusLed,
+ BatteryGauge,
+ RadialGauge,
+ BigRadialGauge,
+ Popup,
+ Button,
+ TreeNav,
+ Sparkline,
+ LineChart,
+}
diff --git a/frontend/src/index.css b/frontend/src/index.css
index 4312d8f..dc0085b 100644
--- a/frontend/src/index.css
+++ b/frontend/src/index.css
@@ -1,4 +1,16 @@
-/* sera complété à la Tâche 10 */
+@import './design-system/tokens.css';
+
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+* {
+ box-sizing: border-box;
+}
+
body {
+ font-family: var(--font-ui);
+ background-color: var(--bg-1);
+ color: var(--ink-1);
margin: 0;
}
diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts
new file mode 100644
index 0000000..4e33cb8
--- /dev/null
+++ b/frontend/tailwind.config.ts
@@ -0,0 +1,38 @@
+import type { Config } from 'tailwindcss'
+
+export default {
+ content: ['./index.html', './src/**/*.{ts,tsx}'],
+ theme: {
+ extend: {
+ colors: {
+ accent: 'var(--accent)',
+ 'accent-soft': 'var(--accent-soft)',
+ 'bg-0': 'var(--bg-0)',
+ 'bg-1': 'var(--bg-1)',
+ 'bg-2': 'var(--bg-2)',
+ 'bg-3': 'var(--bg-3)',
+ 'bg-4': 'var(--bg-4)',
+ 'bg-5': 'var(--bg-5)',
+ 'ink-1': 'var(--ink-1)',
+ 'ink-2': 'var(--ink-2)',
+ 'ink-3': 'var(--ink-3)',
+ 'ink-4': 'var(--ink-4)',
+ ok: 'var(--ok)',
+ warn: 'var(--warn)',
+ err: 'var(--err)',
+ info: 'var(--info)',
+ blue: 'var(--blue)',
+ purple: 'var(--purple)',
+ },
+ fontFamily: {
+ ui: ['Inter', 'system-ui', 'sans-serif'],
+ mono: ['"JetBrains Mono"', 'monospace'],
+ terminal: ['"Share Tech Mono"', 'monospace'],
+ },
+ minHeight: {
+ touch: '48px',
+ },
+ },
+ },
+ plugins: [],
+} satisfies Config