feat(web): refonte visuelle « Emerald » (thème clair/sombre, polices, tokens)

Adopte le langage visuel du design system Emerald dans l'IDE web :
neutres zinc + accent emerald, polices auto-hébergées Inter + JetBrains
Mono, radii/pills mono, points de statut, motif terminal, halo/grain.
Refonte purement visuelle et additive (layout, routing, stores, protocole
inchangés).

- style.css : défauts sombres dans @theme + override clair sous
  html[data-theme=light] ; accent scindé (bright #34d399 / solid #059669 /
  hover) ; tokens border-soft/info/warn/danger/coffee ; idiomes globaux
  (focus/selection accent, scrollbars fines, jl-pulse/jl-blink, classes
  .chip/.status/.eyebrow/.label-mono/.caret/.halo/.grain, .btn*/.badge pill).
- lib/theme.ts : singleton themeMode/resolvedTheme (dark/light/system,
  persistedRef arb.theme) + application dataset.theme/metas ; script
  anti-FOUC inline dans index.html ; toggle ActivityBar + Réglages.
- Thèmes Monaco + xterm dark/light réactifs (palette ANSI + coloration
  syntaxique) ; fonts.ready + remeasureFonts pour l'alignement des glyphes.
- status-tokens : tons sémantiques tokenisés ; balayage des ~340 couleurs
  Tailwind brutes vers les tokens sur tout components/* et views/*
  (exceptions assumées commentées).
- GroupSummary.color exploité : icône de groupe teintée + ColorSwatchPicker
  (lib/group-colors.ts) dans les modals de groupe.
- Polices via @fontsource-variable/{inter,jetbrains-mono} (offline).
This commit is contained in:
2026-07-20 21:30:57 +02:00
parent 008e976e01
commit 063f5e928b
59 changed files with 986 additions and 366 deletions

View File

@@ -0,0 +1,12 @@
// Presets de couleur d'accent pour les groupes (champ GroupSummary.color, hex #rrggbb).
// Alignés sur la palette « Emerald » (accent / info / warn / coffee / danger + quelques teintes).
export const GROUP_COLOR_PRESETS: readonly string[] = [
'#34d399', // emerald (accent)
'#7dd3fc', // sky (info)
'#fcd34d', // amber (warn)
'#f59e0b', // coffee
'#f87171', // rose (danger)
'#c4b5fd', // violet
'#67e8f9', // cyan
'#a3e635', // lime
];

View File

@@ -5,20 +5,85 @@
// avancés (TS/JSON) dégradent proprement : suffisant pour éditer + sauver dans le navigateur.
import * as monaco from 'monaco-editor';
import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
import type { ResolvedTheme } from './theme';
(self as unknown as { MonacoEnvironment: monaco.Environment }).MonacoEnvironment = {
getWorker: () => new EditorWorker(),
};
// Thème dérivé de vs-dark dont le fond s'aligne sur --color-surface-0 (#09090b) : cohérence
// visuelle de l'éditeur avec le reste du chrome IDE. Défini une fois au chargement du module.
// Deux thèmes « Emerald » (hex concrets : Monaco n'accepte pas de var CSS). Les couleurs de token
// (rules) sont SANS '#', les couleurs de chrome (colors) AVEC '#'. À garder synchronisé avec style.css.
monaco.editor.defineTheme('arboretum-dark', {
base: 'vs-dark',
inherit: true,
rules: [],
rules: [
{ token: 'comment', foreground: '52525b', fontStyle: 'italic' },
{ token: 'keyword', foreground: '34d399' },
{ token: 'storage', foreground: '34d399' },
{ token: 'string', foreground: 'fcd34d' },
{ token: 'number', foreground: '7dd3fc' },
{ token: 'constant', foreground: '7dd3fc' },
{ token: 'type', foreground: '5eead4' },
{ token: 'function', foreground: '7dd3fc' },
{ token: 'variable', foreground: 'f4f4f5' },
{ token: 'operator', foreground: 'a1a1aa' },
{ token: 'delimiter', foreground: 'a1a1aa' },
{ token: 'tag', foreground: 'f87171' },
{ token: 'attribute.name', foreground: 'fcd34d' },
{ token: 'attribute.value', foreground: '34d399' },
],
colors: {
'editor.background': '#09090b',
'editor.foreground': '#f4f4f5',
'editorCursor.foreground': '#34d399',
'editor.selectionBackground': '#3f3f46',
'editor.lineHighlightBackground': '#18181b',
'editorLineNumber.foreground': '#52525b',
'editorLineNumber.activeForeground': '#a1a1aa',
'editorIndentGuide.background1': '#27272a',
'editorGutter.background': '#09090b',
'editorWidget.background': '#18181b',
'editorWidget.border': '#27272a',
},
});
monaco.editor.defineTheme('arboretum-light', {
base: 'vs',
inherit: true,
rules: [
{ token: 'comment', foreground: 'a1a1aa', fontStyle: 'italic' },
{ token: 'keyword', foreground: '059669' },
{ token: 'storage', foreground: '059669' },
{ token: 'string', foreground: 'b45309' },
{ token: 'number', foreground: '0284c7' },
{ token: 'constant', foreground: '0284c7' },
{ token: 'type', foreground: '0d9488' },
{ token: 'function', foreground: '0284c7' },
{ token: 'variable', foreground: '18181b' },
{ token: 'operator', foreground: '52525b' },
{ token: 'delimiter', foreground: '52525b' },
{ token: 'tag', foreground: 'dc2626' },
{ token: 'attribute.name', foreground: 'b45309' },
{ token: 'attribute.value', foreground: '059669' },
],
colors: {
'editor.background': '#fafafa',
'editor.foreground': '#18181b',
'editorCursor.foreground': '#059669',
'editor.selectionBackground': '#d4d4d8',
'editor.lineHighlightBackground': '#f4f4f5',
'editorLineNumber.foreground': '#a1a1aa',
'editorLineNumber.activeForeground': '#52525b',
'editorIndentGuide.background1': '#e4e4e7',
'editorGutter.background': '#fafafa',
'editorWidget.background': '#ffffff',
'editorWidget.border': '#e4e4e7',
},
});
/** Nom du thème Monaco pour le thème résolu courant. */
export function monacoThemeName(resolved: ResolvedTheme): 'arboretum-dark' | 'arboretum-light' {
return resolved === 'light' ? 'arboretum-light' : 'arboretum-dark';
}
export { monaco };

