Files
system_update/docs/superpowers/specs/2026-06-05-jalon2-polish-design-system-design.md
T
gilles 8d105b63ec docs: spec jalon 2 - séparation terminal par machine + remontée d'état
Suite au test live: retour d'usage (amelioration.md) sur la séparation
des sorties entre machines distinctes dans le terminal.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 05:05:27 +02:00

10 KiB

Jalon 2 — Polish design system — Design

Spec du deuxième jalon : refonte de l'UI avec le design system Gruvbox seventies. Statut : validé (2026-06-05). Langue de travail : français. Voir aussi : CLAUDE.md, design_system/consigne_design_system.md, jalon 1 (docs/superpowers/specs/2026-06-04-jalon1-tranche-verticale-apt-design.md).

Objectif

Le jalon 1 a livré une UI fonctionnelle mais "brute" : des <button className="interactive"> et des styles inline, sans utiliser les composants du design system. Le ui-kit.tsx porté n'est même pas consommable (il expose ses composants via Object.assign(window, …) et dépend de Font Awesome non chargé).

Ce jalon branche correctement le design system et refond les écrans existants avec ses composants, en respectant la consigne (design_system/consigne_design_system.md). Aucune nouvelle capacité métier : c'est un jalon qualité.

Périmètre

Dedans : wiring du DS (exports ESM, Font Awesome, polices, tous bundlés offline), refonte des écrans existants avec les composants DS, ajout d'un header (titre + actions + bascule thème), ajout d'une status bar style tmux, vérification des deux thèmes (dark + light).

Dehors : aucune logique backend, aucune nouvelle route, pas de panneaux redimensionnables (split panes — reporté), pas de nouveaux écrans.

Décisions verrouillées

Sujet Décision
Font Awesome Bundlé via npm @fortawesome/fontawesome-free (solid), import CSS dans main.tsx. Offline, pas de CDN.
Polices Bundlées via @fontsource/inter, @fontsource/jetbrains-mono, @fontsource/share-tech-mono, importées dans main.tsx. Offline.
Consommation ui-kit Ajout d'exports ESM nommés. On garde @ts-nocheck (pas de réécriture typée) et le Object.assign(window, …) existant.
Layout Ajout d'un header et d'une status bar. Les 3 volets restent.
Thème Bascule dark/light via lib/theme.ts (persistance localStorage), IconButton sun/moon dans le header.
Tests Helpers purs testés (theme, sumUpdates). Le reste = vérif visuelle. ui-kit jamais importé dans un test (touche window/document au chargement).

Contrainte transverse

