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

@@ -1,35 +1,86 @@
@import 'tailwindcss';
/* Tokens de design centralisés.
Couche SÉMANTIQUE aliasant la palette Tailwind (zinc/emerald/amber/sky/red) : rendu
identique aujourd'hui, mais un point d'entrée unique pour la refonte IDE et un futur
thème clair (il suffirait de redéfinir ces variables sous [data-theme=light]). */
/* ============================================================
Tokens de design « Emerald ».
Couche SÉMANTIQUE : les défauts (bloc @theme) portent les valeurs du thème SOMBRE ;
le thème CLAIR redéfinit les mêmes --color-* sous html[data-theme="light"].
Mécanique Tailwind v4 : @theme émet les vars sur :root et les utilitaires
(bg-surface-0, text-fg, ...) RÉFÉRENCENT ces vars ; comme [data-theme=light] cible le
même <html> que :root (et gagne la cascade), il suffit de réécrire les vars pour
basculer tout le thème, sans dupliquer d'utilitaires. Les modificateurs d'opacité
(bg-danger/15) génèrent un color-mix(... var(--color-danger) ...) → suivent le thème.
============================================================ */
@theme {
/* Ombres (sombre par défaut ; override clair plus bas). */
--shadow-card: 0 1px 2px 0 rgb(0 0 0 / 0.4);
--shadow-pop: 0 12px 32px -12px rgb(0 0 0 / 0.7);
--shadow-pop: 0 24px 48px -20px rgb(0 0 0 / 0.65);
/* Surfaces : du fond le plus profond de l'app aux surfaces survolées. */
--color-surface-0: var(--color-zinc-950); /* fond application (ex-#09090b) */
--color-surface-1: var(--color-zinc-900); /* cartes, panneaux */
--color-surface-2: var(--color-zinc-800); /* boutons, chips */
--color-surface-3: var(--color-zinc-700); /* survol des surfaces */
--color-surface-0: #09090b; /* fond application (Emerald --bg) */
--color-surface-1: #18181b; /* cartes, panneaux (Emerald --surface) */
--color-surface-2: #27272a; /* surface interactive opaque (boutons, chips) */
--color-surface-3: #3f3f46; /* survol des surfaces */
/* Bordures. */
--color-border: var(--color-zinc-800);
--color-border-strong: var(--color-zinc-700);
--color-border: #27272a;
--color-border-strong: #3f3f46;
--color-border-soft: #1c1c1f; /* séparateurs discrets (Emerald --border-soft) */
/* Texte. */
--color-fg: var(--color-zinc-200);
--color-fg-muted: var(--color-zinc-400);
--color-fg-subtle: var(--color-zinc-500);
--color-fg: #f4f4f5;
--color-fg-muted: #a1a1aa;
--color-fg-subtle: #52525b;
/* Accent (interactions, focus). */
--color-accent: var(--color-emerald-600);
--color-accent-hover: var(--color-emerald-500);
/* Accent. Emerald scinde en 3 :
- accent (BRIGHT #34d399) : texte, points, anneaux, liens, carets, indicateurs ;
- accent-solid (#059669) : fond plein avec texte blanc (bouton primaire) ;
- accent-hover (#10b981) : survol du plein. */
--color-accent: #34d399;
--color-accent-solid: #059669;
--color-accent-hover: #10b981;
/* Tons sémantiques (statuts / signalétique). */
--color-coffee: #f59e0b;
--color-info: #7dd3fc;
--color-warn: #fcd34d;
--color-danger: #f87171;
/* Polices auto-hébergées (fontes variables). Ces tokens alimentent aussi font-sans /
font-mono de Tailwind. Noms exacts des paquets @fontsource-variable : "... Variable". */
--font-sans: 'Inter Variable', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;
--font-mono: 'JetBrains Mono Variable', ui-monospace, 'Fira Code', Menlo, Consolas, monospace;
}
/* Métriques du chrome IDE (barres, onglets, lignes). Consommées via des valeurs
arbitraires Tailwind, ex. h-[var(--ide-tab-h)]. */
/* ---------- Thème clair : réécriture des mêmes tokens (même élément que :root) ---------- */
html[data-theme='light'] {
--shadow-card: 0 1px 2px 0 rgb(24 24 27 / 0.08);
--shadow-pop: 0 18px 38px -22px rgb(24 24 27 / 0.22);
--color-surface-0: #fafafa;
--color-surface-1: #ffffff;
--color-surface-2: #f4f4f5;
--color-surface-3: #e4e4e7;
--color-border: #e4e4e7;
--color-border-strong: #d4d4d8;
--color-border-soft: #ececee;
--color-fg: #18181b;
--color-fg-muted: #52525b;
--color-fg-subtle: #a1a1aa;
--color-accent: #059669;
--color-accent-solid: #059669;
--color-accent-hover: #047857;
--color-coffee: #d97706;
--color-info: #0284c7;
--color-warn: #d97706;
--color-danger: #dc2626;
}
/* Métriques du chrome IDE (barres, onglets, lignes), indépendantes du thème.
Consommées via des valeurs arbitraires Tailwind, ex. h-[var(--ide-tab-h)]. */
:root {
--ide-activitybar-w: 3rem;
--ide-titlebar-h: 2.25rem;
@@ -47,9 +98,31 @@ body,
body {
background-color: var(--color-surface-0);
color: var(--color-fg);
font-family: var(--font-sans);
-webkit-font-smoothing: antialiased;
}
/* ---------- Animations signature (hors couches : les @keyframes ne sont pas layerisés) ---------- */
@keyframes jl-pulse {
0%,
100% {
box-shadow: 0 0 0 0 rgb(52 211 153 / 0.55);
}
50% {
box-shadow: 0 0 0 5px rgb(52 211 153 / 0);
}
}
@keyframes jl-blink {
0%,
49% {
opacity: 1;
}
50%,
100% {
opacity: 0;
}
}
@layer base {
/* Tailwind v4 ne met plus cursor:pointer sur les boutons : on le rétablit
(sauf désactivés, qui gardent le curseur par défaut / not-allowed). */
@@ -57,38 +130,168 @@ body {
[role='button']:not([aria-disabled='true']) {
cursor: pointer;
}
/* Focus visible global (fallback accent). Les composants qui posent leur propre
focus-visible:outline-none + ring gardent leur anneau (leur règle gagne en spécificité). */
:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: 2px;
}
/* Sélection de texte du chrome (xterm/Monaco gardent la leur). */
::selection {
background: var(--color-accent);
color: var(--color-surface-0);
}
/* Scrollbars fines et discrètes (absentes auparavant). */
* {
scrollbar-width: thin;
scrollbar-color: var(--color-surface-3) transparent;
}
::-webkit-scrollbar {
width: 10px;
height: 10px;
}
::-webkit-scrollbar-thumb {
background: var(--color-surface-3);
border-radius: 9999px;
border: 2px solid transparent;
background-clip: content-box;
}
::-webkit-scrollbar-thumb:hover {
background: var(--color-border-strong);
}
::-webkit-scrollbar-corner {
background: transparent;
}
/* Respect de prefers-reduced-motion (généralise le bloc scopé de ToastContainer). */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.001ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.001ms !important;
}
}
}
@layer components {
/* Champs : rayon unifié + anneau de focus visible (absent auparavant). */
/* Champs : rayon unifié + anneau de focus visible. */
.input {
@apply w-full rounded-lg border border-border-strong bg-surface-0 px-2.5 py-1.5 text-sm text-zinc-100 outline-none transition-colors placeholder:text-zinc-600 focus-visible:border-zinc-500 focus-visible:ring-2 focus-visible:ring-accent-hover/40;
@apply w-full rounded-lg border border-border-strong bg-surface-0 px-2.5 py-1.5 text-sm text-fg outline-none transition-colors placeholder:text-fg-subtle focus-visible:border-accent focus-visible:ring-2 focus-visible:ring-accent/40;
}
/* Boutons : socle CSS pour les usages hors composant (ex. RouterLink class="btn").
Le composant BaseButton réplique ces variantes et ajoute tailles / loading / focus. */
/* Boutons : socle CSS pour les usages hors composant (RouterLink class="btn").
BaseButton réplique ces variantes et ajoute tailles / loading. Idiomes Emerald :
rayon 11px, font 600, hover accent. */
.btn {
@apply inline-flex items-center justify-center gap-1.5 rounded-lg border border-border-strong bg-surface-2 px-3 py-1.5 text-sm font-medium text-fg transition-colors hover:bg-surface-3 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-hover/70 focus-visible:ring-offset-2 focus-visible:ring-offset-surface-0 disabled:cursor-not-allowed disabled:opacity-50;
@apply inline-flex items-center justify-center gap-[9px] rounded-[11px] border border-border bg-surface-2 px-3 py-1.5 text-sm font-semibold text-fg transition-[background-color,border-color,color,transform] hover:border-accent hover:text-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/60 focus-visible:ring-offset-2 focus-visible:ring-offset-surface-0 disabled:cursor-not-allowed disabled:opacity-50;
}
.btn-primary {
@apply inline-flex items-center justify-center gap-1.5 rounded-lg bg-accent px-3 py-1.5 text-sm font-medium text-white transition-colors hover:bg-accent-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-hover/70 focus-visible:ring-offset-2 focus-visible:ring-offset-surface-0 disabled:cursor-not-allowed disabled:opacity-50;
@apply inline-flex items-center justify-center gap-[9px] rounded-[11px] border border-accent-solid bg-accent-solid px-3 py-1.5 text-sm font-semibold text-white transition-[background-color,border-color,color,transform] hover:-translate-y-px hover:bg-accent-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/70 focus-visible:ring-offset-2 focus-visible:ring-offset-surface-0 disabled:cursor-not-allowed disabled:opacity-50;
}
.btn-danger {
@apply inline-flex items-center justify-center gap-1.5 rounded-lg border border-red-900 bg-red-950 px-3 py-1.5 text-sm font-medium text-red-300 transition-colors hover:bg-red-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-500/70 focus-visible:ring-offset-2 focus-visible:ring-offset-surface-0 disabled:cursor-not-allowed disabled:opacity-50;
@apply inline-flex items-center justify-center gap-[9px] rounded-[11px] border border-danger/40 bg-danger/10 px-3 py-1.5 text-sm font-semibold text-danger transition-[background-color,border-color,color,transform] hover:bg-danger/20 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-danger/70 focus-visible:ring-offset-2 focus-visible:ring-offset-surface-0 disabled:cursor-not-allowed disabled:opacity-50;
}
.btn-ghost {
@apply inline-flex items-center justify-center gap-1.5 rounded-lg px-3 py-1.5 text-sm font-medium text-zinc-300 transition-colors hover:bg-surface-2/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-hover/70 focus-visible:ring-offset-2 focus-visible:ring-offset-surface-0 disabled:cursor-not-allowed disabled:opacity-50;
@apply inline-flex items-center justify-center gap-[9px] rounded-[11px] px-3 py-1.5 text-sm font-semibold text-fg-muted transition-[background-color,border-color,color] hover:bg-surface-2/60 hover:text-fg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/60 focus-visible:ring-offset-2 focus-visible:ring-offset-surface-0 disabled:cursor-not-allowed disabled:opacity-50;
}
.btn-coffee {
@apply inline-flex items-center justify-center gap-[7px] rounded-[9px] border border-coffee bg-transparent px-[13px] py-2 text-[13px] font-semibold text-coffee transition-[background-color,transform] hover:-translate-y-px hover:bg-coffee/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-coffee/60 focus-visible:ring-offset-2 focus-visible:ring-offset-surface-0;
}
/* Pastille (pill mono Emerald). Les variantes de ton vivent dans lib/status-tokens. */
.badge {
@apply inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-[11px] font-medium;
@apply inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 font-mono text-[11px];
}
/* Puce d'accent pulsée (halo box-shadow, pas un fondu d'opacité). */
.badge-dot {
@apply inline-block h-2 w-2 rounded-full bg-accent;
animation: jl-pulse 2.2s infinite;
}
/* Cartes : factorise le motif répété dans les listes/sections/formulaires. */
/* Chip (tag mono discret). */
.chip {
@apply inline-flex items-center rounded-full border border-border bg-surface-0 px-[9px] py-1 font-mono text-[11px] text-fg-muted;
}
/* Statut mono uppercase + point coloré. */
.status {
@apply inline-flex items-center gap-1.5 font-mono text-[11px] tracking-[0.05em] text-fg-muted uppercase;
}
.status-dot {
@apply inline-block h-[7px] w-[7px] rounded-full;
}
/* Micro-titres. */
.eyebrow {
@apply font-mono text-[13px] text-accent;
}
.label-mono {
@apply font-mono text-[11px] tracking-[0.08em] text-fg-subtle uppercase;
}
/* Caret clignotant pour les mockups terminaux (JAMAIS xterm). */
.caret {
@apply ml-0.5 inline-block h-[15px] w-2 bg-accent align-[-2px];
animation: jl-blink 1.05s steps(1) infinite;
}
/* Cartes. */
.card {
@apply rounded-xl border border-border bg-surface-1/50 p-3;
@apply rounded-[14px] border border-border bg-surface-1/50 p-3;
}
.card-hover {
@apply transition-[transform,border-color,box-shadow] hover:-translate-y-1 hover:border-accent hover:shadow-pop;
}
.card-inset {
@apply rounded-lg border border-border bg-surface-0/40 p-2;
@apply rounded-[10px] border border-border bg-surface-0/40 p-2;
}
/* Calques décoratifs (halo accent + grain pointillé). Statiques (safe reduced-motion). */
.halo {
position: absolute;
top: -40px;
left: 50%;
width: 1100px;
max-width: 140%;
height: 560px;
transform: translateX(-50%);
background: radial-gradient(50% 60% at 50% 0%, var(--color-accent) 0%, transparent 70%);
opacity: 0.12;
filter: blur(18px);
pointer-events: none;
z-index: 0;
}
.grain {
position: absolute;
inset: 0;
background-image: radial-gradient(var(--color-fg) 1px, transparent 1px);
background-size: 34px 34px;
opacity: 0.025;
pointer-events: none;
z-index: 0;
-webkit-mask-image: linear-gradient(to bottom, #000, transparent 70%);
mask-image: linear-gradient(to bottom, #000, transparent 70%);
}
/* Lignes de diff : le FOND teinté porte la sémantique (ajout/suppression), le texte
reste neutre (lisible en clair ET sombre, façon GitHub). */
.diff-add {
@apply bg-accent/12 text-fg;
}
.diff-del {
@apply bg-danger/12 text-fg;
}
.diff-ctx {
@apply text-fg-muted;
}
}
/* Halo plus discret en thème clair. */
html[data-theme='light'] .halo {
opacity: 0.06;
}