View File

@@ -1,35 +1,35 @@
// Tons de statut centralisés : paires bg/text et couleurs de point, partagées par
// BaseBadge (pastille générique) et SessionStateBadge (état de session live).
// Évite la duplication historique des classes amber/sky/emerald/zinc/red.
// Tons SÉMANTIQUES basés sur les tokens de thème (--color-*) → s'adaptent au thème clair/sombre.
export type Tone = 'zinc' | 'emerald' | 'amber' | 'sky' | 'red';
export type Tone = 'neutral' | 'accent' | 'warn' | 'info' | 'danger';
/** Classe fond + texte de la pastille pour chaque ton. */
/** Classe fond + texte de la pastille pour chaque ton (tinte via color-mix du modificateur d'opacité). */
export const TONE_BADGE: Record<Tone, string> = {
zinc: 'bg-zinc-800 text-zinc-400',
emerald: 'bg-emerald-950 text-emerald-400',
amber: 'bg-amber-950 text-amber-300',
sky: 'bg-sky-950 text-sky-300',
red: 'bg-red-950 text-red-400',
neutral: 'bg-surface-2 text-fg-muted',
accent: 'bg-accent/15 text-accent',
warn: 'bg-warn/15 text-warn',
info: 'bg-info/15 text-info',
danger: 'bg-danger/15 text-danger',
};
/** Classe de couleur du point d'état pour chaque ton. */
export const TONE_DOT: Record<Tone, string> = {
zinc: 'bg-zinc-500',
emerald: 'bg-emerald-400',
amber: 'bg-amber-300',
sky: 'bg-sky-300',
red: 'bg-red-400',
neutral: 'bg-fg-subtle',
accent: 'bg-accent',
warn: 'bg-warn',
info: 'bg-info',
danger: 'bg-danger',
};
/** Activité fine d'une session live (P3-B) : busy / waiting / idle (null = idle). */
export type SessionActivityTone = 'busy' | 'waiting' | 'idle' | null;
/** Mappe l'activité live d'une session vers un ton : waiting -> amber, busy -> sky, idle -> emerald. */
/** Mappe l'activité live d'une session vers un ton : waiting -> warn, busy -> info, idle -> accent. */
export function activityTone(activity: SessionActivityTone): Tone {
if (activity === 'waiting') return 'amber';
if (activity === 'busy') return 'sky';
return 'emerald';
if (activity === 'waiting') return 'warn';
if (activity === 'busy') return 'info';
return 'accent';
}
/** L'état busy/waiting mérite un point pulsé (activité en cours) ; idle reste fixe. */

View File

