Draft panes are gone: an empty pane is a placeholder again, and every spawn affordance opens a dialog around the shared spawn form. Escape, Cancel, the ✕ and a backdrop click dismiss it; a successful submit puts the session in a new tab (Spawn similar and the fast-forward fallback keep placing it beside the pane they acted on). Removes the per-leaf draft state, its persistence strip, and the form reuse/teardown machinery the panes needed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
5.5 KiB
Design
Visual system for the Maestro web UI. Source of truth is
web/src/styles.css (:root token block); this document mirrors it. Register:
product — design serves the task. See PRODUCT.md for strategy.
Theme
Dark is the default (terminal context); light is a full peer. Everything is driven by CSS custom-property tokens, so a theme is a token swap, never a restyle. Resolution order:
:root[data-theme="light"|"dark"]— explicit user pin (persisted inlocalStorageasmaestro-theme), set by the header toggle.@media (prefers-color-scheme: light)— followed when the user hasn't pinned a theme.- Otherwise the
:rootdefaults (dark).
The xterm terminal stays dark in both themes by convention; --term-bg is dark
in light mode too.
Color (OKLCH)
Strategy: Restrained (product floor) with one committed signal hue — green = "alive", carrying running status, primary action, focus, and selection. Neutrals are a cool slate (hue ~256). Green is the signal, never a costume: text stays neutral high-contrast, surfaces stay slate, so the result reads terminal-native without CRT novelty.
| Role | Dark | Light |
|---|---|---|
--bg |
oklch(0.178 0.012 256) |
oklch(0.985 0.003 256) |
--surface |
oklch(0.214 0.014 256) |
oklch(1 0 0) |
--surface-2 |
oklch(0.252 0.016 256) |
oklch(0.965 0.004 256) |
--surface-3 |
oklch(0.292 0.018 256) |
oklch(0.93 0.006 256) |
--border |
oklch(0.318 0.018 256) |
oklch(0.892 0.008 256) |
--border-strong |
oklch(0.4 0.022 256) |
oklch(0.82 0.012 256) |
--ink (body text) |
oklch(0.945 0.006 256) |
oklch(0.26 0.014 256) |
--muted (secondary) |
oklch(0.722 0.014 256) |
oklch(0.47 0.016 256) |
--faint (tertiary/decorative) |
oklch(0.58 0.014 256) |
oklch(0.62 0.016 256) |
--accent / --live |
oklch(0.83–0.84 0.16–0.17 152) |
oklch(0.6 0.15–0.16 151) |
--accent-ink (text on accent) |
oklch(0.2 0.03 256) |
oklch(0.99 0.01 152) |
--danger |
oklch(0.72 0.18 25) |
oklch(0.55 0.21 27) |
--warn |
oklch(0.82 0.14 85) |
oklch(0.66 0.15 75) |
Body and secondary text meet WCAG AA (≥4.5:1) against their surface in both
themes; --faint is reserved for decorative/large use only. Status never relies
on color alone — a running dot pulses and is filled; a dead session is a hollow
ring and carries an "exited" badge.
Tints/glow derive from the accent at low alpha: --accent-tint (0.12),
--accent-ring (focus, ~0.55), --live-glow (status halo).
Typography
Monospace is the UI typeface (brand identity), system-stack only (offline- safe, zero web-font dependency, embedded clean into the web binary):
--font-mono:ui-monospace, "SF Mono", "Cascadia Code", "JetBrains Mono", "Roboto Mono", Menlo, Consolas, monospace— chrome, labels, data, ids, the wordmark.--font-sans:system-uistack — reserved for the few prose strings (empty-state sentences).
Fixed rem scale (product register; not fluid/clamped): --fs-11 0.6875rem
through --fs-20 1.25rem, in ~1.12–1.2 steps. Base body --fs-15 (0.9375rem),
line-height 1.45. No display sizes — the largest type is the 1.25rem wordmark.
Spacing, radius, sizing
- Spacing scale:
--sp-1..--sp-8= 4 / 8 / 12 / 16 / 20 / 24 / 32 px. - Radius:
--r-16 ·--r-29 ·--r-314 ·--r-pill999. - Touch/row sizing:
--row-h46,--control-h38,--tap40 (mobile-first touch targets).
Motion
State and feedback only; 120–240 ms, ease-out (cubic-bezier(0.22,1,0.36,1)),
no bounce. Tokens --dur-fast/--dur/--dur-slow.
- Living signals: brand dot + running-status ripple pulse (the "alive" cue);
connectingstate pill pulses. - Interaction: row hover/focus, button press (1px), input focus ring, chevron rotation, theme-toggle sun/moon swap.
- Every animation has a
prefers-reduced-motion: reducepath (pulses off, transitions near-instant).
Layout & structure
Mobile-first, structural responsiveness (not fluid type). Single ≤520px
breakpoint stacks the header (full-width search, wrapped actions). A faint
slate grid texture sits on --bg as terminal atmosphere (very low contrast,
not decoration).
Components
Three surfaces, all DOM built in TS, styled by class:
- Catalog — sticky blurred header (pulsing wordmark, search, theme toggle,
Spawn); status bar (
N sessions · M livetally); session tree (collapsible directory folders with counts, leaf rows with status dot, name, command meta, exited badge, reveal-on-hover kill). Empty states are distinct and teach: no-sessions, no-search-match. - Attach — header (back,
session <id>, live/closed/error state pill) over a framed dark xterm with a green cursor. - Spawn modal — the spawn form in a centered dialog (a bottom sheet below 600px) over a blurred backdrop; mono inputs with green focus ring; grouped worktree fieldset. Escape, Cancel, ✕ and a backdrop click all dismiss it. Submitting gives the new session its own tab; "Spawn similar" seeds the form from a session's dir + command and puts the result beside it.
Interactive elements ship default / hover / focus-visible / active / disabled states; the accent green is used only for action, selection, focus, and live status — never decoration.
Dev review aids (DEV-only, stripped from production)
web/src/mock.ts + query params seed a representative catalog without a running
daemon: ?mock (seed sessions), ?mock&spawn (open the spawn modal),
?theme=light|dark (pin a theme). Guarded by import.meta.env.DEV.