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>
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 })—namemappé viaICON_MAPversfa-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
client/src/components/ui-kit.tsx: ajouter en fin de fichierexport { Icon, Tooltip, IconButton, Toggle, StatusLed, BatteryGauge, RadialGauge, BigRadialGauge, Popup, Button, TreeNav, Sparkline, LineChart };Conserver@ts-nocheck, l'import React et leObject.assign(window, …).client/src/main.tsx: ajouter les imports CSSimport "@fortawesome/fontawesome-free/css/all.min.css";import "@fontsource/inter";import "@fontsource/jetbrains-mono";import "@fontsource/share-tech-mono";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 parborder-right: 1px solid var(--border-1); nb machines, total updates ; horloge live (Share Tech Mono, tick 1s viasetInterval, nettoyé au démontage) à droite. Hauteur 24-28px.lib/theme.ts:type Theme = "dark" | "light"getInitialTheme(): Theme— litlocalStorage["su-theme"], défaut"dark", robuste si localStorage indisponible.applyTheme(t: Theme): void—document.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>. ConserverclassName="glass",onClicksé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 (objetMachineView, pas juste l'id) depuisApp. 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 (useEffectdeps) ; 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
localStorageindisponible (mode privé) →getInitialThemeretombe sur"dark",applyThemeignore l'échec de persistance (try/catch) sans casser l'UI.- Icône inconnue → le composant
Iconretombe déjà surcircle-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 deui-kit.)lib/stats.test.ts:sumUpdates({a:2,b:3})===5;sumUpdates({})===0.- Vérif build :
pnpm check+pnpm buildverts. - 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-kitexporte 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.