Le design system impose (consigne) : variables CSS uniquement, composants existants réutilisés, <Icon name=> (jamais d'emoji/SVG custom), pas de hover sauf jauges (pression 3D .interactive), tooltips obligatoires sur IconButton isolé, polices Inter/JetBrains Mono/Share Tech Mono, labels uppercase. Tout écran doit être lisible et cohérent en dark ET light.

API du design system (vérifiée dans ui-kit.tsx)

  • Icon({ name, size, style })name mappé via ICON_MAP vers fa-solid fa-…. Icônes dispo : cpu, memory, disk, network, clock, grid, list, cog, alert, bell, server, chart, bars, terminal, refresh, play, pause, power, sun, moon, search, close, chevR/L/D/U, plus, filter, download, folder, node, user.
  • Button({ children, icon, onClick, variant, size }) — variant: default/primary/ghost/danger ; size: sm/md/lg.
  • IconButton({ icon, label, onClick, active, danger, size, primary })label = tooltip (obligatoire).
  • StatusLed({ status, size, pulse }) — status: ok/warn/err/info/off.
  • Popup({ open, onClose, title, children, footer, width }).
  • Toggle, Tooltip, BatteryGauge, RadialGauge, BigRadialGauge, TreeNav, Sparkline, LineChart (non utilisés ici mais exportés).

Wiring du design system

  1. client/src/components/ui-kit.tsx : ajouter en fin de fichier export { Icon, Tooltip, IconButton, Toggle, StatusLed, BatteryGauge, RadialGauge, BigRadialGauge, Popup, Button, TreeNav, Sparkline, LineChart }; Conserver @ts-nocheck, l'import React et le Object.assign(window, …).
  2. client/src/main.tsx : ajouter les imports CSS import "@fortawesome/fontawesome-free/css/all.min.css"; import "@fontsource/inter"; import "@fontsource/jetbrains-mono"; import "@fontsource/share-tech-mono";
  3. package.json : ajouter les deps @fortawesome/fontawesome-free, @fontsource/inter, @fontsource/jetbrains-mono, @fontsource/share-tech-mono.

Layout cible

┌─ Header : « System Update »  ............  [+ Ajouter] [☀/☾] ┐
├──────────┬─────────────────────────────┬────────────────────┤
│ Hermes   │ Dashboard (tuiles machines) │ Terminal           │
├──────────┴─────────────────────────────┴────────────────────┤
│ SYSTEM UPDATE · N machines · M updates · ⏱ 14:22:07          │
└──────────────────────────────────────────────────────────────┘

App.tsx orchestre : <Header> en haut, la rangée 3 volets au milieu (flex:1), <StatusBar> en bas.

Remontée d'état dans App : la liste des machines, les compteurs d'updates, la machine sélectionnée et le thème vivent désormais dans App (le Dashboard les reçoit en props, plus de fetch local autonome). Cela alimente le Header (action Ajouter), la StatusBar (N machines, M updates) et le TerminalPanel (machine sélectionnée). Le chargement des machines + snapshots se fait dans App via une fonction load() passée au Dashboard pour rafraîchir après action.

Composants

Nouveaux

  • Header.tsx : titre « System Update », bouton <Button variant="primary" icon="plus">Ajouter</Button> (ouvre la modale via callback remonté), et <IconButton icon={theme==="dark"?"sun":"moon"} label="Basculer le thème">. Hauteur 48-56px, fond --bg-2.
  • StatusBar.tsx : 1re cellule mode « SYSTEM UPDATE » fond --accent ; cellules suivantes séparées par border-right: 1px solid var(--border-1) ; nb machines, total updates ; horloge live (Share Tech Mono, tick 1s via setInterval, nettoyé au démontage) à droite. Hauteur 24-28px.
  • lib/theme.ts :
    • type Theme = "dark" | "light"
    • getInitialTheme(): Theme — lit localStorage["su-theme"], défaut "dark", robuste si localStorage indisponible.
    • applyTheme(t: Theme): voiddocument.documentElement.dataset.theme = t + persiste.
    • nextTheme(t: Theme): Theme — bascule dark↔light (fonction pure, testable).
  • lib/stats.ts : sumUpdates(counts: Record<string, number>): number (fonction pure, testable).

Refondus

  • MachineTile.tsx : point d'état → <StatusLed status={machine.status} pulse={machine.status==="running"}> ; compteur → <span className="label">UPDATES</span> <span className="mono">{count}</span> ; actions → <IconButton icon="refresh" label="Rafraîchir">, <IconButton icon="download" label="Upgrade">, <IconButton icon="power" label="Redémarrer" danger>. Conserver className="glass", onClick sélection (les IconButton stoppent la propagation).
  • AddMachineModal.tsx : enveloppé dans <Popup open onClose title="Ajouter une machine" footer={…}> ; footer = <Button variant="ghost" onClick={onClose}>Annuler</Button> + <Button variant="primary" icon="download" onClick={submit}>Ajouter</Button> ; champs en inputs tokenisés ; erreur affichée avec <StatusLed status="err"> + texte --err. Logique de soumission inchangée (POST /api/machines).
  • Dashboard.tsx : retirer le bouton « + Ajouter » local (déplacé dans le Header) ; le Dashboard expose l'ouverture de la modale via prop/état remonté à App. Grille de tuiles inchangée. État vide : texte --ink-3.
  • HermesPanel.tsx : en-tête .label + <Icon name="bell"> (ou autre), texte stub inchangé.
  • TerminalPanel.tsx : recevoir la machine sélectionnée (objet MachineView, pas juste l'id) depuis App. En-tête clair au-dessus du xterm : <StatusLed status> + .label « TERMINAL » + nom de la machine (.mono) + hostname (--ink-3). Séparation franche entre machines (retour d'usage, amelioration.md) : à chaque changement de machine, écrire une bannière de séparation dans le terminal, p. ex. \n──────── <nom> (<hostname>) ────────\n (couleur accent), avant de rejouer le flux. Le terminal est déjà recréé par machine (useEffect deps) ; l'en-tête nommé + la bannière rendent le passage d'une machine à l'autre non ambigu (fini l'UUID). xterm inchangé sinon.

Flux thème

Au montage de App : applyTheme(getInitialTheme()). État theme dans App ; le toggle du Header appelle setTheme(nextTheme(theme)) puis applyTheme. data-theme initial dans index.html reste dark (cohérent avec le défaut).

Gestion d'erreurs / cas limites

  • localStorage indisponible (mode privé) → getInitialTheme retombe sur "dark", applyTheme ignore l'échec de persistance (try/catch) sans casser l'UI.
  • Icône inconnue → le composant Icon retombe déjà sur circle-question.
  • Horloge : l'intervalle est nettoyé dans le cleanup du useEffect.

Tests

  • lib/theme.test.ts : nextTheme("dark")==="light" et inverse ; getInitialTheme() retombe sur "dark" quand localStorage vide. (localStorage mocké, pas d'import de ui-kit.)
  • lib/stats.test.ts : sumUpdates({a:2,b:3})===5 ; sumUpdates({})===0.
  • Vérif build : pnpm check + pnpm build verts.
  • Vérif visuelle manuelle (utilisateur) : dark ET light lisibles ; icônes FA affichées ; polices Inter/JetBrains Mono/Share Tech Mono appliquées ; tooltips sur les IconButton ; modale Popup OK ; status bar + horloge ; aucun hover sur boutons/tuiles (pression 3D seulement).

Critères d'acceptation

  • ui-kit exporte ses composants en ESM ; les écrans les importent (plus aucun <button className="interactive"> brut dans les features).
  • Font Awesome et les 3 polices sont bundlés (offline) et appliqués.
  • Header avec titre, bouton Ajouter, bascule thème fonctionnelle et persistée.
  • Status bar tmux avec mode, compteurs et horloge live.
  • MachineTile utilise StatusLed + IconButton (tooltips) ; AddMachineModal utilise Popup + Button.
  • Le terminal identifie clairement la machine courante (nom + hostname, plus d'UUID) et marque une séparation franche au passage d'une machine à l'autre.
  • Les deux thèmes sont cohérents et lisibles.
  • pnpm check, pnpm build, et les tests des helpers passent.