From 7935663ed7b2adcf15fe2c22475a6c6935c071a0 Mon Sep 17 00:00:00 2001 From: Miguel Palhas Date: Wed, 2 Sep 2026 16:14:12 +0100 Subject: [PATCH] feat(maestro): graphite web default + palette picker Graphite replaces the signal-green accent as the default web palette; data-palette on selects ice-cyan, periwinkle, amber-phosphor, ember or orchid (each with dark + light token sets), picked from a new sidebar-footer dropdown and persisted in localStorage. Co-Authored-By: Claude Fable 5 --- web/src/catalog.ts | 3 +- web/src/kit.ts | 6 +- web/src/styles.css | 431 +++++++++++++++++++++++++++++++++++++----- web/src/theme.test.ts | 74 ++++++++ web/src/theme.ts | 65 ++++++- 5 files changed, 528 insertions(+), 51 deletions(-) create mode 100644 web/src/theme.test.ts diff --git a/web/src/catalog.ts b/web/src/catalog.ts index 651bab5..0efa00a 100644 --- a/web/src/catalog.ts +++ b/web/src/catalog.ts @@ -24,7 +24,7 @@ import { } from "./activity.js"; import type { TreeNode } from "./tree.js"; import { buildTree } from "./tree.js"; -import { createThemeToggle } from "./theme.js"; +import { createPaletteSelect, createThemeToggle } from "./theme.js"; /** Ask the shell to open the spawn modal. Handled in main.ts, where the * multi-view controller lives; dispatched from every Spawn affordance. */ @@ -380,6 +380,7 @@ export function renderSidebar(store: CatalogStore, opts: SidebarOptions = {}): S helpBtn.addEventListener("click", () => toggleHelp()); footer.appendChild(helpBtn); footer.appendChild(createThemeToggle()); + footer.appendChild(createPaletteSelect()); // DEV-only shortcut to the standalone design kit (?kit). Lets you reach it by // clicking inside a proxied preview (e.g. crit live) that has no address bar. if (import.meta.env.DEV) { diff --git a/web/src/kit.ts b/web/src/kit.ts index 352afe2..f1a721e 100644 --- a/web/src/kit.ts +++ b/web/src/kit.ts @@ -4,7 +4,7 @@ // critiqued as a whole — buttons (including terminal-native variants for // review), inputs, chips, badges, session rows, and empty states. import "./kit.css"; -import { createThemeToggle } from "./theme.js"; +import { createPaletteSelect, createThemeToggle } from "./theme.js"; function h(tag: string, className?: string, html?: string): HTMLElement { const el = document.createElement(tag); @@ -102,7 +102,7 @@ export function renderKit(): HTMLElement { back.className = "btn btn-ghost"; back.href = "?mock"; back.textContent = "← back to app"; - headerActions.append(back, createThemeToggle()); + headerActions.append(back, createThemeToggle(), createPaletteSelect()); header.appendChild(headerActions); page.appendChild(header); @@ -113,7 +113,7 @@ export function renderKit(): HTMLElement { { const { el, body } = section( "Color tokens", - "Slate neutrals (hue ~256) + signal-green = alive. Swap by theme toggle.", + "Graphite neutrals; activity colours are the only hue. Swap by theme toggle + palette picker.", ); body.classList.add("kit-swatches"); const tokens = [ diff --git a/web/src/styles.css b/web/src/styles.css index 205ea26..9afeb05 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -1,8 +1,12 @@ /* ========================================================================= maestro web — design system Register: product (a tool; design serves the task). - Identity: precise, calm, alive. Monospace-as-UI; signal-green = "alive". + Identity: precise, calm, alive. Monospace-as-UI; graphite chrome where the + activity colours (green = alive) are the only hue on screen. Theme: dark (default) + light, all via OKLCH custom-property tokens. + Palette: `data-palette` on swaps the accent + base tint (see the + "palettes" section at the end of the token area); graphite is the bare + document. ========================================================================= */ :root { @@ -63,24 +67,25 @@ /* ---- color: dark (default) ---- */ color-scheme: dark; - --bg: oklch(0.178 0.012 256); - --bg-grid: oklch(0.215 0.014 256); - --surface: oklch(0.214 0.014 256); - --surface-2: oklch(0.252 0.016 256); - --surface-3: oklch(0.292 0.018 256); - --border: oklch(0.318 0.018 256); - --border-strong: oklch(0.4 0.022 256); + --bg: oklch(0.172 0.006 256); + --bg-grid: oklch(0.21 0.007 256); + --surface: oklch(0.208 0.007 256); + --surface-2: oklch(0.246 0.008 256); + --surface-3: oklch(0.286 0.009 256); + --border: oklch(0.312 0.01 256); + --border-strong: oklch(0.395 0.012 256); - --ink: oklch(0.945 0.006 256); - --muted: oklch(0.722 0.014 256); - --faint: oklch(0.58 0.014 256); + --ink: oklch(0.945 0.004 256); + --muted: oklch(0.72 0.008 256); + --faint: oklch(0.58 0.008 256); - /* signal green = "alive", primary action, focus, selection */ - --accent: oklch(0.83 0.16 152); - --accent-strong: oklch(0.88 0.17 152); - --accent-ink: oklch(0.2 0.03 256); - --accent-tint: oklch(0.83 0.16 152 / 0.12); - --accent-ring: oklch(0.83 0.16 152 / 0.55); + /* graphite: the accent is ink itself — primary action, focus, selection. + The activity colours below stay the only hue on screen. */ + --accent: oklch(0.92 0.005 256); + --accent-strong: oklch(0.97 0.003 256); + --accent-ink: oklch(0.2 0.012 256); + --accent-tint: oklch(0.92 0.005 256 / 0.1); + --accent-ring: oklch(0.92 0.005 256 / 0.5); --live: oklch(0.84 0.17 152); --live-glow: oklch(0.84 0.17 152 / 0.55); @@ -92,30 +97,30 @@ --shadow-1: 0 1px 2px oklch(0 0 0 / 0.4); --shadow-2: 0 12px 28px -8px oklch(0 0 0 / 0.6), 0 2px 8px oklch(0 0 0 / 0.4); - --term-bg: oklch(0.158 0.012 256); + --term-bg: oklch(0.155 0.006 256); } :root[data-theme="light"], :root[data-theme="light"] body { color-scheme: light; - --bg: oklch(0.985 0.003 256); - --bg-grid: oklch(0.93 0.006 256); + --bg: oklch(0.985 0.002 256); + --bg-grid: oklch(0.93 0.004 256); --surface: oklch(1 0 0); - --surface-2: oklch(0.965 0.004 256); - --surface-3: oklch(0.93 0.006 256); - --border: oklch(0.892 0.008 256); - --border-strong: oklch(0.82 0.012 256); + --surface-2: oklch(0.965 0.003 256); + --surface-3: oklch(0.93 0.004 256); + --border: oklch(0.892 0.005 256); + --border-strong: oklch(0.82 0.008 256); - --ink: oklch(0.26 0.014 256); - --muted: oklch(0.47 0.016 256); - --faint: oklch(0.62 0.016 256); + --ink: oklch(0.26 0.012 256); + --muted: oklch(0.47 0.012 256); + --faint: oklch(0.62 0.012 256); - --accent: oklch(0.6 0.15 151); - --accent-strong: oklch(0.55 0.16 151); - --accent-ink: oklch(0.99 0.01 152); - --accent-tint: oklch(0.6 0.15 151 / 0.12); - --accent-ring: oklch(0.6 0.15 151 / 0.5); + --accent: oklch(0.27 0.01 256); + --accent-strong: oklch(0.2 0.01 256); + --accent-ink: oklch(0.985 0.002 256); + --accent-tint: oklch(0.27 0.01 256 / 0.1); + --accent-ring: oklch(0.27 0.01 256 / 0.45); --live: oklch(0.6 0.16 151); --live-glow: oklch(0.6 0.16 151 / 0.45); @@ -136,23 +141,23 @@ :root:not([data-theme]) { color-scheme: light; - --bg: oklch(0.985 0.003 256); - --bg-grid: oklch(0.93 0.006 256); + --bg: oklch(0.985 0.002 256); + --bg-grid: oklch(0.93 0.004 256); --surface: oklch(1 0 0); - --surface-2: oklch(0.965 0.004 256); - --surface-3: oklch(0.93 0.006 256); - --border: oklch(0.892 0.008 256); - --border-strong: oklch(0.82 0.012 256); + --surface-2: oklch(0.965 0.003 256); + --surface-3: oklch(0.93 0.004 256); + --border: oklch(0.892 0.005 256); + --border-strong: oklch(0.82 0.008 256); - --ink: oklch(0.26 0.014 256); - --muted: oklch(0.47 0.016 256); - --faint: oklch(0.62 0.016 256); + --ink: oklch(0.26 0.012 256); + --muted: oklch(0.47 0.012 256); + --faint: oklch(0.62 0.012 256); - --accent: oklch(0.6 0.15 151); - --accent-strong: oklch(0.55 0.16 151); - --accent-ink: oklch(0.99 0.01 152); - --accent-tint: oklch(0.6 0.15 151 / 0.12); - --accent-ring: oklch(0.6 0.15 151 / 0.5); + --accent: oklch(0.27 0.01 256); + --accent-strong: oklch(0.2 0.01 256); + --accent-ink: oklch(0.985 0.002 256); + --accent-tint: oklch(0.27 0.01 256 / 0.1); + --accent-ring: oklch(0.27 0.01 256 / 0.45); --live: oklch(0.6 0.16 151); --live-glow: oklch(0.6 0.16 151 / 0.45); @@ -169,6 +174,311 @@ } } +/* ---- palettes ---- + `data-palette` on recolours the accent + base tint on top of the + light/dark machinery above. Graphite (the default) is the bare document. + Each palette ships a dark block, a pinned-light block, and a system-light + block; `--live`/`--danger`/`--warn` stay semantic and are not redefined + (except amber-phosphor, whose accent would collide with the stock warn). */ + +/* ice-cyan: signal-cyan accent on the slate base. */ +:root[data-palette="ice-cyan"] { + --bg: oklch(0.178 0.012 256); + --bg-grid: oklch(0.215 0.014 256); + --surface: oklch(0.214 0.014 256); + --surface-2: oklch(0.252 0.016 256); + --surface-3: oklch(0.292 0.018 256); + --border: oklch(0.318 0.018 256); + --border-strong: oklch(0.4 0.022 256); + --ink: oklch(0.945 0.006 256); + --muted: oklch(0.722 0.014 256); + --faint: oklch(0.58 0.014 256); + --accent: oklch(0.85 0.115 205); + --accent-strong: oklch(0.89 0.12 205); + --accent-ink: oklch(0.24 0.03 230); + --accent-tint: oklch(0.85 0.115 205 / 0.12); + --accent-ring: oklch(0.85 0.115 205 / 0.55); + --term-bg: oklch(0.158 0.012 256); +} +:root[data-palette="ice-cyan"][data-theme="light"], +:root[data-palette="ice-cyan"][data-theme="light"] body { + --bg: oklch(0.985 0.003 256); + --bg-grid: oklch(0.93 0.006 256); + --surface: oklch(1 0 0); + --surface-2: oklch(0.965 0.004 256); + --surface-3: oklch(0.93 0.006 256); + --border: oklch(0.892 0.008 256); + --border-strong: oklch(0.82 0.012 256); + --ink: oklch(0.26 0.014 256); + --muted: oklch(0.47 0.016 256); + --faint: oklch(0.62 0.016 256); + --accent: oklch(0.52 0.1 220); + --accent-strong: oklch(0.47 0.11 220); + --accent-ink: oklch(0.99 0.005 220); + --accent-tint: oklch(0.52 0.1 220 / 0.12); + --accent-ring: oklch(0.52 0.1 220 / 0.5); + --term-bg: oklch(0.165 0.012 256); +} +@media (prefers-color-scheme: light) { + :root[data-palette="ice-cyan"]:not([data-theme]) { + --bg: oklch(0.985 0.003 256); + --bg-grid: oklch(0.93 0.006 256); + --surface: oklch(1 0 0); + --surface-2: oklch(0.965 0.004 256); + --surface-3: oklch(0.93 0.006 256); + --border: oklch(0.892 0.008 256); + --border-strong: oklch(0.82 0.012 256); + --ink: oklch(0.26 0.014 256); + --muted: oklch(0.47 0.016 256); + --faint: oklch(0.62 0.016 256); + --accent: oklch(0.52 0.1 220); + --accent-strong: oklch(0.47 0.11 220); + --accent-ink: oklch(0.99 0.005 220); + --accent-tint: oklch(0.52 0.1 220 / 0.12); + --accent-ring: oklch(0.52 0.1 220 / 0.5); + --term-bg: oklch(0.165 0.012 256); + } +} +/* periwinkle: indigo base, blue-violet accent. */ +:root[data-palette="periwinkle"] { + --bg: oklch(0.175 0.018 278); + --bg-grid: oklch(0.212 0.021 278); + --surface: oklch(0.212 0.021 278); + --surface-2: oklch(0.25 0.024 278); + --surface-3: oklch(0.29 0.026 278); + --border: oklch(0.318 0.026 278); + --border-strong: oklch(0.4 0.028 278); + --ink: oklch(0.93 0.012 278); + --muted: oklch(0.71 0.02 278); + --faint: oklch(0.57 0.022 278); + --accent: oklch(0.75 0.125 275); + --accent-strong: oklch(0.79 0.13 275); + --accent-ink: oklch(0.18 0.04 278); + --accent-tint: oklch(0.75 0.125 275 / 0.14); + --accent-ring: oklch(0.75 0.125 275 / 0.55); + --term-bg: oklch(0.155 0.018 278); +} +:root[data-palette="periwinkle"][data-theme="light"], +:root[data-palette="periwinkle"][data-theme="light"] body { + --bg: oklch(0.982 0.004 278); + --bg-grid: oklch(0.93 0.008 278); + --surface: oklch(1 0 0); + --surface-2: oklch(0.96 0.006 278); + --surface-3: oklch(0.93 0.008 278); + --border: oklch(0.885 0.01 278); + --border-strong: oklch(0.82 0.014 278); + --ink: oklch(0.27 0.02 278); + --muted: oklch(0.47 0.02 278); + --faint: oklch(0.62 0.02 278); + --accent: oklch(0.5 0.17 272); + --accent-strong: oklch(0.45 0.17 272); + --accent-ink: oklch(0.99 0.005 272); + --accent-tint: oklch(0.5 0.17 272 / 0.12); + --accent-ring: oklch(0.5 0.17 272 / 0.5); + --term-bg: oklch(0.165 0.018 278); +} +@media (prefers-color-scheme: light) { + :root[data-palette="periwinkle"]:not([data-theme]) { + --bg: oklch(0.982 0.004 278); + --bg-grid: oklch(0.93 0.008 278); + --surface: oklch(1 0 0); + --surface-2: oklch(0.96 0.006 278); + --surface-3: oklch(0.93 0.008 278); + --border: oklch(0.885 0.01 278); + --border-strong: oklch(0.82 0.014 278); + --ink: oklch(0.27 0.02 278); + --muted: oklch(0.47 0.02 278); + --faint: oklch(0.62 0.02 278); + --accent: oklch(0.5 0.17 272); + --accent-strong: oklch(0.45 0.17 272); + --accent-ink: oklch(0.99 0.005 272); + --accent-tint: oklch(0.5 0.17 272 / 0.12); + --accent-ring: oklch(0.5 0.17 272 / 0.5); + --term-bg: oklch(0.165 0.018 278); + } +} + +/* amber-phosphor: CRT amber on warm graphite. `--warn` shifts orange so the + awaiting colour stays distinct from the accent. */ +:root[data-palette="amber-phosphor"] { + --bg: oklch(0.178 0.008 70); + --bg-grid: oklch(0.214 0.01 70); + --surface: oklch(0.214 0.01 70); + --surface-2: oklch(0.252 0.012 70); + --surface-3: oklch(0.292 0.013 70); + --border: oklch(0.318 0.014 70); + --border-strong: oklch(0.4 0.016 70); + --ink: oklch(0.94 0.008 78); + --muted: oklch(0.72 0.014 75); + --faint: oklch(0.58 0.014 75); + --accent: oklch(0.83 0.14 78); + --accent-strong: oklch(0.87 0.145 78); + --accent-ink: oklch(0.25 0.05 78); + --accent-tint: oklch(0.83 0.14 78 / 0.13); + --accent-ring: oklch(0.83 0.14 78 / 0.55); + --warn: oklch(0.76 0.15 55); + --term-bg: oklch(0.158 0.008 70); +} +:root[data-palette="amber-phosphor"][data-theme="light"], +:root[data-palette="amber-phosphor"][data-theme="light"] body { + --bg: oklch(0.983 0.004 78); + --bg-grid: oklch(0.93 0.007 78); + --surface: oklch(1 0 0); + --surface-2: oklch(0.962 0.005 78); + --surface-3: oklch(0.93 0.007 78); + --border: oklch(0.888 0.01 78); + --border-strong: oklch(0.82 0.012 78); + --ink: oklch(0.27 0.015 70); + --muted: oklch(0.47 0.016 70); + --faint: oklch(0.62 0.016 70); + --accent: oklch(0.6 0.13 70); + --accent-strong: oklch(0.55 0.13 70); + --accent-ink: oklch(0.99 0.01 78); + --accent-tint: oklch(0.6 0.13 70 / 0.12); + --accent-ring: oklch(0.6 0.13 70 / 0.5); + --warn: oklch(0.58 0.14 50); + --term-bg: oklch(0.165 0.008 70); +} +@media (prefers-color-scheme: light) { + :root[data-palette="amber-phosphor"]:not([data-theme]) { + --bg: oklch(0.983 0.004 78); + --bg-grid: oklch(0.93 0.007 78); + --surface: oklch(1 0 0); + --surface-2: oklch(0.962 0.005 78); + --surface-3: oklch(0.93 0.007 78); + --border: oklch(0.888 0.01 78); + --border-strong: oklch(0.82 0.012 78); + --ink: oklch(0.27 0.015 70); + --muted: oklch(0.47 0.016 70); + --faint: oklch(0.62 0.016 70); + --accent: oklch(0.6 0.13 70); + --accent-strong: oklch(0.55 0.13 70); + --accent-ink: oklch(0.99 0.01 78); + --accent-tint: oklch(0.6 0.13 70 / 0.12); + --accent-ring: oklch(0.6 0.13 70 / 0.5); + --warn: oklch(0.58 0.14 50); + --term-bg: oklch(0.165 0.008 70); + } +} + +/* ember: warm orange-red accent. */ +:root[data-palette="ember"] { + --bg: oklch(0.18 0.009 50); + --bg-grid: oklch(0.216 0.011 50); + --surface: oklch(0.216 0.011 50); + --surface-2: oklch(0.254 0.013 50); + --surface-3: oklch(0.294 0.014 50); + --border: oklch(0.32 0.015 50); + --border-strong: oklch(0.4 0.017 50); + --ink: oklch(0.94 0.007 50); + --muted: oklch(0.72 0.013 50); + --faint: oklch(0.58 0.013 50); + --accent: oklch(0.74 0.15 45); + --accent-strong: oklch(0.78 0.155 45); + --accent-ink: oklch(0.2 0.04 45); + --accent-tint: oklch(0.74 0.15 45 / 0.14); + --accent-ring: oklch(0.74 0.15 45 / 0.55); + --term-bg: oklch(0.158 0.009 50); +} +:root[data-palette="ember"][data-theme="light"], +:root[data-palette="ember"][data-theme="light"] body { + --bg: oklch(0.983 0.004 50); + --bg-grid: oklch(0.93 0.006 50); + --surface: oklch(1 0 0); + --surface-2: oklch(0.962 0.005 50); + --surface-3: oklch(0.93 0.006 50); + --border: oklch(0.888 0.009 50); + --border-strong: oklch(0.82 0.012 50); + --ink: oklch(0.27 0.014 50); + --muted: oklch(0.47 0.015 50); + --faint: oklch(0.62 0.015 50); + --accent: oklch(0.58 0.17 40); + --accent-strong: oklch(0.53 0.17 40); + --accent-ink: oklch(0.99 0.008 40); + --accent-tint: oklch(0.58 0.17 40 / 0.11); + --accent-ring: oklch(0.58 0.17 40 / 0.5); + --term-bg: oklch(0.165 0.009 50); +} +@media (prefers-color-scheme: light) { + :root[data-palette="ember"]:not([data-theme]) { + --bg: oklch(0.983 0.004 50); + --bg-grid: oklch(0.93 0.006 50); + --surface: oklch(1 0 0); + --surface-2: oklch(0.962 0.005 50); + --surface-3: oklch(0.93 0.006 50); + --border: oklch(0.888 0.009 50); + --border-strong: oklch(0.82 0.012 50); + --ink: oklch(0.27 0.014 50); + --muted: oklch(0.47 0.015 50); + --faint: oklch(0.62 0.015 50); + --accent: oklch(0.58 0.17 40); + --accent-strong: oklch(0.53 0.17 40); + --accent-ink: oklch(0.99 0.008 40); + --accent-tint: oklch(0.58 0.17 40 / 0.11); + --accent-ring: oklch(0.58 0.17 40 / 0.5); + --term-bg: oklch(0.165 0.009 50); + } +} + +/* orchid: muted violet accent on a faintly violet base. */ +:root[data-palette="orchid"] { + --bg: oklch(0.177 0.014 305); + --bg-grid: oklch(0.213 0.017 305); + --surface: oklch(0.213 0.017 305); + --surface-2: oklch(0.251 0.02 305); + --surface-3: oklch(0.291 0.021 305); + --border: oklch(0.318 0.022 305); + --border-strong: oklch(0.4 0.024 305); + --ink: oklch(0.935 0.01 305); + --muted: oklch(0.715 0.018 305); + --faint: oklch(0.575 0.018 305); + --accent: oklch(0.78 0.13 310); + --accent-strong: oklch(0.82 0.135 310); + --accent-ink: oklch(0.22 0.05 310); + --accent-tint: oklch(0.78 0.13 310 / 0.14); + --accent-ring: oklch(0.78 0.13 310 / 0.55); + --term-bg: oklch(0.155 0.014 305); +} +:root[data-palette="orchid"][data-theme="light"], +:root[data-palette="orchid"][data-theme="light"] body { + --bg: oklch(0.983 0.004 305); + --bg-grid: oklch(0.93 0.007 305); + --surface: oklch(1 0 0); + --surface-2: oklch(0.961 0.005 305); + --surface-3: oklch(0.93 0.007 305); + --border: oklch(0.886 0.01 305); + --border-strong: oklch(0.82 0.013 305); + --ink: oklch(0.27 0.018 305); + --muted: oklch(0.47 0.018 305); + --faint: oklch(0.62 0.018 305); + --accent: oklch(0.5 0.17 308); + --accent-strong: oklch(0.45 0.17 308); + --accent-ink: oklch(0.99 0.005 308); + --accent-tint: oklch(0.5 0.17 308 / 0.11); + --accent-ring: oklch(0.5 0.17 308 / 0.5); + --term-bg: oklch(0.165 0.014 305); +} +@media (prefers-color-scheme: light) { + :root[data-palette="orchid"]:not([data-theme]) { + --bg: oklch(0.983 0.004 305); + --bg-grid: oklch(0.93 0.007 305); + --surface: oklch(1 0 0); + --surface-2: oklch(0.961 0.005 305); + --surface-3: oklch(0.93 0.007 305); + --border: oklch(0.886 0.01 305); + --border-strong: oklch(0.82 0.013 305); + --ink: oklch(0.27 0.018 305); + --muted: oklch(0.47 0.018 305); + --faint: oklch(0.62 0.018 305); + --accent: oklch(0.5 0.17 308); + --accent-strong: oklch(0.45 0.17 308); + --accent-ink: oklch(0.99 0.005 308); + --accent-tint: oklch(0.5 0.17 308 / 0.11); + --accent-ring: oklch(0.5 0.17 308 / 0.5); + --term-bg: oklch(0.165 0.014 305); + } +} + /* ---------- reset / base ---------- */ * { @@ -435,6 +745,35 @@ textarea { flex: 1 1 auto; } +/* Palette picker: a ghost-button-shaped