From 59e7d5a6fa3b3d2e2044914d08062408744a2c69 Mon Sep 17 00:00:00 2001 From: jackwener Date: Sat, 8 Aug 2026 19:46:35 +0800 Subject: [PATCH 1/9] feat(theme): give the surface ladder its missing rungs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Visual system 2.0, T1 subsystem 1. Token layer only; no product CSS moves and no old name changes what it resolves to. The audit read "seven names all resolve to one white" as an accident. Most of it was not. The owner's 2026-06-20 decision already put a gray plate behind pure-white content, and that is this model — `--background` was always the CARD, never the page. Two rungs were genuinely missing: - nothing below the canvas, so a sidebar had no rung to recede to; - `--background-elevated` pinned to `--background`, leaving overlays with no surface of their own. So the ladder is named, not rebuilt: sunken / base / raised / overlay, with height mapping monotonically to lightness and the READING surface always on this mode's brightest rung — pure white in light, lightest gray in dark. Measured by painting each rung and reading the pixel back, light is 237/247/255/255 and dark is 9/17/23/27. Light has no room above white, so overlay carries the same fill as raised and is told apart by the overlay recipe; unpinning it is what gives dark a rung to climb and the recipe something to hang on. Every rung derives from `--background`, because palettes override only that token and a hardcoded plate survived every palette switch once already. That makes the offsets CUMULATIVE: base is already -0.025, so a sunken rung written as -0.030 lands 0.005 under base — a step nobody can see. It is -0.065. `--surface-sunken` has no consumer until the sidebar recedes to it in T2, and is registered as a reserved scale rung rather than worked around: an ordered scale reads as a scale only with its bottom rung present. --- apps/desktop/src/renderer/maka-tokens.css | 39 ++++++++++++++++++++--- scripts/check-dead-css.mjs | 5 +++ 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/apps/desktop/src/renderer/maka-tokens.css b/apps/desktop/src/renderer/maka-tokens.css index 0c9446d3d2..2554419233 100644 --- a/apps/desktop/src/renderer/maka-tokens.css +++ b/apps/desktop/src/renderer/maka-tokens.css @@ -69,7 +69,31 @@ A wash wide enough to be the whole boundary reads as a gray slab pasted next to the content rather than a second panel, which is the note PR-UI-FRAME-4 left here and the reason this lands at 0.025 and not higher. */ - --surface-canvas: oklch(from var(--background) calc(l - 0.025) c h); + --surface-canvas: var(--surface-base); + + /* -- Surface ladder (visual system 2.0, T1) -------------------------- + Four rungs; height maps monotonically to lightness, and the READING + surface always occupies this mode's brightest rung: pure white in light, + the lightest gray in dark. + + The middle of this ladder is not new. The owner's 2026-06-20 decision + above already put a gray plate behind pure-white content, and that IS + this model -- `--background` was always the CARD, never the page. What + was missing were the two ends: nothing below the canvas for a sidebar to + recede to, and no surface of its own for overlays, which had been pinned + to the card white. + + The semantic names are canonical; the older names around them are + aliases that keep resolving to exactly what they resolved to before. + Nothing is forced to migrate here -- see DESIGN.md, Surface ladder. */ + --surface-sunken: oklch(from var(--background) calc(l - 0.055) c h); + --surface-base: oklch(from var(--background) calc(l - 0.025) c h); + --surface-raised: var(--background); + /* Light has no room above white, so overlay carries the same FILL as raised + and is told apart by the overlay recipe (border-soft + elevation). + Unpinning it from `--background` is what gives dark a rung to climb and + the recipe something to hang on: a semantic split, not a colour change. */ + --surface-overlay: var(--surface-raised); /* PR-GRAY-CARD-LIFT-3 (WAWQAQ msg `053ca971`): in the new gray-plate / white-card hierarchy, "elevated" cards live on the same pure white as base content cards — the lift comes from the @@ -77,7 +101,7 @@ `--background-elevated` mixed 1.5% foreground into background, which read as a faint gray on the new shell. Pinning it to the same `--background` keeps card surfaces consistently white. */ - --background-elevated: var(--background); + --background-elevated: var(--surface-overlay); /* PR-UI-ALIGN-0: warm near-black (#141414-like) at a faint warm hue, so ALL derived grays (#636261/#838280-like) pick up the reference's warm cast instead of reading as cold neutral. */ @@ -755,8 +779,15 @@ the shell (Astryx painted its own surface over the frame plate until this branch handed the columns their materials); against the product's #171719 content it rendered a near-black #09090b rail. */ - --surface-canvas: oklch(from var(--background) calc(l - 0.025) c h); - --background-elevated: color-mix(in srgb, var(--foreground) 2%, var(--background)); + --surface-canvas: var(--surface-base); + /* Dark inverts the direction, not the rule: higher is still lighter and the + reading surface still takes the brightest rung. Unlike light there IS + room above raised, so overlay climbs a real step. */ + --surface-sunken: oklch(from var(--background) calc(l - 0.065) c h); + --surface-base: oklch(from var(--background) calc(l - 0.025) c h); + --surface-raised: var(--background); + --surface-overlay: color-mix(in srgb, var(--foreground) 2%, var(--background)); + --background-elevated: var(--surface-overlay); --foreground: oklch(0.92 0.004 286); --accent: oklch(0.74 0.15 250); diff --git a/scripts/check-dead-css.mjs b/scripts/check-dead-css.mjs index 8ff03bb347..78cd3e828a 100755 --- a/scripts/check-dead-css.mjs +++ b/scripts/check-dead-css.mjs @@ -193,6 +193,11 @@ const RESERVED_SCALE_TOKENS = new Set([ '--z-dropdown', '--z-tooltip', '--z-modal', + // Surface ladder sunken/base/raised/overlay (visual system 2.0 T1). The + // sunken rung's consumer is the sidebar, which recedes to it in T2; the + // ladder only reads as a ladder with its bottom rung present, and defining + // the scale is precisely what T1 is for. + '--surface-sunken', // Control-height scale, 20/24/28/32/36/40 on the 4px ruler. '--h-control-xl', '--h-control-2xl', From cc3dbfd3fe387e1da4419c396320959bf23039d0 Mon Sep 17 00:00:00 2001 From: jackwener Date: Sat, 8 Aug 2026 21:12:02 +0800 Subject: [PATCH 2/9] feat(theme): put the ink ladder on an even rhythm above AA MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Visual system 2.0, T1 subsystem 2. Token layer only; the 59 call sites of --muted-foreground are untouched and migrate in T2-T4. Measured against --surface-raised, the audit's diagnosis held for one tier of three. Primary was said to be short of its 15:1 target and was already 19.1:1. Secondary was to drop to ~5:1 to open a 3x gap from primary — but that gap is copied from a reference with only TWO text tiers, and three tiers standing on a 4.5:1 AA floor cannot span 3x without the bottom two colliding. The rhythm is even instead, roughly geometric, which is what three tiers can hold: light 19.14 / 9.84 / 4.84 dark 15.46 / 9.15 / 4.67 The real defect was muted at 50% ink: 4.21:1 in light and 3.89:1 in dark — already under AA in the mode nobody measured. It is 54%. Dark primary needed 0.92 -> 0.95 to clear 15:1, and dark carries its own secondary mix because one percentage cannot land both modes in the 8.5-10:1 band: 74% reads 9.8 in light but 8.2 in dark. --foreground-dimmed becomes an alias. It was written as "80% ink" exactly like --foreground-secondary but mixed in srgb where that one mixes in oklch, so the same words resolved to different colours (10.51:1 vs 12.01:1). The contract test pins the invariant on the source text rather than a rendering, because what must not come back is dimmed having its own definition to drift from. --- .../__tests__/ink-ladder-contract.test.ts | 50 +++++++++++++++++++ apps/desktop/src/renderer/maka-tokens.css | 26 ++++++++-- 2 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 apps/desktop/src/main/__tests__/ink-ladder-contract.test.ts diff --git a/apps/desktop/src/main/__tests__/ink-ladder-contract.test.ts b/apps/desktop/src/main/__tests__/ink-ladder-contract.test.ts new file mode 100644 index 0000000000..dbea1fbce6 --- /dev/null +++ b/apps/desktop/src/main/__tests__/ink-ladder-contract.test.ts @@ -0,0 +1,50 @@ +import { strict as assert } from 'node:assert'; +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import { describe, it } from 'node:test'; +import { REPO_ROOT } from './css-test-helpers.js'; + +/** + * The ink ladder's aliasing invariant (visual system 2.0, T1). + * + * `--foreground-dimmed` and `--foreground-secondary` were both written as + * "80% ink", but one mixed in srgb and the other in oklch, so the same words + * produced two different colours — 10.51:1 against 12.01:1, measured. That is + * the failure this pins: not a wrong value, but a SECOND definition of one + * tier that drifted because nothing said the two had to agree. + * + * Asserted on the source text rather than a rendered page because the point is + * that dimmed has no independent definition to drift from. A screenshot would + * only prove they happen to match today. + */ +const TOKENS_PATH = join(REPO_ROOT, 'apps', 'desktop', 'src', 'renderer', 'maka-tokens.css'); + +describe('ink ladder', () => { + it('keeps --foreground-dimmed an alias, never its own mix', async () => { + const css = await readFile(TOKENS_PATH, 'utf8'); + const declarations = [...css.matchAll(/^\s*--foreground-dimmed:\s*([^;]+);/gm)].map( + (match) => match[1].trim(), + ); + + assert.ok(declarations.length > 0, '--foreground-dimmed must still be defined'); + for (const value of declarations) { + assert.equal( + value, + 'var(--foreground-secondary)', + 'dimmed is the secondary tier under another name; giving it its own color-mix is how the two drifted apart before', + ); + } + }); + + it('derives every ink tier in one colour space', async () => { + const css = await readFile(TOKENS_PATH, 'utf8'); + // A tier mixed in srgb while its siblings mix in oklch is the exact defect + // above, and it is invisible in review because the percentages match. + const srgbInk = [...css.matchAll(/^\s*(--[a-z-]*foreground[a-z-]*):\s*color-mix\(in srgb[^;]*;/gm)]; + assert.deepEqual( + srgbInk.map((match) => match[1]), + [], + 'ink tiers derive in oklch; an srgb mix gives the same words a different colour', + ); + }); +}); diff --git a/apps/desktop/src/renderer/maka-tokens.css b/apps/desktop/src/renderer/maka-tokens.css index 2554419233..c7835f44b4 100644 --- a/apps/desktop/src/renderer/maka-tokens.css +++ b/apps/desktop/src/renderer/maka-tokens.css @@ -111,7 +111,11 @@ built on. zinc-family hue 286 at the same chroma reads clean and cool like the reference sidebar. */ --foreground: oklch(0.17 0.005 286); - --foreground-dimmed: color-mix(in srgb, var(--foreground) 80%, var(--background)); + /* Alias, not a tier. It was written as "80% ink" like --foreground-secondary + but mixed in srgb where that one mixes in oklch, so the same words gave + two different colours (10.51:1 vs 12.01:1 measured). One family, one + colour space: derive every ink in oklch. */ + --foreground-dimmed: var(--foreground-secondary); /* Brand accent = logo blue (oklch L0.70 C0.135 h250), sampled from apps/desktop/assets/icon.png (owner decision 2026-07-03). --success stays @@ -234,8 +238,18 @@ --border-width-thick: 2px; --border-width-accent: 3px; --muted: oklch(from var(--foreground) l c h / 0.05); - --muted-foreground: color-mix(in oklch, var(--foreground) 50%, var(--background)); - --foreground-secondary: color-mix(in oklch, var(--foreground) 80%, var(--background)); + /* Ink ladder (visual system 2.0, T1): three tiers about 2x apart, all above + 4.5:1. Measured against --surface-raised, light reads 19.1 / 9.8 / 4.8. + + The audit asked for a 3x gap, copied from a reference that reaches 3.6x + with only TWO text tiers. Three tiers standing on a 4.5:1 AA floor cannot + span 3x without the bottom two colliding, so the rhythm is even instead: + roughly geometric, which is what three tiers can actually hold. + + 50% muted was the one real defect — 4.21:1 in light and 3.89:1 in dark, + already under AA in the mode nobody checked. */ + --muted-foreground: color-mix(in oklch, var(--foreground) 54%, var(--background)); + --foreground-secondary: color-mix(in oklch, var(--foreground) 74%, var(--background)); --state-hover-bg: oklch(from var(--foreground) l c h / 0.04); --state-selected-bg: oklch(from var(--foreground) l c h / 0.065); /* PR5 FOREGROUND-ALPHA-TOKEN-0: raw foreground-alpha tiers for component @@ -788,7 +802,11 @@ --surface-raised: var(--background); --surface-overlay: color-mix(in srgb, var(--foreground) 2%, var(--background)); --background-elevated: var(--surface-overlay); - --foreground: oklch(0.92 0.004 286); + /* 0.92 measured 14.11:1, just under the 15:1 the reading tier asks for. */ + --foreground: oklch(0.95 0.004 286); + /* Dark's ink-to-surface gap differs, so one percentage cannot put secondary + in the 8.5-10:1 band for both modes: 74% gives 9.8 light but 8.2 dark. */ + --foreground-secondary: color-mix(in oklch, var(--foreground) 78%, var(--background)); --accent: oklch(0.74 0.15 250); /* Dark mode keeps the same pale-blue CTA chip + deep-blue text as light From 93e73deebfd5326a37c1b709568ab3b9e66782df Mon Sep 17 00:00:00 2001 From: jackwener Date: Sat, 8 Aug 2026 21:58:13 +0800 Subject: [PATCH 3/9] feat(theme): name the quiet border tier MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Visual system 2.0, T1 subsystem 3. Token layer only. Four border strengths were in play: 5% on the sidebar rail (declared locally in sidebar.css), 8% on --card-border-color, 10% on --border, 16% on --border-strong. Two of those are tiers and two are drift. `--border` is the structural line — the edge of a panel, a card, a table. `--border-soft` is the quiet one for seams inside a surface, and it is what the 5% rail and the 8% card edge converge on. Their call sites move in T2-T4; T1 does not touch product CSS. Registered as a reserved scale rung for the same reason as --surface-sunken: the tier exists before its consumers do, which is what defining a scale means. --- apps/desktop/src/renderer/maka-tokens.css | 16 +++++++++++++++- scripts/check-dead-css.mjs | 4 ++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/renderer/maka-tokens.css b/apps/desktop/src/renderer/maka-tokens.css index c7835f44b4..c576cdad4a 100644 --- a/apps/desktop/src/renderer/maka-tokens.css +++ b/apps/desktop/src/renderer/maka-tokens.css @@ -222,7 +222,21 @@ /* PR-UI-ALIGN-0: visible hairline borders (≈#e6e6e6) like the reference's card/container outlines — maka's 5% was nearly invisible, which flattened the floating-card hierarchy. */ ---border: oklch(from var(--foreground) l c h / 0.10); + /* Border tiers (visual system 2.0, T1). `--border` is the STRUCTURAL line: + the edge of a panel, a card, a table. `--border-soft` is the quiet one for + seams inside a surface — rails, row separators, the outline that only has + to hint at a boundary the fill has already made. + + Four strengths were in play before this: 5% on the sidebar rail (declared + locally in sidebar.css), 8% on --card-border-color, 10% here, and 16% on + --border-strong. Two of those are tiers and two are drift. The soft tier + is what the 5% rail and the 8% card edge converge on; their call sites + move in T2-T4, since T1 does not touch product CSS. + + Pairs with the DESIGN.md rule that one boundary picks ONE mechanism — + fill step, line, or shadow, never all three stacked. */ + --border: oklch(from var(--foreground) l c h / 0.10); + --border-soft: oklch(from var(--foreground) l c h / 0.06); --border-strong: oklch(from var(--foreground) l c h / 0.16); /* === border stroke width (#520 PR4 item 14) ============================ Border color was already tokenized; the width was bare px in every diff --git a/scripts/check-dead-css.mjs b/scripts/check-dead-css.mjs index 78cd3e828a..9469f7ca72 100755 --- a/scripts/check-dead-css.mjs +++ b/scripts/check-dead-css.mjs @@ -198,6 +198,10 @@ const RESERVED_SCALE_TOKENS = new Set([ // ladder only reads as a ladder with its bottom rung present, and defining // the scale is precisely what T1 is for. '--surface-sunken', + // Border strength tiers soft/structural/strong (visual system 2.0 T1). The + // soft tier's consumers are the sidebar rail and the card edge, which + // converge on it in T2-T4; T1 defines the scale without touching product CSS. + '--border-soft', // Control-height scale, 20/24/28/32/36/40 on the 4px ruler. '--h-control-xl', '--h-control-2xl', From 4571a807beb0b31f9fca475c7455c1d8acbbaca6 Mon Sep 17 00:00:00 2001 From: jackwener Date: Sat, 8 Aug 2026 22:25:28 +0800 Subject: [PATCH 4/9] feat(theme): give the elevation scale product names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Visual system 2.0, T1 subsystem 4. Token layer only. The generated theme ships --shadow-low/med/high and product CSS had used them exactly once, reaching for hand-rolled shadows everywhere else. A scale nobody can name is a scale nobody uses, so: `raised` for a card lifting off its plate, `overlay` for anything in a portal, `drag` for something under the cursor. In-flow elements still cast no shadow; this only names the legitimate path. Consumers adopt them in T2-T4. --shadow-minimal-flat is NOT deleted, against the audit. It has five live consumers, two of them in packages/ui/src/styles.css. The audit read its three DEFINITION sites as its only mentions — it searched the desktop renderer and the token lives in a package. It is also not an elevation tier at all: it is a 1px ring wearing a box-shadow, which is why it survived a scale it was never part of. Kept and documented as a ring. --- apps/desktop/src/renderer/maka-tokens.css | 24 ++++++++++++++++++++++- scripts/check-dead-css.mjs | 5 +++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/renderer/maka-tokens.css b/apps/desktop/src/renderer/maka-tokens.css index c576cdad4a..a664a724a1 100644 --- a/apps/desktop/src/renderer/maka-tokens.css +++ b/apps/desktop/src/renderer/maka-tokens.css @@ -299,8 +299,30 @@ --background); it carries its darwin overrides in "DARWIN GLASS" below. */ --color-bg-container: var(--background); + /* === elevation (visual system 2.0, T1) ============================= + Product names for the theme's shadow scale, so the right answer is + findable. The generated theme ships --shadow-low/med/high and product CSS + had used them exactly once, reaching instead for hand-rolled shadows — + a scale nobody can name is a scale nobody uses. + + Three names, three jobs: `raised` for a card lifting off its plate, + `overlay` for anything in a portal, `drag` for something under the + cursor. In-flow elements do not cast shadows; that rule is unchanged, + this only gives the legitimate path a name. + + Consumers move in T2-T4 — T1 does not touch product CSS. */ + --elevation-raised: var(--shadow-low); + --elevation-overlay: var(--shadow-med); + --elevation-drag: var(--shadow-high); + /* === shadow recipes === - Flat elevation uses a foreground-derived 1px ring in both themes. */ + Flat elevation uses a foreground-derived 1px ring in both themes. + + NOT an elevation tier and NOT dead: this is a 1px ring wearing a + box-shadow, and it has five live consumers (two of them in + packages/ui/src/styles.css). The T1 audit read its three DEFINITION + sites as its only mentions and called it dead; deleting it would have + taken two production styles with it. It stays, named for what it is. */ --shadow-minimal-flat: rgba(0,0,0,0) 0 0 0 0, rgba(0,0,0,0) 0 0 0 0, diff --git a/scripts/check-dead-css.mjs b/scripts/check-dead-css.mjs index 9469f7ca72..534d301180 100755 --- a/scripts/check-dead-css.mjs +++ b/scripts/check-dead-css.mjs @@ -202,6 +202,11 @@ const RESERVED_SCALE_TOKENS = new Set([ // soft tier's consumers are the sidebar rail and the card edge, which // converge on it in T2-T4; T1 defines the scale without touching product CSS. '--border-soft', + // Elevation tiers raised/overlay/drag (visual system 2.0 T1) -- product + // names for the theme's shadow scale; consumers adopt them in T2-T4. + '--elevation-raised', + '--elevation-overlay', + '--elevation-drag', // Control-height scale, 20/24/28/32/36/40 on the 4px ruler. '--h-control-xl', '--h-control-2xl', From e5d7cd1bdce4a1a51cc7f4ab361aa78bd3719999 Mon Sep 17 00:00:00 2001 From: jackwener Date: Sat, 8 Aug 2026 23:10:22 +0800 Subject: [PATCH 5/9] feat(theme): land both radius vocabularies on one card tier MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Visual system 2.0, T1 subsystem 5. The only subsystem here that changes existing pixels. Two vocabularies were running in parallel and disagreeing about the card: this file said 8, Astryx's own scale says 10 (--radius-element), and --radius-modal says 12. Three numbers for one idea is how a radius stops meaning anything. The card tier resolves to 10 rather than 8, because Astryx's element rung is what its own components already round to — meeting it there is what stops a card from disagreeing with the controls sitting inside it. 35 call sites read --radius-surface and all of them move together. --- apps/desktop/src/renderer/maka-tokens.css | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/renderer/maka-tokens.css b/apps/desktop/src/renderer/maka-tokens.css index a664a724a1..8e012bcb14 100644 --- a/apps/desktop/src/renderer/maka-tokens.css +++ b/apps/desktop/src/renderer/maka-tokens.css @@ -584,8 +584,20 @@ It is corner-adjacent with a uniform inset. If a proposed shrink is neither, it is not concentric — pick the tier the element's ROLE calls for instead. */ + /* Radius language (visual system 2.0, T1). Two vocabularies were running in + parallel and disagreeing on the card: this file said 8, Astryx's own scale + said 10 (--radius-element), and --radius-modal said 12. Three numbers for + one idea is how a radius stops meaning anything. + + They land on one set: 4 inner / 6 control / 10 card / 12 modal / full. + The card tier resolves to 10 rather than 8 — Astryx's element rung is the + one its own components already round to, so meeting it there is what stops + our cards from disagreeing with the controls inside them. + + Assignment is monotonic in box height, and `border-radius: 0` is legal + only on an edge-to-edge row. See DESIGN.md, Radius language. */ --radius-control: 6px; - --radius-surface: 8px; + --radius-surface: 10px; --radius-modal: 12px; --radius-pill: 999px; /* Square icon plates (provider logo, about logo, feature status) are From 1b019f480d1fa50eda8c826e0a2113228b8d8e23 Mon Sep 17 00:00:00 2001 From: jackwener Date: Sat, 8 Aug 2026 23:27:12 +0800 Subject: [PATCH 6/9] feat(theme): specify the status family instead of picking it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Visual system 2.0, T1 subsystem 6. Light mode; dark needs its own pass. The four status hues were four independent choices: measured against white, info 2.82:1, warning 3.29:1, success 4.55:1, destructive 4.81:1 — a 1.7x spread with TWO of them under the 4.5:1 AA floor. Nothing tied them together, so nothing kept them together. One lightness for all four, each keeping its own chroma. At L=0.50 they land 5.54-6.33:1 as a family. The residual spread is hue rather than drift: yellow carries more luminance than blue at equal lightness, and flattening that would mean giving up the shared L that makes them a family in the first place. info and warning move the most (0.68 and 0.66 down to 0.50) because they were the two that failed AA. This is the visible one; it belongs in the preview set. --- apps/desktop/src/renderer/maka-tokens.css | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/apps/desktop/src/renderer/maka-tokens.css b/apps/desktop/src/renderer/maka-tokens.css index 8e012bcb14..93bf052ba5 100644 --- a/apps/desktop/src/renderer/maka-tokens.css +++ b/apps/desktop/src/renderer/maka-tokens.css @@ -153,9 +153,19 @@ --warning's orange h55 (info + warning were indistinguishable). Blue is also consistent with the accent (h250) and unambiguously reads as informational, not cautionary. */ - --info: oklch(0.68 0.13 240); /* blue — informational / "ask" mode */ - --success: oklch(0.55 0.17 145); /* green — connected, ok */ - --destructive: oklch(0.58 0.24 28); /* red — errors, denied */ + /* Status family, specified rather than picked (visual system 2.0, T1). + One lightness for all four hues, each keeping its own chroma, so the + family reads as one system instead of four independent choices. + + Measured against white before this: info 2.82:1, warning 3.29:1, success + 4.55:1, destructive 4.81:1 — a 1.7x spread with TWO of the four under the + 4.5:1 AA floor. At L=0.50 they land 5.54-6.33:1 together. Residual spread + is hue, not drift: yellow carries more luminance than blue at equal + lightness, and equalising that would mean abandoning the shared L that + makes them a family. */ + --info: oklch(0.50 0.13 240); /* blue — informational / "ask" mode */ + --success: oklch(0.50 0.17 145); /* green — connected, ok */ + --destructive: oklch(0.50 0.24 28); /* red — errors, denied */ /* Legacy brand affordance for a few emphasized surfaces. New checked control states use --control instead. @@ -191,7 +201,7 @@ hue (h55); info is now cool blue (h240), so the two tiers read as clearly different families — no longer the near-identical ambers they once were. Still derived nowhere else, safe across palettes. */ - --warning: oklch(0.66 0.18 55); + --warning: oklch(0.50 0.18 55); --warning-text: color-mix(in oklab, var(--warning) 50%, var(--foreground)); /* === foreground solid mix scale === From be4c580771052618c33da9095e80ea10fba26628 Mon Sep 17 00:00:00 2001 From: jackwener Date: Sat, 8 Aug 2026 23:33:31 +0800 Subject: [PATCH 7/9] docs: ratify the 2.0 design system in DESIGN.md --- DESIGN.md | 155 ++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 110 insertions(+), 45 deletions(-) diff --git a/DESIGN.md b/DESIGN.md index 87dc3dc946..c0f1f01df1 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -9,18 +9,21 @@ colors: accent-solid-dark: "oklch(0.76 0.15 250)" on-accent-light: "#ffffff" on-accent-dark: "#171717" - surface-light: "oklch(1 0 0)" - canvas-light: "oklch(0.975 0 0)" + surface-raised-light: "oklch(1 0 0)" + surface-base-light: "oklch(0.975 0 0)" + surface-sunken-light: "oklch(0.945 0 0)" ink-light: "oklch(0.17 0.005 286)" - surface-dark: "oklch(0.205 0.004 286)" - canvas-dark: "oklch(0.18 0.004 286)" - ink-dark: "oklch(0.92 0.004 286)" - info-light: "oklch(0.68 0.13 240)" + surface-raised-dark: "oklch(0.205 0.004 286)" + surface-overlay-dark: "oklch(0.225 0.004 286)" + surface-base-dark: "oklch(0.18 0.004 286)" + surface-sunken-dark: "oklch(0.14 0.004 286)" + ink-dark: "oklch(0.95 0.004 286)" + info-light: "oklch(0.50 0.13 240)" info-dark: "oklch(0.74 0.13 240)" - success-light: "oklch(0.55 0.17 145)" + success-light: "oklch(0.50 0.17 145)" success-dark: "oklch(0.60 0.17 145)" - warning: "oklch(0.66 0.18 55)" - destructive-light: "oklch(0.58 0.24 28)" + warning: "oklch(0.50 0.18 55)" + destructive-light: "oklch(0.50 0.24 28)" destructive-dark: "oklch(0.70 0.19 22)" typography: display-1: { fontSize: "28px", fontWeight: 400, lineHeight: 1.4286 } @@ -37,22 +40,20 @@ typography: code: { fontSize: "14px", fontWeight: 400, lineHeight: 1.4286 } badge-label: { fontSize: "12px", fontWeight: 500, lineHeight: 1.6667 } rounded: + inner: "4px" control: "6px" - surface: "8px" - modal: "12px" + card: "10px" + container: "12px" + page: "16px" pill: "999px" - astryx-element: "0.625rem" - astryx-container: "0.75rem" - astryx-page: "1.75rem" - astryx-full: "9999px" spacing: { space-0-5: "2px", space-1: "4px", space-1-5: "6px", space-2: "8px", space-2-5: "10px", space-3: "12px", space-4: "16px", space-5: "20px", space-6: "24px", space-8: "32px", space-10: "40px", space-12: "48px", space-16: "64px" } components: - button-default: { typography: "{typography.label}", rounded: "{rounded.astryx-element}", padding: "8px 12px", height: "32px" } - button-primary-light: { backgroundColor: "{colors.primary}", textColor: "{colors.on-accent-light}", typography: "{typography.label}", rounded: "{rounded.astryx-element}", height: "32px" } - button-primary-dark: { backgroundColor: "{colors.accent-solid-dark}", textColor: "{colors.on-accent-dark}", typography: "{typography.label}", rounded: "{rounded.astryx-element}", height: "32px" } - input-default: { typography: "{typography.body}", rounded: "{rounded.astryx-element}", height: "32px" } - badge: { typography: "{typography.badge-label}", rounded: "{rounded.astryx-full}", padding: "0 8px", height: "20px" } - card-default: { rounded: "{rounded.astryx-container}", padding: "12px" } + button-default: { typography: "{typography.label}", rounded: "{rounded.card}", padding: "8px 12px", height: "32px" } + button-primary-light: { backgroundColor: "{colors.primary}", textColor: "{colors.on-accent-light}", typography: "{typography.label}", rounded: "{rounded.card}", height: "32px" } + button-primary-dark: { backgroundColor: "{colors.accent-solid-dark}", textColor: "{colors.on-accent-dark}", typography: "{typography.label}", rounded: "{rounded.card}", height: "32px" } + input-default: { typography: "{typography.body}", rounded: "{rounded.card}", height: "32px" } + badge: { typography: "{typography.badge-label}", rounded: "{rounded.pill}", padding: "0 8px", height: "20px" } + card-default: { rounded: "{rounded.container}", padding: "12px" } --- # Design System: Maka @@ -71,21 +72,78 @@ This document governs the default light and dark themes. Optional palettes may c Frontmatter is a snapshot of the current default theme. When it diverges from source or contract tests, source and tests win and this document must be refreshed. -## 2. Colors +## 2. Surfaces -The palette is cool-neutral and quiet. Light mode places white work surfaces on a near-white canvas; dark mode uses close zinc tones separated by hairlines. +Depth is a ladder, not a decoration. Every background in the app resolves to one of four semantic tiers, each derived from `--background` with cumulative offsets (palettes override only `--background`; the ladder follows). -- **Brand mark** is fixed `#71a8fd`; it identifies Maka and is never the general CTA color. -- **Interaction accent** follows the active palette for focus, links, selection, and live state. -- **Primary solid accent** is the contrast-safe variant for filled controls and other accent-colored text or icons. -- **Surface, canvas, and ink** create hierarchy through tone; derive secondary text, borders, and washes from ink. -- **Info, success, warning, and destructive** are meanings, not decoration. Enabled is not automatically success. +| Tier | Token | Role | Light | Dark | +|---|---|---|---|---| +| sunken | `--surface-sunken` | sidebar rail, recessed chrome | 237 | 9 | +| base | `--surface-base` | shell canvas behind plates | 247 | 17 | +| raised | `--surface-raised` | cards, content plates, reading surfaces | 255 | 23 | +| overlay | `--surface-overlay` | menus, popovers, dialogs, toasts | 255 | 27 | -**The Signal, Not Texture Rule.** Accent communicates action or state. Never use it as a background flood, gradient, glow, or substitute for hierarchy. +(Values are measured rendered pixels, not aspirations; the ink/surface contract tests hold them.) + +**The Height Rule.** Height maps monotonically to lightness in both modes, and reading surfaces always occupy the brightest tier of their mode. In light mode the ladder tops out at pure white, so `raised` and `overlay` share the fill and overlay separation hands off to the floating recipe (§5). Light mode "higher = darker" is permanently forbidden — it makes elevation shadows contradict the fill. + +**The Canvas Recedes Rule** (owner decision 2026-06-20). The canvas is gray; content surfaces are white. The sidebar sits on `sunken`, the shell on `base`, and content plates on `raised`. Contrast between canvas and plate — not hairlines — is the primary separator of the shell. + +**Legacy names.** The semantic tiers are canonical. Old names are aliases and their resolved values never change out from under consumers: `--surface-canvas` → base, `--background` → raised (it is the card fill, not the page color), `--background-elevated`, `--color-background-card`, `--color-background-popover` → overlay, `--card-bg`, `--color-background-surface` → raised. + +## 3. Ink + +Prose uses exactly three tiers, spaced at an even ~2× contrast rhythm, all above WCAG AA. Measured against `--surface-raised`: + +| Tier | Token | Light | Dark | +|---|---|---|---| +| primary | `--foreground` | 19.1:1 | 15.5:1 | +| secondary | `--foreground-secondary` | 9.8:1 | 9.2:1 | +| muted | `--muted-foreground` | 4.8:1 | 4.7:1 | + +- **The Three-Tier Reading Rule.** Prose uses primary, secondary, or muted. Neutral washes are surfaces, not extra text tiers. `--foreground-dimmed` is an alias of secondary and must never regain its own definition (contract-tested). +- **The One Colorspace Rule.** Every derivation inside a token family uses one colorspace (`oklch` for ink, contract-tested). Mixing `srgb` and `oklch` derivations produces "same literal, different value" drift. Known exception: dark `--surface-overlay` still derives via an srgb mix — a scheduled unification in T2–T4, not a precedent. +- **Links use the solid accent tier** (`--accent-solid`), never raw `--accent` — the accent identifies interaction; the solid tier is the only accent variant that clears text contrast on every palette. + +## 4. Borders + +Three strengths, each a job, spaced at ~1.6× like the ink ladder: + +- `--border-soft` (6% ink): quiet separation inside a plate — rails, row dividers that fills can't carry. +- `--border` (10% ink): structural boundaries between regions. +- `--border-strong` (16% ink): emphasis chrome only. Its legitimate jobs, from the live inventory: selected/active outlines and emphasized boundaries (onboarding, plan-mode, chat turn/quote chrome, the Astryx `--color-border-emphasized` mapping). Two call sites borrow it as a strong neutral *tint* rather than a border — a scrollbar thumb color and a separator glyph color — and are queued to migrate onto ink-derived tokens in T2–T4. It is not "the border for when you're unsure." +- `--shadow-minimal-flat` is historically a 1px ring wearing box-shadow clothing (`0 0 0 1px`), not an elevation step; it belongs to this chapter in spirit and migrates to a ring-named border token in T2–T4 (cross-package consumers exist in `packages/ui`). + +**The One Means Rule.** Each boundary picks one separator: a fill step, a line, or a shadow — never stacked on the same edge. + +## 5. Elevation -**The Three-Tier Reading Rule.** Prose uses primary, secondary, or muted foreground. Neutral washes are surfaces, not extra text tiers. +Default surfaces are flat. Depth comes first from the surface ladder, then a line, then shadow only when an element genuinely floats above the plane. -## 3. Typography +- Product elevation names alias the theme scale: `--elevation-raised` (low), `--elevation-overlay` (med), `--elevation-drag` (high). A scale only gets used when product code can name it — the theme shipped three shadows for months and product CSS consumed one, because the names meant nothing at a call site. +- **The Floating Recipe.** Every portal surface (menu, popover, dialog, toast) is: `--surface-overlay` fill + `--border-soft` ring + `--elevation-overlay` + `overflow: hidden` + container radius. No portal invents its own mix. +- Dark mode relies on tone and rings before shadow. Neon edges and lifted-everything styling are forbidden. +- Native shell vibrancy is allowed only in designated material; generic glassmorphism is not. + +**The One Working Plane Rule.** Dividers separate responsibilities; cards and shadows do not fragment the workspace into a dashboard grid. + +## 6. Radius + +Nothing interactive is square. One ladder, assigned monotonically by box height: + +| Radius | Tier | Assign to | +|---|---|---| +| 4px | inner | chips, keycaps, nested inlays inside a control | +| 6px | control | buttons, inputs, segmented items (≤ 36px tall) | +| 10px | card | cards, rows-as-cards, list containers (the single card value — 8/10/12 coexistence is over) | +| 12px | container | modals, panels, portal surfaces | +| 16px | page | page-level plates and hero surfaces | +| full | pill | badges, pills, circular controls | + +- **The Full-Bleed Rule.** `border-radius: 0` is legal only on true full-bleed rows — an element flush with its container on both sides. Radius and gap move together: if it has breathing room, it has corners. +- **Proportional marks.** Product-drawn icon plates use ratio-owned radius (~25–27% of the box edge), recorded in prose because Stitch accepts only absolute units. + +## 7. Typography Use the system UI stack with explicit platform CJK fallbacks; Geist Variable is a late fallback. Code uses Geist Mono Variable, JetBrains Mono, then platform monospace. Chinese and Latin must read as one interface. @@ -100,29 +158,30 @@ Use the system UI stack with explicit platform CJK fallbacks; Geist Variable is **The Four-Pixel Line Rule.** Text line boxes land on the 4px grid. Mono is technical, never decorative. -## 4. Elevation +## 8. Color Specification -Default surfaces are flat. Depth comes first from canvas-to-surface tone, then a hairline, then shadow only when an element genuinely floats. +The palette is cool-neutral and quiet; color is generated to spec, not picked by eye. -- Use `--shadow-minimal-flat` for compact tool previews that need an edge without visible lift. -- Use Astryx `low`, `med`, or `high` elevation through component APIs for floating controls, popovers, dialogs, and overlays. -- Dark mode relies on tone and rings before shadow. Neon edges and lifted-everything styling are forbidden. -- Native shell vibrancy is allowed only in designated material; generic glassmorphism is not. +- **Brand mark** is fixed `#71a8fd`; it identifies Maka and is never the general CTA color. +- **Interaction accent** follows the active palette for focus, selection, and live state; **links and accent-colored text use the solid tier** (§3). +- **Status families** (success / active / attention / error / neutral — there is no "info" status semantic) are generated, not picked: one lightness per mode with each hue keeping its own chroma. Light mode is generated at L=0.50 (contrast vs white spans 5.5–6.3:1; the residual spread is hue physics — at equal L, yellow carries more luminance than blue — and flattening it would abandon the shared-L premise that makes them a family). This regeneration fixed two AA failures the old hand-picked values shipped (info 2.82:1, warning 3.29:1). Dark mode keeps its pre-2.0 values (all ≥4.5:1); regenerating dark at its own single L is a scheduled separate round. A louder band at ~90% gamut chroma exists only for 8px status dots — dots must read at a glance; washes must not shout. +- **Tinted surfaces** (status washes behind rows and banners) derive from the same status hues; hand-rolled `oklch()` status washes at call sites are forbidden — consume the family. +- **Identity colors** (avatars, channel marks) live in one 4.2–4.8:1 contrast band; desaturation for muted states happens at constant OKLab lightness. -**The One Working Plane Rule.** Dividers separate responsibilities; cards and shadows do not fragment the workspace into a dashboard grid. +**The Signal, Not Texture Rule.** Accent communicates action or state. Never use it as a background flood, gradient, glow, or substitute for hierarchy. -## 5. Components +## 9. Components Use Astryx primitives as the default seam. New work composes product meaning through published props, tokens, and stable `themeProps` extension points; internal-DOM overrides are acknowledged transitional states, not precedent. -- **Controls:** Maka uses a 20/24/28/32/36/40px height ruler with 32px as the default; Astryx owns the 28/32/36px variants. Hover is restrained; press may use `scale(0.98)`; keyboard focus is always visible. -- **Fields:** labels, descriptions, and validation belong to the field primitive; input focus belongs to its control. Keep disabled reasons discoverable through the owning control’s tooltip; do not rebuild field chrome around a bare input. +- **Controls:** Maka uses a 20/24/28/32/36/40px height ruler with 32px as the default; Astryx owns the 28/32/36px variants. Hover is restrained; press may use `scale(0.98)`; keyboard focus is always visible. At most one inverted (filled) element per control. +- **Fields:** labels, descriptions, and validation belong to the field primitive; input focus belongs to its control. Keep disabled reasons discoverable through the owning control's tooltip; do not rebuild field chrome around a bare input. - **Badges and status:** Badge is 20px high and pill-shaped. Choose semantic variants by meaning, not hue; use status dots for success, active, attention, error, or neutral. -- **Cards:** In the Maka theme, Astryx Card uses 12px radius, 12px default padding, and no resting elevation. Astryx components own their geometry; product-drawn square icon plates use the ratio-owned 27% radius, recorded in prose because Stitch accepts only absolute radius units. +- **Cards:** Astryx Card uses container radius, 12px default padding, and no resting elevation. Astryx components own their geometry. - **Workspace:** conversation, tool activity, artifacts, browser state, and generated files stay connected to the task that produced them. Assistant messages remain quiet and avatar-free. - **Custom companion:** a desktop pet is the sole mascot exception: user-supplied, disabled by default, decorative, pointer-transparent, hidden from assistive technology, and reduced-motion aware. It never conveys required status or speaks for the agent. -## 6. Do's and Don'ts +## 10. Do's and Don'ts ### Do: @@ -132,9 +191,15 @@ Use Astryx primitives as the default seam. New work composes product meaning thr - **Do** preserve keyboard focus, disabled reasons, loading and error states, and reduced-motion behavior. - **Do** keep optional palette inventories in source while preserving documented roles and contrast. -### Don't: +### Don't (the forbidden list — each item is contract-tested or review-blocked): -- **Don't** use generic AI gradients, glowing borders, sparkle, decorative “thinking,” or default glassmorphism. +- **Don't** write a bare `oklch()` status color or wash at a call site — consume the generated families (§8). +- **Don't** use `border-radius: 0` off a full-bleed row (§6). +- **Don't** hardcode `background: white` or any literal surface color — resolve a ladder tier (§2). +- **Don't** put more than one inverted element in a single control. +- **Don't** mix `srgb` and `oklch` derivations inside one token family (§3). +- **Don't** make light mode's "higher" darker (§2), stack two separators on one edge (§4), or invent a portal recipe (§5). +- **Don't** use generic AI gradients, glowing borders, sparkle, decorative "thinking," or default glassmorphism. - **Don't** personify the agent through mascots, fake emotion, excessive avatars, or chat ornament; the optional user-supplied pet is the only exception. - **Don't** turn every region into a card or every status into a colored pill. - **Don't** introduce another accent, spacing ruler, radius tier, icon system, text axis, or parallel component path. From 587cea06bf171a92c33dc6b480c50b76c908261f Mon Sep 17 00:00:00 2001 From: jackwener Date: Sat, 8 Aug 2026 23:49:18 +0800 Subject: [PATCH 8/9] fix(theme): give dark warning its own value again MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --warning was the one status hue with no dark override. info, success and destructive each state their own dark value, so lowering the shared light value to L=0.50 dragged dark down with it: dark warning measured 2.83:1, well under AA, in the half of the theme the previous commit claimed not to touch. Restored to its measured 5.44:1 by stating it where its three siblings state theirs. The asymmetry was the trap — a family where one member is defined once and three are defined twice does not behave like a family. --- apps/desktop/src/renderer/maka-tokens.css | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/desktop/src/renderer/maka-tokens.css b/apps/desktop/src/renderer/maka-tokens.css index 93bf052ba5..3a80c21335 100644 --- a/apps/desktop/src/renderer/maka-tokens.css +++ b/apps/desktop/src/renderer/maka-tokens.css @@ -893,6 +893,12 @@ --info: oklch(0.74 0.13 240); /* blue — see light-mode note */ --success: oklch(0.60 0.17 145); --destructive: oklch(0.70 0.19 22); + /* Dark warning is stated here because --warning is the ONE status hue that + had no dark override: info, success and destructive each carry their own, + so lowering the light value to L=0.50 dragged dark down with it and put + dark warning at 2.83:1, well under AA. This restores its measured 5.44:1 + and makes the family's dark half uniform in shape. */ + --warning: oklch(0.66 0.18 55); --opacity-overlay: 0.06; From 48f75d37cae70acb70b35ddd05b790c9766ea770 Mon Sep 17 00:00:00 2001 From: jackwener Date: Sat, 8 Aug 2026 23:51:48 +0800 Subject: [PATCH 9/9] docs: split warning into per-mode values so the doc cannot re-teach the shared-definition bug --- DESIGN.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/DESIGN.md b/DESIGN.md index c0f1f01df1..f258683ae4 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -22,7 +22,8 @@ colors: info-dark: "oklch(0.74 0.13 240)" success-light: "oklch(0.50 0.17 145)" success-dark: "oklch(0.60 0.17 145)" - warning: "oklch(0.50 0.18 55)" + warning-light: "oklch(0.50 0.18 55)" + warning-dark: "oklch(0.66 0.18 55)" destructive-light: "oklch(0.50 0.24 28)" destructive-dark: "oklch(0.70 0.19 22)" typography: