Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .impeccable/design.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
10 changes: 6 additions & 4 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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

Expand Down
54 changes: 45 additions & 9 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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,
Expand All @@ -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;
Expand Down Expand Up @@ -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 {
Expand All @@ -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;
Expand All @@ -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;
Expand Down
42 changes: 42 additions & 0 deletions tests/e2e/viewer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,48 @@ 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 surfaceLuminance = async (selector: string) =>
page.locator(selector).evaluate((element) => {
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 channel = (value: number) => {
const normalized = value / 255;
return normalized <= 0.03928 ? normalized / 12.92 : ((normalized + 0.055) / 1.055) ** 2.4;
};

return Math.max(
...colors.map(
({ red, green, blue }) => 0.2126 * channel(red) + 0.7152 * channel(green) + 0.0722 * channel(blue),
),
);
});

expect(await surfaceLuminance(".shell-mark")).toBeLessThan(0.35);
expect(await surfaceLuminance(".carbon-output")).toBeLessThan(0.35);
});

test("download action emits a file", async ({ page }) => {
await goToHash(page, getFragmentHash("Viewer bootstrap"));
await waitForViewerState(page, "artifact");
Expand Down
Loading