From 1cb6c361c9f9458ddc158f21467afd8e9ff937c2 Mon Sep 17 00:00:00 2001 From: Yuhan Lei Date: Sat, 23 May 2026 08:25:37 +0800 Subject: [PATCH 1/7] feat(ui): warm-tone three-tier dark surface + light sidebar lift MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reverse the 2026-05-14 "three columns same color" dark lock by lifting `--sidebar` from canvas into its own middle tier; bump `--surface-raised` and dark ink to restore depth. Light `--sidebar` moves one notch to separate from cream. Dark (pawwork.json + theme.css dual track): - --sidebar #1a1917 → #232120 (chrome middle tier) - --surface-raised #262523 → #2d2a27 (~1.27:1 against canvas) - --fg-strong / --fg-base #ebe7e0 → #f4efe7 (+9 sRGB delta sharpen) - --fg-weak / --fg-weaker #7e7872 → #928c82 (~5.6:1, AA large-text) - --icon-strong #ebe7e0 → #f4efe7 - --icon-base / --icon-weak #7e7872 → #928c82 - Rewrite the main dark block comment from "2-tier collapse" to "3-tier". Light: - --sidebar #f9f9f9 → #f2f0ed (distinguish from cream #faf9f7) Explicitly unchanged: canvas, hairline 0.08, brand orange, R-B=3, font sizes, spacing, shadows, radii, light fg / icon, palette.ink (terminal seed, intentionally decoupled from chrome). Expected cascade (raising --surface-raised lifts every raised consumer): dialog, composer dock, sheet, menu, msg-user bubble, list-hover, command-palette body, kbd. --- packages/ui/src/styles/theme.css | 49 ++++++++++++----------- packages/ui/src/theme/themes/pawwork.json | 20 ++++----- 2 files changed, 35 insertions(+), 34 deletions(-) diff --git a/packages/ui/src/styles/theme.css b/packages/ui/src/styles/theme.css index 89eae6be2..758d7efab 100644 --- a/packages/ui/src/styles/theme.css +++ b/packages/ui/src/styles/theme.css @@ -87,7 +87,7 @@ --surface-raised: #ffffff; --surface-sunken: #faf9f7; /* light alias → bg-cream; no third neutral tier (L9) */ --code-surface: rgba(0, 0, 0, 0.04); /* #642: alpha overlay for inline code + code-block */ - --sidebar: #f9f9f9; /* neutral pale grey, decoupled from bg-cream per ui_kits mock */ + --sidebar: #f2f0ed; /* chrome 独立 surface(与 cream #faf9f7 区分) */ --surface-base-active: rgba(0, 0, 0, 0.051); /* ────────── Light ink · 2-tier collapse (strong + weak); base/weaker pairs merge. @@ -278,24 +278,25 @@ * aligned with that runtime path; it is not the runtime source of truth. * ────────────────────────────────────────────────────────────────────────── */ :root[data-color-scheme="dark"] { - /* Dark surface · 2-tier collapse (canvas + raised), R-B=3 warm coffee. - * The legacy 6-tier ladder collapses into 2 visible tones: - * canvas #1a1917 — chrome floor: bg / sidebar / popover / textfield / sunken alias - * raised #262523 — anchored: dialog / composer / msg-user / list-hover / command palette + /* Dark surface · 3-tier (canvas + sidebar + raised), R-B=3 warm coffee. + * The legacy 6-tier ladder collapses into 3 visible tones: + * canvas #1a1917 — chrome floor: bg / sunken alias / cream alias / surface-base alias + * sidebar #232120 — chrome middle: 左栏独立 surface (2026-05-23 从 canvas 拆出) + * raised #2d2a27 — anchored: dialog / composer / msg-user / list-hover / popover / textfield / picker / cp body * Token names survive for light-mode parity and callsite stability. */ --bg-base: #1a1917; --bg-cream: #1a1917; /* dark collapse → canvas (no separate cream tier) */ - --surface-base: #1a1917; /* dark collapse → canvas (popover / textfield share chrome) */ - --surface-raised: #262523; + --surface-base: #1a1917; /* dark collapse → canvas (secondary button / inline controls) */ + --surface-raised: #2d2a27; --surface-sunken: #1a1917; /* dark collapse → canvas (no sunken tier) */ --code-surface: rgba(255, 255, 255, 0.06); /* #642: alpha overlay for inline code + code-block */ - --sidebar: #1a1917; /* dark collapse → canvas (three columns share one surface) */ + --sidebar: #232120; /* sidebar chrome 中间一档 */ /* Dark ink · 2-tier collapse (fg + fg-weak); strong/base and weak/weaker pairs merge */ - --fg-strong: #ebe7e0; - --fg-base: #ebe7e0; /* dark collapse → fg */ - --fg-weak: #7e7872; - --fg-weaker: #7e7872; /* dark collapse → fg-weak */ + --fg-strong: #f4efe7; + --fg-base: #f4efe7; /* dark collapse → fg */ + --fg-weak: #928c82; + --fg-weaker: #928c82; /* dark collapse → fg-weak */ /* Dark border · 1-tier collapse to a single hairline alpha; container outlines * keep the `--ring-base` 1px white-18% halo for elevation pickup. @@ -307,9 +308,9 @@ --border-weaker: rgba(255, 255, 255, 0.08); /* Icon mirrors collapsed dark ink */ - --icon-base: #7e7872; - --icon-strong: #ebe7e0; - --icon-weak: #7e7872; + --icon-base: #928c82; + --icon-strong: #f4efe7; + --icon-weak: #928c82; /* Brand hover lifted in dark for legibility on warm coffee surface */ --brand-primary-hover: #ff7a3a; @@ -435,23 +436,23 @@ --bg-base: #1a1917; --bg-cream: #1a1917; --surface-base: #1a1917; - --surface-raised: #262523; + --surface-raised: #2d2a27; --surface-sunken: #1a1917; --code-surface: rgba(255, 255, 255, 0.06); /* #642: alpha overlay for inline code + code-block */ - --sidebar: #1a1917; + --sidebar: #232120; - --fg-strong: #ebe7e0; - --fg-base: #ebe7e0; - --fg-weak: #7e7872; - --fg-weaker: #7e7872; + --fg-strong: #f4efe7; + --fg-base: #f4efe7; + --fg-weak: #928c82; + --fg-weaker: #928c82; --border-base: rgba(255, 255, 255, 0.08); --border-weak: rgba(255, 255, 255, 0.08); --border-weaker: rgba(255, 255, 255, 0.08); - --icon-base: #7e7872; - --icon-strong: #ebe7e0; - --icon-weak: #7e7872; + --icon-base: #928c82; + --icon-strong: #f4efe7; + --icon-weak: #928c82; --brand-primary-hover: #ff7a3a; diff --git a/packages/ui/src/theme/themes/pawwork.json b/packages/ui/src/theme/themes/pawwork.json index bfe59abc6..be4514bcf 100644 --- a/packages/ui/src/theme/themes/pawwork.json +++ b/packages/ui/src/theme/themes/pawwork.json @@ -54,7 +54,7 @@ "letter-spacing-display": "-0.32px", "letter-spacing-h1": "-0.16px", "letter-spacing-cjk": "0", - "sidebar": "#f9f9f9", + "sidebar": "#f2f0ed", "surface-interactive-base": "#fdf6f0", "surface-interactive-hover": "#fbece0", "surface-base-active": "rgba(0, 0, 0, 0.051)", @@ -103,7 +103,7 @@ "bg-base": "#1a1917", "bg-cream": "#1a1917", "surface-base": "#1a1917", - "surface-raised": "#262523", + "surface-raised": "#2d2a27", "surface-sunken": "#1a1917", "code-surface": "rgba(255, 255, 255, 0.06)", "font-size-display": "28px", @@ -137,21 +137,21 @@ "letter-spacing-display": "-0.32px", "letter-spacing-h1": "-0.16px", "letter-spacing-cjk": "0", - "sidebar": "#1a1917", + "sidebar": "#232120", "surface-interactive-base": "rgba(255, 89, 16, 0.14)", "surface-interactive-hover": "rgba(255, 89, 16, 0.20)", "surface-base-active": "rgba(255, 255, 255, 0.059)", - "fg-strong": "#ebe7e0", - "fg-base": "#ebe7e0", - "fg-weak": "#7e7872", - "fg-weaker": "#7e7872", + "fg-strong": "#f4efe7", + "fg-base": "#f4efe7", + "fg-weak": "#928c82", + "fg-weaker": "#928c82", "border-base": "rgba(255, 255, 255, 0.08)", "border-weak": "rgba(255, 255, 255, 0.08)", "border-weaker": "rgba(255, 255, 255, 0.08)", - "icon-base": "#7e7872", + "icon-base": "#928c82", "icon-disabled": "#4a4542", - "icon-strong": "#ebe7e0", - "icon-weak": "#7e7872", + "icon-strong": "#f4efe7", + "icon-weak": "#928c82", "diff-add": "rgba(58, 176, 107, 0.15)", "diff-del": "rgba(230, 92, 74, 0.15)", "error": "#e65c4a", From 8df5c0bdd93562d5d770588d051b108fc2a84d98 Mon Sep 17 00:00:00 2001 From: Yuhan Lei Date: Sat, 23 May 2026 08:25:48 +0800 Subject: [PATCH 2/7] refactor(ui): lift popover / picker / textfield wrappers to raised MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirror the token lift in the three floating-surface containers. With the new dark 3-tier system, the old --surface-base equals canvas; floating elements that stayed on canvas would read as "pressed in" instead of floating. Moving them to --surface-raised restores the depth cue. popover.css: - container: surface-base → surface-raised - item hover / focus-visible: bg-cream → row-hover-overlay (cream collapses to canvas in dark and would press the item in; row-hover-overlay is the spec hover token for popover family) - item active / selected: bg-cream → surface-interactive-base (aligns with DESIGN.md L622 popover-family active rule) - selected adds font-weight 500 + color var(--fg-strong) picker.css: - [data-picker-content] background: surface-base → surface-raised - picker.test.ts: rename the test and update the assertion to match. text-field.css: - New dark-only override on both tracks ([data-color-scheme="dark"] + @media (prefers-color-scheme: dark)) - Targets only the normal-variant wrapper when neither data-disabled nor data-readonly is set. - The :not() guard keeps disabled / readonly on --bg-cream (= canvas in dark); a normal input lifts to raised, and disabled sinks back to thread, which reads as a legitimate disabled state. - Light normal-variant keeps --surface-base = white, unchanged. --- packages/ui/src/components/picker.css | 2 +- packages/ui/src/components/picker.test.ts | 4 ++-- packages/ui/src/components/popover.css | 12 +++++++----- packages/ui/src/components/text-field.css | 20 ++++++++++++++++++++ 4 files changed, 30 insertions(+), 8 deletions(-) diff --git a/packages/ui/src/components/picker.css b/packages/ui/src/components/picker.css index 73fffb386..b286384c3 100644 --- a/packages/ui/src/components/picker.css +++ b/packages/ui/src/components/picker.css @@ -29,7 +29,7 @@ } [data-picker-content] { - background-color: var(--surface-base); + background-color: var(--surface-raised); border-radius: var(--radius-md); padding: 4px; box-shadow: var(--ring-base), var(--shadow-floating); diff --git a/packages/ui/src/components/picker.test.ts b/packages/ui/src/components/picker.test.ts index 0e086ab6a..8004fab64 100644 --- a/packages/ui/src/components/picker.test.ts +++ b/packages/ui/src/components/picker.test.ts @@ -52,11 +52,11 @@ describe("picker.css: trigger contract", () => { }) describe("picker.css: content contract", () => { - test("content uses --surface-base background", () => { + test("content uses --surface-raised background", () => { const contentIdx = css.indexOf("[data-picker-content] {") expect(contentIdx).toBeGreaterThan(-1) const block = css.slice(contentIdx, contentIdx + 300) - expect(block).toContain("--surface-base") + expect(block).toContain("--surface-raised") }) test("content uses --radius-md", () => { diff --git a/packages/ui/src/components/popover.css b/packages/ui/src/components/popover.css index 2e08c245f..b39bf92a4 100644 --- a/packages/ui/src/components/popover.css +++ b/packages/ui/src/components/popover.css @@ -5,7 +5,7 @@ [data-component="popover-content"] { z-index: 50; border-radius: var(--radius-md); - background-color: var(--surface-base); + background-color: var(--surface-raised); background-clip: padding-box; box-shadow: var(--ring-base), var(--shadow-floating); @@ -113,24 +113,26 @@ &:hover, &[data-force-state="hover"] { - background: var(--bg-cream); + background: var(--row-hover-overlay); } &[data-active], &[data-force-state="active"] { - background: var(--bg-cream); + background: var(--surface-interactive-base); opacity: 0.8; } &:focus-visible, &[data-force-state="focus-visible"] { outline: none; - background: var(--bg-cream); + background: var(--row-hover-overlay); } &[data-selected], &[data-force-state="selected"] { - background: var(--bg-cream); + background: var(--surface-interactive-base); + font-weight: 500; + color: var(--fg-strong); } &[data-disabled], diff --git a/packages/ui/src/components/text-field.css b/packages/ui/src/components/text-field.css index 351a1f96c..752e78a13 100644 --- a/packages/ui/src/components/text-field.css +++ b/packages/ui/src/components/text-field.css @@ -222,3 +222,23 @@ } } } + +/* Dark normal-variant wrapper lifts to raised (`#2d2a27`) so the input + * separates from thread / sidebar canvas (per DESIGN.md L353). The + * `:not([data-disabled]):not([data-readonly])` guard keeps the existing + * disabled / readonly path on `--bg-cream` (= canvas in dark, the spec + * sunken-back-to-thread reading). Specificity (0,5,0) sits above the + * `[data-disabled]` block (0,4,0); the :not() guard is what lets the + * disabled rule still take over. */ +:root[data-color-scheme="dark"] + [data-component="input"][data-variant="normal"] + [data-slot="input-wrapper"]:not([data-disabled]):not([data-readonly]) { + background: var(--surface-raised); +} +@media (prefers-color-scheme: dark) { + :root:not([data-color-scheme="light"]) + [data-component="input"][data-variant="normal"] + [data-slot="input-wrapper"]:not([data-disabled]):not([data-readonly]) { + background: var(--surface-raised); + } +} From 3e37a05b2656510b7a16bdb1473af78d41e81058 Mon Sep 17 00:00:00 2001 From: Yuhan Lei Date: Sat, 23 May 2026 08:20:48 +0800 Subject: [PATCH 3/7] fix(ui): address review feedback on popover selected and textfield guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move the dark-mode `:not([data-disabled]):not([data-readonly])` guard from [data-slot="input-wrapper"] up to the input root selector. The disabled / readonly attributes are set on the root (see Lines 123-138), not on the wrapper, so the original guard was a no-op and the dark `--surface-raised` override would have hijacked the disabled / readonly cream background. Reported by coderabbitai on PR #847. Also clean up popover-item selected styling: replace the hardcoded `font-weight: 500` with `var(--font-weight-emphasis)` to match picker.css, and drop the redundant `color: var(--fg-strong)` — the base [data-slot="popover-item"] rule already sets it (popover.css:106). Reported by gemini-code-assist on PR #847. --- packages/ui/src/components/popover.css | 3 +-- packages/ui/src/components/text-field.css | 8 ++++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/ui/src/components/popover.css b/packages/ui/src/components/popover.css index b39bf92a4..18c375ec7 100644 --- a/packages/ui/src/components/popover.css +++ b/packages/ui/src/components/popover.css @@ -131,8 +131,7 @@ &[data-selected], &[data-force-state="selected"] { background: var(--surface-interactive-base); - font-weight: 500; - color: var(--fg-strong); + font-weight: var(--font-weight-emphasis); } &[data-disabled], diff --git a/packages/ui/src/components/text-field.css b/packages/ui/src/components/text-field.css index 752e78a13..8b2660adc 100644 --- a/packages/ui/src/components/text-field.css +++ b/packages/ui/src/components/text-field.css @@ -231,14 +231,14 @@ * `[data-disabled]` block (0,4,0); the :not() guard is what lets the * disabled rule still take over. */ :root[data-color-scheme="dark"] - [data-component="input"][data-variant="normal"] - [data-slot="input-wrapper"]:not([data-disabled]):not([data-readonly]) { + [data-component="input"][data-variant="normal"]:not([data-disabled]):not([data-readonly]) + [data-slot="input-wrapper"] { background: var(--surface-raised); } @media (prefers-color-scheme: dark) { :root:not([data-color-scheme="light"]) - [data-component="input"][data-variant="normal"] - [data-slot="input-wrapper"]:not([data-disabled]):not([data-readonly]) { + [data-component="input"][data-variant="normal"]:not([data-disabled]):not([data-readonly]) + [data-slot="input-wrapper"] { background: var(--surface-raised); } } From a27d1827efe027c5b04b77dd7599c7e181604965 Mon Sep 17 00:00:00 2001 From: Yuhan Lei Date: Sat, 23 May 2026 08:47:52 +0800 Subject: [PATCH 4/7] refactor(ui): drop dark composer dock lift, fall back to bg-base The dock card already gets card identity from the 1px hairline plus the sole-sanctioned dark drop shadow (0 4px 12px rgba(0,0,0,0.3)); the extra --surface-raised fill in dark mode added a third visual cue and made the dock pop ~17 sRGB above thread canvas, more than Codex Desktop or Claude Desktop. Letting the dock fall back to --bg-base (= canvas in dark = white in light) keeps both modes symmetric and reuses the same logic. dock-card.css: remove the dark override that set background-color to --surface-raised; the dark rule now only carries the box-shadow override. Light mode is untouched (it already uses --bg-base). prompt-input.tsx: the bottom fade gradient must match the dock fill so text fading behind the toolbar reads cleanly; switch the gradient source from --surface-raised to --bg-base on both modes. DESIGN.md L189 + L417 updated to record the decision: dock no longer sits in the raised tier, and the "sole sanctioned dark drop-shadow" rule survives independent of the fill choice. --- packages/app/src/components/prompt-input.tsx | 2 +- packages/ui/src/components/dock-card.css | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index c972ff44e..be9d5d775 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -578,7 +578,7 @@ export const PromptInput: Component = (props) => { style={{ height: space, background: - "linear-gradient(to top, var(--surface-raised) calc(100% - 20px), transparent)", + "linear-gradient(to top, var(--bg-base) calc(100% - 20px), transparent)", }} /> diff --git a/packages/ui/src/components/dock-card.css b/packages/ui/src/components/dock-card.css index c5817527d..313173c77 100644 --- a/packages/ui/src/components/dock-card.css +++ b/packages/ui/src/components/dock-card.css @@ -8,7 +8,6 @@ } :root[data-color-scheme="dark"] [data-dock="card"] { - background-color: var(--surface-raised); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); } From ab7c431c8ed3237d8c59cc519599bbdcc82409eb Mon Sep 17 00:00:00 2001 From: Yuhan Lei Date: Sat, 23 May 2026 08:59:46 +0800 Subject: [PATCH 5/7] refactor(ui): use --surface-dock alias for cross-mode dock fill MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pure bg-base fallback (a27d1827ef) made the dark dock invisible — the sole-sanctioned drop shadow vanishes into canvas and the 1px hairline alone cannot carry the card boundary. Move dock to sidebar tone in dark (matches Codex Desktop) while keeping it white in light (matches Claude Desktop and Codex light); introduce `--surface-dock` so the inline gradient mask in prompt-input.tsx stays aligned with dock fill across both modes without duplicating the dark override. theme.css: - light :root: `--surface-dock: #ffffff` (= bg-base in light) - dark explicit + @media: `--surface-dock: #232120` (= sidebar in dark) pawwork.json: mirror the alias in both light and dark overrides so theme-parity stays green. dock-card.css: `[data-dock="card"]` background switches to `var(--surface-dock)`; the dark override now only carries the sole-sanctioned drop shadow, no fill duplication. prompt-input.tsx: bottom fade gradient uses `var(--surface-dock)` so the fade-to-fill matches the dock card in both modes. DESIGN.md L189 + L417 updated: dock sits at the chrome-middle tier (Codex same-track), not in raised; `--surface-dock` documented as the single source of truth for dock fill. --- packages/app/src/components/prompt-input.tsx | 2 +- packages/ui/src/components/dock-card.css | 2 +- packages/ui/src/styles/theme.css | 3 +++ packages/ui/src/theme/themes/pawwork.json | 2 ++ 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index be9d5d775..4ea91be1e 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -578,7 +578,7 @@ export const PromptInput: Component = (props) => { style={{ height: space, background: - "linear-gradient(to top, var(--bg-base) calc(100% - 20px), transparent)", + "linear-gradient(to top, var(--surface-dock) calc(100% - 20px), transparent)", }} /> diff --git a/packages/ui/src/components/dock-card.css b/packages/ui/src/components/dock-card.css index 313173c77..f3cbb3c42 100644 --- a/packages/ui/src/components/dock-card.css +++ b/packages/ui/src/components/dock-card.css @@ -1,5 +1,5 @@ [data-dock="card"] { - background-color: var(--bg-base); + background-color: var(--surface-dock); border-radius: var(--radius-lg); box-shadow: 0 0 0 1px var(--border-weak), var(--shadow-raised); overflow: hidden; diff --git a/packages/ui/src/styles/theme.css b/packages/ui/src/styles/theme.css index 758d7efab..6450a1ae9 100644 --- a/packages/ui/src/styles/theme.css +++ b/packages/ui/src/styles/theme.css @@ -88,6 +88,7 @@ --surface-sunken: #faf9f7; /* light alias → bg-cream; no third neutral tier (L9) */ --code-surface: rgba(0, 0, 0, 0.04); /* #642: alpha overlay for inline code + code-block */ --sidebar: #f2f0ed; /* chrome 独立 surface(与 cream #faf9f7 区分) */ + --surface-dock: #ffffff; /* composer/dock fill — light: white, dark: sidebar tone (see dark blocks) */ --surface-base-active: rgba(0, 0, 0, 0.051); /* ────────── Light ink · 2-tier collapse (strong + weak); base/weaker pairs merge. @@ -291,6 +292,7 @@ --surface-sunken: #1a1917; /* dark collapse → canvas (no sunken tier) */ --code-surface: rgba(255, 255, 255, 0.06); /* #642: alpha overlay for inline code + code-block */ --sidebar: #232120; /* sidebar chrome 中间一档 */ + --surface-dock: #232120; /* composer/dock 跟 sidebar 同色(chrome 中间一档) */ /* Dark ink · 2-tier collapse (fg + fg-weak); strong/base and weak/weaker pairs merge */ --fg-strong: #f4efe7; @@ -440,6 +442,7 @@ --surface-sunken: #1a1917; --code-surface: rgba(255, 255, 255, 0.06); /* #642: alpha overlay for inline code + code-block */ --sidebar: #232120; + --surface-dock: #232120; --fg-strong: #f4efe7; --fg-base: #f4efe7; diff --git a/packages/ui/src/theme/themes/pawwork.json b/packages/ui/src/theme/themes/pawwork.json index be4514bcf..ba8beb0d4 100644 --- a/packages/ui/src/theme/themes/pawwork.json +++ b/packages/ui/src/theme/themes/pawwork.json @@ -55,6 +55,7 @@ "letter-spacing-h1": "-0.16px", "letter-spacing-cjk": "0", "sidebar": "#f2f0ed", + "surface-dock": "#ffffff", "surface-interactive-base": "#fdf6f0", "surface-interactive-hover": "#fbece0", "surface-base-active": "rgba(0, 0, 0, 0.051)", @@ -138,6 +139,7 @@ "letter-spacing-h1": "-0.16px", "letter-spacing-cjk": "0", "sidebar": "#232120", + "surface-dock": "#232120", "surface-interactive-base": "rgba(255, 89, 16, 0.14)", "surface-interactive-hover": "rgba(255, 89, 16, 0.20)", "surface-base-active": "rgba(255, 255, 255, 0.059)", From 6866b9fc57f6839339bf9a70b4aa5acf95e9840a Mon Sep 17 00:00:00 2001 From: Yuhan Lei Date: Sat, 23 May 2026 11:42:31 +0800 Subject: [PATCH 6/7] refactor(ui): collapse dock to surface-raised, ease light sidebar to -9 sRGB MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop the dedicated `--surface-dock` token in favor of `--surface-raised`. Composer dock shares the same elevation role as dialog, popover, card, and textfield — all are "raised surfaces" with identical physical behavior (light: shadow on white; dark: one lightness tier above sidebar). The token was a single-use alias introduced to back an inline TSX gradient; that gradient now reads `--surface-raised` directly, removing a mode-dependent indirection. In dark, this lifts the dock from `#232120` (same as sidebar) to `#2D2A27` (raised). The previous two-tier dock-as-sidebar layout matched classic IDEs (VS Code, Cursor) but pulled the composer out of the chat-app convention where ChatGPT, Claude, Perplexity, and Gemini all lift the composer one tier above the thread. Three tiers now read as thread #1A1917 → sidebar #232120 → raised dock #2D2A27, uniform +9 sRGB steps. In light, sidebar moves from `#F2F0ED` (-15 sRGB vs thread) to `#F8F6F3` (-9 sRGB). The original tone sat at the heavy end of modern productivity apps (alongside Gemini at -15); the new value lands at the median alongside Notion (-9), Copilot, and Figma (-10). The -9 step mirrors the dark mode's +9 between every tier, giving the surface system uniform elevation steps across both modes. Composer dock in light remains visually identical (raised = #FFFFFF = thread) — shadow continues to express elevation per the asymmetric light/dark elevation convention. Verification: theme-parity 200 tests pass; bun snap app-shell + sidebar visually confirm both surfaces. Source rationale lives in docs/research/2026-05-23-dark-mode-composer-dock-layering.md and docs/research/2026-05-23-light-mode-sidebar-contrast.md (local-only). --- packages/app/src/components/prompt-input.tsx | 2 +- packages/ui/src/components/dock-card.css | 2 +- packages/ui/src/styles/theme.css | 10 ++++------ packages/ui/src/theme/themes/pawwork.json | 4 +--- 4 files changed, 7 insertions(+), 11 deletions(-) diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 4ea91be1e..c972ff44e 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -578,7 +578,7 @@ export const PromptInput: Component = (props) => { style={{ height: space, background: - "linear-gradient(to top, var(--surface-dock) calc(100% - 20px), transparent)", + "linear-gradient(to top, var(--surface-raised) calc(100% - 20px), transparent)", }} /> diff --git a/packages/ui/src/components/dock-card.css b/packages/ui/src/components/dock-card.css index f3cbb3c42..1aeb9f2a6 100644 --- a/packages/ui/src/components/dock-card.css +++ b/packages/ui/src/components/dock-card.css @@ -1,5 +1,5 @@ [data-dock="card"] { - background-color: var(--surface-dock); + background-color: var(--surface-raised); border-radius: var(--radius-lg); box-shadow: 0 0 0 1px var(--border-weak), var(--shadow-raised); overflow: hidden; diff --git a/packages/ui/src/styles/theme.css b/packages/ui/src/styles/theme.css index 6450a1ae9..6e6393bf3 100644 --- a/packages/ui/src/styles/theme.css +++ b/packages/ui/src/styles/theme.css @@ -87,8 +87,7 @@ --surface-raised: #ffffff; --surface-sunken: #faf9f7; /* light alias → bg-cream; no third neutral tier (L9) */ --code-surface: rgba(0, 0, 0, 0.04); /* #642: alpha overlay for inline code + code-block */ - --sidebar: #f2f0ed; /* chrome 独立 surface(与 cream #faf9f7 区分) */ - --surface-dock: #ffffff; /* composer/dock fill — light: white, dark: sidebar tone (see dark blocks) */ + --sidebar: #f8f6f3; /* chrome 独立 surface(暖灰,-9 sRGB 比 thread 暗) */ --surface-base-active: rgba(0, 0, 0, 0.051); /* ────────── Light ink · 2-tier collapse (strong + weak); base/weaker pairs merge. @@ -282,8 +281,9 @@ /* Dark surface · 3-tier (canvas + sidebar + raised), R-B=3 warm coffee. * The legacy 6-tier ladder collapses into 3 visible tones: * canvas #1a1917 — chrome floor: bg / sunken alias / cream alias / surface-base alias - * sidebar #232120 — chrome middle: 左栏独立 surface (2026-05-23 从 canvas 拆出) - * raised #2d2a27 — anchored: dialog / composer / msg-user / list-hover / popover / textfield / picker / cp body + * sidebar #232120 — chrome middle: 左栏独立 surface (+9 sRGB 比 canvas 亮) + * raised #2d2a27 — anchored: dialog / composer dock / msg-user / list-hover / popover / textfield / picker (+9 sRGB 比 sidebar 亮) + * 三档步长均匀 +9 sRGB,跟浅色 sidebar -9 形成对称。 * Token names survive for light-mode parity and callsite stability. */ --bg-base: #1a1917; --bg-cream: #1a1917; /* dark collapse → canvas (no separate cream tier) */ @@ -292,7 +292,6 @@ --surface-sunken: #1a1917; /* dark collapse → canvas (no sunken tier) */ --code-surface: rgba(255, 255, 255, 0.06); /* #642: alpha overlay for inline code + code-block */ --sidebar: #232120; /* sidebar chrome 中间一档 */ - --surface-dock: #232120; /* composer/dock 跟 sidebar 同色(chrome 中间一档) */ /* Dark ink · 2-tier collapse (fg + fg-weak); strong/base and weak/weaker pairs merge */ --fg-strong: #f4efe7; @@ -442,7 +441,6 @@ --surface-sunken: #1a1917; --code-surface: rgba(255, 255, 255, 0.06); /* #642: alpha overlay for inline code + code-block */ --sidebar: #232120; - --surface-dock: #232120; --fg-strong: #f4efe7; --fg-base: #f4efe7; diff --git a/packages/ui/src/theme/themes/pawwork.json b/packages/ui/src/theme/themes/pawwork.json index ba8beb0d4..31fad10a3 100644 --- a/packages/ui/src/theme/themes/pawwork.json +++ b/packages/ui/src/theme/themes/pawwork.json @@ -54,8 +54,7 @@ "letter-spacing-display": "-0.32px", "letter-spacing-h1": "-0.16px", "letter-spacing-cjk": "0", - "sidebar": "#f2f0ed", - "surface-dock": "#ffffff", + "sidebar": "#f8f6f3", "surface-interactive-base": "#fdf6f0", "surface-interactive-hover": "#fbece0", "surface-base-active": "rgba(0, 0, 0, 0.051)", @@ -139,7 +138,6 @@ "letter-spacing-h1": "-0.16px", "letter-spacing-cjk": "0", "sidebar": "#232120", - "surface-dock": "#232120", "surface-interactive-base": "rgba(255, 89, 16, 0.14)", "surface-interactive-hover": "rgba(255, 89, 16, 0.20)", "surface-base-active": "rgba(255, 255, 255, 0.059)", From 9c6f7168d88a793b01a8f72feb09b28f7c14f9bd Mon Sep 17 00:00:00 2001 From: Yuhan Lei Date: Sat, 23 May 2026 13:53:29 +0800 Subject: [PATCH 7/7] fix(ui): align popover stories with raised content surface Update popover.stories.tsx MenuMatrix (light + dark wrappers) and WithSearch wrapper to use --surface-raised, matching the actual content background in popover.css. The hand-crafted story wrappers previously used --surface-base, which drifts in dark mode (canvas #1a1917 vs raised #2d2a27). Visual regressions on popover hover / active / selected states would have validated the wrong background tier. --- packages/ui/src/components/popover.stories.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ui/src/components/popover.stories.tsx b/packages/ui/src/components/popover.stories.tsx index fab1fbb91..e62623afd 100644 --- a/packages/ui/src/components/popover.stories.tsx +++ b/packages/ui/src/components/popover.stories.tsx @@ -63,7 +63,7 @@ export const MenuMatrix = { {/* Light */}
(