@@ -1,16 +1,64 @@
// Source unique du thème xterm. xterm exige des couleurs hex concrètes (pas de var CSS),
// donc les valeurs vivent ici plutôt que dupliquées dans chaque composant terminal.
// À garder synchronisé avec --color-surface-0 de style.css si la surface de base change.
// Source unique des thèmes xterm. xterm exige des couleurs hex CONCRÈTES (pas de var CSS),
// donc les valeurs vivent ici. À garder synchronisé avec les tokens --color-* de style.css.
// Deux thèmes (sombre / clair) + palette ANSI 16 couleurs alignée « Emerald ».
import type { ITheme } from '@xterm/xterm';
import type { ResolvedTheme } from './theme';
// Aligne le fond sur --color-surface-0 (#09090b) ; fg = zinc-300, curseur = zinc-200,
// sélection = zinc-700.
export const TERMINAL_THEME: ITheme = {
// Sombre : fond = surface-0 (#09090b), curseur sur l'accent bright (#34d399).
export const TERMINAL_THEME_DARK: ITheme = {
background: '#09090b',
foreground: '#d4d4d8',
cursor: '#e4e4e7',
cursor: '#34d399',
cursorAccent: '#09090b',
selectionBackground: '#3f3f46',
// ANSI 16 couleurs (normales puis bright), teintées Emerald.
black: '#27272a',
red: '#f87171',
green: '#34d399',
yellow: '#fcd34d',
blue: '#7dd3fc',
magenta: '#d8b4fe',
cyan: '#67e8f9',
white: '#e4e4e7',
brightBlack: '#52525b',
brightRed: '#fca5a5',
brightGreen: '#6ee7b7',
brightYellow: '#fde68a',
brightBlue: '#bae6fd',
brightMagenta: '#e9d5ff',
brightCyan: '#a5f3fc',
brightWhite: '#fafafa',
};
// Clair : fond = surface-0 clair (#fafafa), curseur sur l'accent (#059669).
export const TERMINAL_THEME_LIGHT: ITheme = {
background: '#fafafa',
foreground: '#18181b',
cursor: '#059669',
cursorAccent: '#fafafa',
selectionBackground: '#d4d4d8',
black: '#18181b',
red: '#dc2626',
green: '#059669',
yellow: '#d97706',
blue: '#0284c7',
magenta: '#9333ea',
cyan: '#0891b2',
white: '#52525b',
brightBlack: '#a1a1aa',
brightRed: '#ef4444',
brightGreen: '#10b981',
brightYellow: '#f59e0b',
brightBlue: '#0ea5e9',
brightMagenta: '#a855f7',
brightCyan: '#06b6d4',
brightWhite: '#18181b',
};
/** Thème xterm pour le thème résolu courant. */
export function terminalTheme(resolved: ResolvedTheme): ITheme {
return resolved === 'light' ? TERMINAL_THEME_LIGHT : TERMINAL_THEME_DARK;
}
export const TERMINAL_FONT_FAMILY =
"ui-monospace, 'JetBrains Mono', 'Fira Code', Menlo, Consolas, monospace";
"'JetBrains Mono Variable', ui-monospace, 'Fira Code', Menlo, Consolas, monospace";

View File

@@ -0,0 +1,58 @@
// Thème clair / sombre : singleton module (même esprit que ws-client / i18n, sans Pinia).
// - `themeMode` est la préférence persistée (dark | light | system, défaut dark) ;
// - `resolvedTheme` résout `system` via prefers-color-scheme ;
// - un watch applique le thème au <html> (dataset.theme + metas color-scheme/theme-color).
// Consommé par le chrome (toggle), et par lib/monaco-setup + TerminalView (thèmes réactifs).
import { computed, ref, watch, type ComputedRef } from 'vue';
import { persistedRef } from './persisted-ref';
export type ThemeMode = 'dark' | 'light' | 'system';
export type ResolvedTheme = 'dark' | 'light';
// Doit rester synchronisé avec --color-surface-0 (style.css) : sert au fond du <html> et aux metas.
const BG: Record<ResolvedTheme, string> = { dark: '#09090b', light: '#fafafa' };
/** Préférence de thème persistée en localStorage (clé `arb.theme`). */
export const themeMode = persistedRef<ThemeMode>('arb.theme', 'dark');
// Suit le thème du système d'exploitation (utilisé quand themeMode === 'system').
const mql =
typeof window !== 'undefined' && typeof window.matchMedia === 'function'
? window.matchMedia('(prefers-color-scheme: dark)')
: null;
const systemDark = ref(mql?.matches ?? true);
mql?.addEventListener('change', (e) => {
systemDark.value = e.matches;
});
/** Thème effectivement appliqué (`system` résolu). */
export const resolvedTheme: ComputedRef<ResolvedTheme> = computed(() => {
if (themeMode.value === 'system') return systemDark.value ? 'dark' : 'light';
return themeMode.value;
});
function applyTheme(resolved: ResolvedTheme): void {
if (typeof document === 'undefined') return;
const root = document.documentElement;
root.dataset.theme = resolved;
root.style.backgroundColor = BG[resolved];
const setMeta = (name: string, content: string): void => {
document.querySelector(`meta[name="${name}"]`)?.setAttribute('content', content);
};
setMeta('color-scheme', resolved);
setMeta('theme-color', BG[resolved]);
}
// Watcher d'application, actif pour toute la durée de vie de l'app (comme ws-client).
watch(resolvedTheme, applyTheme, { immediate: true });
export function setThemeMode(mode: ThemeMode): void {
themeMode.value = mode;
}
/** Cycle dark → light → system (pour un bouton unique). */
export function cycleTheme(): void {
const order: ThemeMode[] = ['dark', 'light', 'system'];
const next = order[(order.indexOf(themeMode.value) + 1) % order.length];
if (next) themeMode.value = next;
}