From 203ef2b25744b942df0e87e5f5059e9f57a6c5e4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 17 Sep 2026 05:01:40 +0000 Subject: [PATCH 1/2] Keep dark mode surfaces on the chassis The generated-link carbon sheet and header identity mark stayed cream after the dark bench rebuild. Dark theme now uses a carbon sheet and charcoal mark, and a Playwright check guards those backgrounds. Co-authored-by: aanishbhirud --- .impeccable/design.json | 2 +- DESIGN.md | 10 +++++--- src/app/globals.css | 54 +++++++++++++++++++++++++++++++++------- tests/e2e/viewer.spec.ts | 28 +++++++++++++++++++++ 4 files changed, 80 insertions(+), 14 deletions(-) diff --git a/.impeccable/design.json b/.impeccable/design.json index bf72b5f..4807c07 100644 --- a/.impeccable/design.json +++ b/.impeccable/design.json @@ -199,7 +199,7 @@ "carbon-paper-dark": { "role": "neutral", "displayName": "Dark-Theme Carbon Transfer Stock", - "canonical": "#d2c8b3", + "canonical": "#221d18", "tonalRamp": ["oklch(15% 0.035 78)", "oklch(27% 0.035 78)", "oklch(39% 0.035 78)", "oklch(51% 0.035 78)", "oklch(63% 0.035 78)", "oklch(75% 0.035 78)", "oklch(87% 0.035 78)", "oklch(95% 0.035 78)"] }, "carbon-ink": { diff --git a/DESIGN.md b/DESIGN.md index 909466e..d8eef29 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -37,9 +37,11 @@ colors: paper-body: "#4d453a" paper-body-dark: "#c9c0b0" carbon-paper: "#d9d0bd" - carbon-paper-dark: "#d2c8b3" + carbon-paper-dark: "#221d18" carbon-ink: "#41392f" + carbon-ink-dark: "#e6dccb" carbon-line: "#776d5e" + carbon-line-dark: "#6a5f51" renderer: "#1c1915" renderer-raised: "#27221c" renderer-text: "#e1d9ca" @@ -182,11 +184,11 @@ The palette is warm, low-gloss, and material-led: dark mode runs charcoal keys o ### Neutral -- **Aged Bone:** Paper-adjacent surfaces, the identity mark, and the light-mode control faces; the charcoal key face stays the dark-mode control material. +- **Aged Bone:** Paper-adjacent surfaces and the light-mode control faces; the identity mark and charcoal key face stay the dark-mode control material. - **Instrument Controls:** Raised keys keep the same physical grammar in both themes: a lifted face, a hard lower foot, and a darker pressed well. Dark mode uses a #26221d charcoal face with ivory labels; light mode uses a bone face with ink labels. A one-pixel burnt-orange edge marks persistent selection without turning the whole key into an accent. - **Instrument Chassis:** The structural shell for procedures, toolbars, panels, and diagnostics. Charcoal in dark mode, warm bone in light mode. - **Instrument Paper:** Rendered markdown uses warm paper in light mode and a charcoal document field with ivory text in dark mode. -- **Carbon Transfer Stock:** A distinct generated-output material with its own ink and perforation lines. +- **Carbon Transfer Stock:** A distinct generated-output material with its own ink and perforation lines. Light mode uses cream stock with dark typewriter ink; dark mode uses a dark carbon sheet with ivory ink so the result stays on-theme. - **Renderer Surface:** Code, diff, CSV, JSON, raw source, and technical preview surfaces follow the shell theme: paper stock with ink text in light mode, charcoal with ivory text in dark mode. **The Three-Signal Rule.** Orange commits, mint confirms, and brick alerts; never interchange these roles or add a competing accent. @@ -268,7 +270,7 @@ Components should feel manufactured, compact, and legible under pressure. Their ### Navigation -The sticky shell header uses themed key controls on the chassis (charcoal keys in dark mode, bone keys in light mode). Artifact navigation uses horizontally scrollable raised switchers with a clearly depressed active item and one burnt-orange inset edge. On narrow screens, hide only secondary global navigation and preserve task controls. +The sticky shell header uses themed key controls on the chassis (charcoal keys in dark mode, bone keys in light mode), including the identity mark. Artifact navigation uses horizontally scrollable raised switchers with a clearly depressed active item and one burnt-orange inset edge. On narrow screens, hide only secondary global navigation and preserve task controls. ### Procedure Steps diff --git a/src/app/globals.css b/src/app/globals.css index 38bb5fc..b8857ea 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -52,6 +52,11 @@ --carbon-paper: #d9d0bd; --carbon-ink: #41392f; --carbon-line: #776d5e; + --carbon-field-bg: rgba(255, 255, 255, 0.13); + --carbon-field-well: rgba(40, 34, 27, 0.07); + --carbon-field-border: rgba(65, 57, 47, 0.42); + --carbon-field-inset: inset 0 2px 4px rgba(65, 57, 47, 0.15); + --carbon-stamp: var(--confirmation-edge); --surface-code: #f6f1e4; --surface-code-raised: #e9e2cf; --surface-code-text: #2f2a20; @@ -146,9 +151,14 @@ --paper-table-head: #2b2620; --paper-cell-ink: #d7cebe; --paper-link-line: #cf7a1c; - --carbon-paper: #d2c8b3; - --carbon-ink: #3f372d; - --carbon-line: #756b5b; + --carbon-paper: #221d18; + --carbon-ink: #e6dccb; + --carbon-line: #6a5f51; + --carbon-field-bg: rgba(0, 0, 0, 0.28); + --carbon-field-well: rgba(0, 0, 0, 0.36); + --carbon-field-border: rgba(230, 220, 203, 0.16); + --carbon-field-inset: inset 0 2px 5px rgba(0, 0, 0, 0.45); + --carbon-stamp: var(--confirmation); --surface: var(--chassis); --surface-strong: var(--chassis-raised); --surface-muted: var(--chassis-deep); @@ -304,6 +314,16 @@ button:disabled { box-shadow: inset 0 1px 2px rgba(20, 17, 13, 0.22); } +.dark .shell-mark { + border-color: var(--control-edge); + background: linear-gradient(180deg, var(--control-face-lift), var(--control-face)); + box-shadow: inset 0 1px 0 rgba(237, 231, 218, 0.09), 0 2px 0 var(--control-foot); +} + +.dark .shell-home-link:active .shell-mark { + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.45); +} + .shell-mark-image { display: block; width: 17px; @@ -820,8 +840,8 @@ button:disabled { justify-content: center; padding: 6px 9px; transform: rotate(-4deg); - border: 2px solid var(--confirmation-edge); - color: var(--confirmation-edge); + border: 2px solid var(--carbon-stamp); + color: var(--carbon-stamp); font-family: var(--font-mono), monospace; font-size: 11px; font-weight: 760; @@ -841,7 +861,7 @@ button:disabled { min-width: 0; padding: 8px; border: 1px solid var(--carbon-line); - background: rgba(255, 255, 255, 0.13); + background: var(--carbon-field-bg); } .carbon-output .metric-label, @@ -855,11 +875,11 @@ button:disabled { margin: 0; padding: 7px; resize: none; - border: 1px solid rgba(65, 57, 47, 0.42); + border: 1px solid var(--carbon-field-border); border-radius: 0; outline: none; - background: rgba(40, 34, 27, 0.07); - box-shadow: inset 0 2px 4px rgba(65, 57, 47, 0.15); + background: var(--carbon-field-well); + box-shadow: var(--carbon-field-inset); color: var(--carbon-ink); font-family: var(--font-mono), monospace; font-size: 10px; @@ -983,6 +1003,13 @@ button:disabled { box-shadow: inset 0 2px 4px rgba(20, 17, 13, 0.22); } +.dark .sample-link:active, +.dark .sample-link.is-active { + background: var(--control-face-pressed); + color: var(--control-text); + box-shadow: inset 3px 0 0 var(--control-accent), inset 0 2px 4px rgba(0, 0, 0, 0.45); +} + .sample-link-index, .sample-link-length, .sample-link-meta { @@ -1002,6 +1029,11 @@ button:disabled { color: #5d5549; } +.dark .sample-link.is-active .sample-link-index, +.dark .sample-link.is-active .sample-link-meta { + color: var(--control-text-muted); +} + .sample-link-copy { display: grid; min-width: 0; @@ -1028,6 +1060,10 @@ button:disabled { color: #40392f; } +.dark .sample-link.is-active .sample-link-length { + color: var(--control-text-muted); +} + .home-operating-limits, .fault-placard { overflow: hidden; diff --git a/tests/e2e/viewer.spec.ts b/tests/e2e/viewer.spec.ts index 4a80785..0aa72ab 100644 --- a/tests/e2e/viewer.spec.ts +++ b/tests/e2e/viewer.spec.ts @@ -545,6 +545,34 @@ test("theme switching works", async ({ page }) => { await expect(page.locator("html")).toHaveClass(/dark/); }); +test("dark mode keeps generated output and the identity mark on the chassis", async ({ page }) => { + await waitForViewerState(page, "empty"); + await page.getByRole("button", { name: /Switch to dark theme/i }).click(); + await expect(page.locator("html")).toHaveClass(/dark/); + + await page.getByRole("button", { name: /generate link/i }).click(); + await expect(page.locator(".carbon-output")).toBeVisible(); + + const relativeLuminance = async (selector: string) => + page.locator(selector).evaluate((element) => { + const match = getComputedStyle(element).backgroundColor.match(/rgba?\(([^)]+)\)/); + if (!match) { + return 1; + } + + const [red, green, blue] = match[1].split(",").map((part) => Number(part.trim())); + const channel = (value: number) => { + const normalized = value / 255; + return normalized <= 0.03928 ? normalized / 12.92 : ((normalized + 0.055) / 1.055) ** 2.4; + }; + + return 0.2126 * channel(red) + 0.7152 * channel(green) + 0.0722 * channel(blue); + }); + + expect(await relativeLuminance(".shell-mark")).toBeLessThan(0.35); + expect(await relativeLuminance(".carbon-output")).toBeLessThan(0.35); +}); + test("download action emits a file", async ({ page }) => { await goToHash(page, getFragmentHash("Viewer bootstrap")); await waitForViewerState(page, "artifact"); From 7da4ef798cd73a64fad79eebef4e70550e31a11d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 23:01:18 +0000 Subject: [PATCH 2/2] Make the dark-mode mark check read the gradient The identity-mark assertion only sampled backgroundColor, which is transparent when the dark mark uses a linear-gradient. The check now uses the brightest opaque stop from the painted surface. Co-authored-by: aanishbhirud --- tests/e2e/viewer.spec.ts | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/tests/e2e/viewer.spec.ts b/tests/e2e/viewer.spec.ts index 0aa72ab..6d206a8 100644 --- a/tests/e2e/viewer.spec.ts +++ b/tests/e2e/viewer.spec.ts @@ -553,24 +553,38 @@ test("dark mode keeps generated output and the identity mark on the chassis", as await page.getByRole("button", { name: /generate link/i }).click(); await expect(page.locator(".carbon-output")).toBeVisible(); - const relativeLuminance = async (selector: string) => + const surfaceLuminance = async (selector: string) => page.locator(selector).evaluate((element) => { - const match = getComputedStyle(element).backgroundColor.match(/rgba?\(([^)]+)\)/); - if (!match) { + const parseRgb = (value: string) => + [...value.matchAll(/rgba?\(([^)]+)\)/g)].flatMap((match) => { + const [red, green, blue, alpha = 1] = match[1].split(",").map((part) => Number(part.trim())); + if (![red, green, blue].every(Number.isFinite) || alpha === 0) { + return []; + } + + return [{ red, green, blue }]; + }); + + const style = getComputedStyle(element); + const colors = [...parseRgb(style.backgroundColor), ...parseRgb(style.backgroundImage)]; + if (colors.length === 0) { return 1; } - const [red, green, blue] = match[1].split(",").map((part) => Number(part.trim())); const channel = (value: number) => { const normalized = value / 255; return normalized <= 0.03928 ? normalized / 12.92 : ((normalized + 0.055) / 1.055) ** 2.4; }; - return 0.2126 * channel(red) + 0.7152 * channel(green) + 0.0722 * channel(blue); + return Math.max( + ...colors.map( + ({ red, green, blue }) => 0.2126 * channel(red) + 0.7152 * channel(green) + 0.0722 * channel(blue), + ), + ); }); - expect(await relativeLuminance(".shell-mark")).toBeLessThan(0.35); - expect(await relativeLuminance(".carbon-output")).toBeLessThan(0.35); + expect(await surfaceLuminance(".shell-mark")).toBeLessThan(0.35); + expect(await surfaceLuminance(".carbon-output")).toBeLessThan(0.35); }); test("download action emits a file", async ({ page }) => {