feat(web): couche de tokens de design centralisée (surfaces, statuts, terminal)
Some checks failed
CI / Build & test (Node 24) (push) Has been cancelled
CI / Pack & boot smoke (Node 22) (push) Has been cancelled
CI / No em/en dashes (push) Has been cancelled
CI / Build & test (Node 22) (push) Has been cancelled

Introduit une couche sémantique dans style.css (@theme) : surfaces (surface-0..3), bordures, texte (fg/muted/subtle) et accent, aliasant la palette Tailwind (rendu identique, vérifié dans le CSS produit). Ajoute les métriques du chrome IDE (--ide-*) pour la refonte à venir.

Centralise deux sources uniques : lib/terminal-theme.ts (thème + police xterm) et lib/status-tokens.ts (tons de statut, dédupliqués entre BaseBadge et SessionStateBadge). Définit le thème Monaco « arboretum-dark ». Remplace les #09090b épars des composants par bg-surface-0 (ne restent que index.html et les renderers exigeant un hex concret).
This commit is contained in:
2026-07-17 16:50:58 +02:00
parent 65ef616867
commit 9a496abd00
12 changed files with 129 additions and 57 deletions

View File

@@ -13,6 +13,7 @@
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import type { SessionSummary } from '@arboretum/shared';
import { TONE_BADGE, TONE_DOT, activityTone, activityPulses } from '../lib/status-tokens';
const props = defineProps<{ session: SessionSummary }>();
const { t } = useI18n();
@@ -20,14 +21,7 @@ const { t } = useI18n();
// activity (P3-B, managées) prime ; registryStatus (P2, découvertes) en repli.
const activity = computed(() => props.session.activity ?? props.session.registryStatus ?? null);
const label = computed(() => (activity.value ? t(`sessions.registryStatus.${activity.value}`) : t('sessions.live')));
const badgeClass = computed(() => ({
'bg-amber-950 text-amber-300': activity.value === 'waiting',
'bg-sky-950 text-sky-300': activity.value === 'busy',
'bg-emerald-950 text-emerald-400': activity.value === 'idle' || activity.value === null,
}));
const dotClass = computed(() => ({
'bg-amber-300 animate-pulse': activity.value === 'waiting',
'bg-sky-300 animate-pulse': activity.value === 'busy',
'bg-emerald-400': activity.value === 'idle' || activity.value === null,
}));
const tone = computed(() => activityTone(activity.value));
const badgeClass = computed(() => TONE_BADGE[tone.value]);
const dotClass = computed(() => [TONE_DOT[tone.value], activityPulses(activity.value) ? 'animate-pulse' : '']);
</script>

View File

@@ -1,6 +1,6 @@
<template>
<!-- focus-within anneau visuel : montre quelle cellule reçoit la frappe clavier -->
<div class="flex min-h-0 flex-col overflow-hidden rounded-lg border border-zinc-800 bg-[#09090b] focus-within:ring-2 focus-within:ring-sky-600">
<div class="flex min-h-0 flex-col overflow-hidden rounded-lg border border-zinc-800 bg-surface-0 focus-within:ring-2 focus-within:ring-sky-600">
<header class="flex items-center gap-2 border-b border-zinc-800 px-2 py-1">
<SessionStateBadge :session="session" />
<span class="truncate font-mono text-xs text-zinc-300" :title="session.cwd">{{ title }}</span>

View File

