feat(web): couche de tokens de design centralisée (surfaces, statuts, terminal)
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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -10,4 +10,15 @@ import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
|
||||
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.
|
||||
monaco.editor.defineTheme('arboretum-dark', {
|
||||
base: 'vs-dark',
|
||||
inherit: true,
|
||||
rules: [],
|
||||
colors: {
|
||||
'editor.background': '#09090b',
|
||||
},
|
||||
});
|
||||
|
||||
export { monaco };
|
||||
|
||||
38
packages/web/src/lib/status-tokens.ts
Normal file
38
packages/web/src/lib/status-tokens.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
// 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.
|
||||
|
||||
export type Tone = 'zinc' | 'emerald' | 'amber' | 'sky' | 'red';
|
||||
|
||||
/** Classe fond + texte de la pastille pour chaque ton. */
|
||||
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',
|
||||
};
|
||||
|
||||
/** 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',
|
||||
};
|
||||
|
||||
/** 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. */
|
||||
export function activityTone(activity: SessionActivityTone): Tone {
|
||||
if (activity === 'waiting') return 'amber';
|
||||
if (activity === 'busy') return 'sky';
|
||||
return 'emerald';
|
||||
}
|
||||
|
||||
/** L'état busy/waiting mérite un point pulsé (activité en cours) ; idle reste fixe. */
|
||||
export function activityPulses(activity: SessionActivityTone): boolean {
|
||||
return activity === 'waiting' || activity === 'busy';
|
||||
}
|
||||
16
packages/web/src/lib/terminal-theme.ts
Normal file
16
packages/web/src/lib/terminal-theme.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
// 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.
|
||||
import type { ITheme } from '@xterm/xterm';
|
||||
|
||||
// Aligne le fond sur --color-surface-0 (#09090b) ; fg = zinc-300, curseur = zinc-200,
|
||||
// sélection = zinc-700.
|
||||
export const TERMINAL_THEME: ITheme = {
|
||||
background: '#09090b',
|
||||
foreground: '#d4d4d8',
|
||||
cursor: '#e4e4e7',
|
||||
selectionBackground: '#3f3f46',
|
||||
};
|
||||
|
||||
export const TERMINAL_FONT_FAMILY =
|
||||
"ui-monospace, 'JetBrains Mono', 'Fira Code', Menlo, Consolas, monospace";
|
||||
@@ -1,9 +1,41 @@
|
||||
@import 'tailwindcss';
|
||||
|
||||
/* Tokens additifs (la palette zinc/emerald/amber/sky/red reste celle de Tailwind). */
|
||||
/* 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]). */
|
||||
@theme {
|
||||
--shadow-card: 0 1px 2px 0 rgb(0 0 0 / 0.4);
|
||||
--shadow-pop: 0 12px 32px -12px rgb(0 0 0 / 0.7);
|
||||
|
||||
/* 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 */
|
||||
|
||||
/* Bordures. */
|
||||
--color-border: var(--color-zinc-800);
|
||||
--color-border-strong: var(--color-zinc-700);
|
||||
|
||||
/* Texte. */
|
||||
--color-fg: var(--color-zinc-200);
|
||||
--color-fg-muted: var(--color-zinc-400);
|
||||
--color-fg-subtle: var(--color-zinc-500);
|
||||
|
||||
/* Accent (interactions, focus). */
|
||||
--color-accent: var(--color-emerald-600);
|
||||
--color-accent-hover: var(--color-emerald-500);
|
||||
}
|
||||
|
||||
/* Métriques du chrome IDE (barres, onglets, lignes). Consommées via des valeurs
|
||||
arbitraires Tailwind, ex. h-[var(--ide-tab-h)]. */
|
||||
:root {
|
||||
--ide-activitybar-w: 3rem;
|
||||
--ide-titlebar-h: 2.25rem;
|
||||
--ide-tab-h: 2.25rem;
|
||||
--ide-statusbar-h: 1.5rem;
|
||||
--ide-row-h: 1.75rem;
|
||||
}
|
||||
|
||||
html,
|
||||
@@ -13,13 +45,13 @@ body,
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--color-zinc-950);
|
||||
color: var(--color-zinc-200);
|
||||
background-color: var(--color-surface-0);
|
||||
color: var(--color-fg);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
/* Tailwind v4 ne met plus cursor:pointer sur les boutons → on le rétablit
|
||||
/* 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). */
|
||||
button:not(:disabled),
|
||||
[role='button']:not([aria-disabled='true']) {
|
||||
@@ -30,22 +62,22 @@ body {
|
||||
@layer components {
|
||||
/* Champs : rayon unifié + anneau de focus visible (absent auparavant). */
|
||||
.input {
|
||||
@apply w-full rounded-lg border border-zinc-700 bg-zinc-950 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-emerald-500/40;
|
||||
@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;
|
||||
}
|
||||
|
||||
/* Boutons : socle CSS pour les usages hors composant (ex. RouterLink class="btn").
|
||||
Le composant BaseButton réplique ces variantes et ajoute tailles / loading / focus. */
|
||||
.btn {
|
||||
@apply inline-flex items-center justify-center gap-1.5 rounded-lg border border-zinc-700 bg-zinc-800 px-3 py-1.5 text-sm font-medium text-zinc-200 transition-colors hover:bg-zinc-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500/70 focus-visible:ring-offset-2 focus-visible:ring-offset-zinc-950 disabled:cursor-not-allowed disabled:opacity-50;
|
||||
@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;
|
||||
}
|
||||
.btn-primary {
|
||||
@apply inline-flex items-center justify-center gap-1.5 rounded-lg bg-emerald-600 px-3 py-1.5 text-sm font-medium text-white transition-colors hover:bg-emerald-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500/70 focus-visible:ring-offset-2 focus-visible:ring-offset-zinc-950 disabled:cursor-not-allowed disabled:opacity-50;
|
||||
@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;
|
||||
}
|
||||
.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-zinc-950 disabled:cursor-not-allowed disabled:opacity-50;
|
||||
@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;
|
||||
}
|
||||
.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-zinc-800/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500/70 focus-visible:ring-offset-2 focus-visible:ring-offset-zinc-950 disabled:cursor-not-allowed disabled:opacity-50;
|
||||
@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;
|
||||
}
|
||||
|
||||
.badge {
|
||||
@@ -54,9 +86,9 @@ body {
|
||||
|
||||
/* Cartes : factorise le motif répété dans les listes/sections/formulaires. */
|
||||
.card {
|
||||
@apply rounded-xl border border-zinc-800 bg-zinc-900/50 p-3;
|
||||
@apply rounded-xl border border-border bg-surface-1/50 p-3;
|
||||
}
|
||||
.card-inset {
|
||||
@apply rounded-lg border border-zinc-800 bg-zinc-950/40 p-2;
|
||||
@apply rounded-lg border border-border bg-surface-0/40 p-2;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div ref="root" class="flex min-h-0 flex-1 flex-col bg-[#09090b]">
|
||||
<div ref="root" class="flex min-h-0 flex-1 flex-col bg-surface-0">
|
||||
<header class="flex items-center gap-3 border-b border-zinc-800 bg-zinc-900/60 px-3 py-1.5">
|
||||
<RouterLink :to="{ name: 'sessions' }" class="text-sm text-zinc-400 transition-colors hover:text-zinc-200">
|
||||
← {{ t('terminal.back') }}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="flex h-dvh min-h-0 flex-col bg-[#09090b] text-zinc-200">
|
||||
<div class="flex h-dvh min-h-0 flex-col bg-surface-0 text-fg">
|
||||
<!-- en-tête IDE -->
|
||||
<header class="flex items-center gap-3 border-b border-zinc-800 px-3 py-1.5">
|
||||
<BaseButton size="sm" variant="ghost" :icon="ChevronLeft" :to="{ name: 'dashboard' }">{{ t('common.back') }}</BaseButton>
|
||||
|
||||
Reference in New Issue
Block a user