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:
12
packages/web/src/lib/group-colors.ts
Normal file
12
packages/web/src/lib/group-colors.ts
Normal 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
|
||||
];
|
||||
@@ -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 };
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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";
|
||||
|
||||
58
packages/web/src/lib/theme.ts
Normal file
58
packages/web/src/lib/theme.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user