@@ -1,5 +1,5 @@
<template>
<div class="relative h-full min-h-0 bg-[#09090b]">
<div class="relative h-full min-h-0 bg-surface-0">
<div ref="container" class="h-full w-full" />
<div
v-if="!controlling && !ended"
@@ -25,6 +25,7 @@ import { Terminal } from '@xterm/xterm';
import { FitAddon } from '@xterm/addon-fit';
import '@xterm/xterm/css/xterm.css';
import { wsClient, type Attachment } from '../lib/ws-client';
import { TERMINAL_THEME, TERMINAL_FONT_FAMILY } from '../lib/terminal-theme';
const props = withDefaults(defineProps<{ sessionId: string; mode?: 'interactive' | 'observer' }>(), {
mode: 'interactive',
@@ -48,14 +49,9 @@ onMounted(async () => {
term = new Terminal({
cursorBlink: true,
fontSize: 13,
fontFamily: "ui-monospace, 'JetBrains Mono', 'Fira Code', Menlo, Consolas, monospace",
fontFamily: TERMINAL_FONT_FAMILY,
scrollback: 20_000, // absorbe le replay élargi (REPLAY_TAIL_BYTES = 1 Mo) → on remonte plus loin
theme: {
background: '#09090b',
foreground: '#d4d4d8',
cursor: '#e4e4e7',
selectionBackground: '#3f3f46',
},
theme: TERMINAL_THEME,
});
const fit = new FitAddon();
term.loadAddon(fit);

View File

@@ -6,30 +6,15 @@
</template>
<script setup lang="ts">
// Pastille d'état : centralise les paires bg/text répétées (zinc/emerald/amber/sky/red).
// Pastille d'état : les paires bg/text et couleurs de point vivent dans lib/status-tokens
// (partagées avec SessionStateBadge).
import { computed } from 'vue';
type Tone = 'zinc' | 'emerald' | 'amber' | 'sky' | 'red';
import { TONE_BADGE, TONE_DOT, type Tone } from '../../lib/status-tokens';
const props = withDefaults(defineProps<{ tone?: Tone; dot?: boolean; pulse?: boolean }>(), {
tone: 'zinc',
});
const TONES: 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',
};
const DOTS: Record<Tone, string> = {
zinc: 'bg-zinc-500',
emerald: 'bg-emerald-400',
amber: 'bg-amber-300',
sky: 'bg-sky-300',
red: 'bg-red-400',
};
const toneClass = computed(() => TONES[props.tone]);
const dotClass = computed(() => [DOTS[props.tone], props.pulse ? 'animate-pulse' : '']);
const toneClass = computed(() => TONE_BADGE[props.tone]);
const dotClass = computed(() => [TONE_DOT[props.tone], props.pulse ? 'animate-pulse' : '']);
</script>

View File

@@ -43,12 +43,12 @@ const tag = computed(() => (asLink.value ? RouterLink : 'button'));
const iconSize = computed(() => (props.size === 'sm' ? 15 : 16));
const BASE =
'inline-flex items-center justify-center gap-1.5 rounded-lg font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-zinc-950 disabled:cursor-not-allowed disabled:opacity-50';
'inline-flex items-center justify-center gap-1.5 rounded-lg font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-surface-0 disabled:cursor-not-allowed disabled:opacity-50';
const VARIANTS: Record<NonNullable<typeof props.variant>, string> = {
primary: 'bg-emerald-600 text-white hover:bg-emerald-500 focus-visible:ring-emerald-500/70',
secondary: 'border border-zinc-700 bg-zinc-800 text-zinc-200 hover:bg-zinc-700 focus-visible:ring-emerald-500/70',
ghost: 'text-zinc-300 hover:bg-zinc-800/60 focus-visible:ring-emerald-500/70',
primary: 'bg-accent text-white hover:bg-accent-hover focus-visible:ring-accent-hover/70',
secondary: 'border border-border-strong bg-surface-2 text-fg hover:bg-surface-3 focus-visible:ring-accent-hover/70',
ghost: 'text-zinc-300 hover:bg-surface-2/60 focus-visible:ring-accent-hover/70',
danger: 'border border-red-900 bg-red-950 text-red-300 hover:bg-red-900 focus-visible:ring-red-500/70',
};

View File

@@ -57,7 +57,7 @@ onMounted(async () => {
if (disposed || !host.value) return;
editor.value = monaco.editor.create(host.value, {
value: '',
theme: 'vs-dark',
theme: 'arboretum-dark',
automaticLayout: true,
minimap: { enabled: false },
wordWrap: 'on',