refactor(ui): move typography authority to the Astryx type scale - #1857
Merged
Conversation
Astro-Han
marked this pull request as draft
August 1, 2026 17:12
Astro-Han
force-pushed
the
refactor/typography-astryx-type-scale
branch
2 times, most recently
from
August 2, 2026 02:41
edba92d to
fbb0c03
Compare
Maka's density was expressed as `html { font-size: 13px }`, which is not a
type scale but an implicit x0.8125 multiplier on every rem in the document —
including the radius and spacing constants Astryx compiles against a 16px
root. Body copy was then pinned back to 13px on the Theme wrapper to undo the
multiplier for prose alone, leaving every other tier shrunk: supporting text
rendered at 9.75px instead of 12, h1 at 19.5 instead of 24, --radius-element
at 8px instead of 10. One intent, two contradicting expressions, and a
compensating patch between them.
Declare the intent where Astryx expects it instead. `{base: 13, ratio: 1.15}`
reproduces Maka's four hand-written tiers exactly (caption 11 / ui 13 /
heading 15 / stat 20) and supplies the matching 4px-grid line heights, so
those names become aliases rather than independent values. The root returns
to 16px and the compensating pin is gone.
The font stacks move for the same reason: Astryx's neutral default leads with
Figtree, which Maka does not bundle and whose stack carries no CJK face, so
Astryx surfaces silently fell back while the product used its own stack.
--font-sans / --font-mono now alias the theme tokens.
astryx-tokens also moves after astryx-components in the cascade order. A
theme layered before the component library can never override the neutral
defaults astryx.css declares on `:root`; that only looked harmless while
nothing resolved a token there, because inside the wrapper the theme still
won on tree distance. The 22 rem literals left in product CSS become absolute
px, matching the spacing scale's existing rule.
Measured in the live app (before -> after): body 13 -> 13px, bubble leading
21.0002 -> 20px, h1 19.5/25.9994 -> 20/28px, h2 16.25/22.75 -> 17/24px,
li and code leading 18.5718 -> 20px, supporting 9.75 -> 11px,
--radius-element 8 -> 10px. The four product tiers are unchanged.
Astryx's reset hard-codes a monospace stack on :where(code, kbd, samp, pre) that never consults --font-family-code, so every code element — Markdown code blocks in the transcript included — opted out of the theme and rendered in ui-monospace rather than the Geist Mono the app bundles and declares. :where() has zero specificity and reset is the lowest layer, so pointing the same selector at the token is the whole fix. This was the last parallel typography authority left after the type-scale consolidation. Verified in the live app: code blocks now resolve "Geist Mono Variable", "JetBrains Mono", ui-monospace, … at 13px.
The consolidated ladder was 13/1.15 — a faithful reproduction of the tiers
Maka had already converged on, chosen so the previous commits' visual delta
stayed attributable. With the authority in one place, retune the values.
Read the shipped bundles of the three products Maka is benchmarked against:
Cursor 3.14.7 (CDP against the live Agents window), Claude Code's desktop
surface (the Epitaxy layer inside Claude.app), and Codex desktop
(openai-codex-electron). All three sit at 14px body. All three keep secondary
text at 12-14px; none goes near the 11px Maka used for reasoning and tool
rows, which carry what the agent is doing and were the least legible text in
the product. Cursor puts tool rows at full body size and de-emphasises purely
with colour.
scale: {base: 14, ratio: 1.125}
1.125 is the only ratio that moves base to 14 while keeping 11 and 20 on the
ladder, so caption and stat stay put and only base (13->14) and heading
(15->16) move. It is also what Astryx's expandTypeScale header recommends for
"Dense/functional". Body leading recomputes to 20px — the same absolute
leading as before, so the type grows without the paragraph loosening.
Three transcript-scoped overrides, all taking values from the ladder:
- Reasoning and tool-call rows read at body size, not supporting size.
- Markdown headings flatten to two steps: h1 at lg, h2-h6 at body, all
weight 600, all on one baseline. Astryx's ladder is a document scale and a
turn is not a document — an agent emits `##` every few lines, and 1.4x per
level shreds one reply into unrelated slabs. Cursor, Claude Code and Codex
all flatten here relative to their own document styles; Claude Code merges
h4-h6 into `strong`. Settings and docs keep the full ladder.
- Markdown renders at density="compact", Astryx's own answer to heading
spacing tuned for pages rather than turns.
Measured in the live app: body 14/20, tool rows 12->14, h1 16/20, h2-h4
14/20 at weight 600, inline code and code blocks 14/20.
Converting product CSS off rem swept up .modelPickerProviderMark, which is the one place the unit is load-bearing rather than habitual: chat-shell-layout-contract.test.ts asserts a fixed 1rem square so the mark tracks the root instead of any single font tier and cannot be squashed by flex. The contract landed in #1854 and the rebase surfaced the collision.
Four defects, one mistake: transcript-only typography was attached to selectors that were either wider than the transcript or narrower than the DOM they were aiming at. The reasoning rows never got the retune. `> span:not(:last-child)` reads as "every span but the chevron"; ChatReasoning wraps its label, duration and preview in a `<div>` (packages/ui/src/astryx-chat-reasoning.tsx), so the rule enlarged the leading icon wrapper and left the reasoning text at supporting size -- the single row this work exists for. Astryx's supporting atoms are `font-size: var(--text-supporting-size)` and the matching leading, so the rows now rebind those two tokens on the trigger: inheritance reaches every span that opts into the role, at any depth, and the rule stops depending on Astryx's child order. `.astryx-heading.level-N` matched nothing. Astryx's Markdown renderer emits bare h1-h6 with StyleX props; those classes come from the standalone Heading component, which Markdown never mounts. They were live enough to turn the CI `Dead CSS` gate red (7 classes over a baseline of 0) and dead enough to have no effect. The heading flattening reached Daily Review. Its argument is that an agent turn is not a document -- but a review report IS one, and both render through the shared MarkdownBody contract. Scoped to `.maka-turn`. `--maka-chat-line` was declared on `.maka-turn, .maka-chat-message` and read by rules matching outside them, where it resolved to nothing and took `line-height` down with it as an invalid value. It is a calc of two global tokens, so the scoping was invented coupling: renamed `--maka-line-body` and declared with the tiers it derives from. Drops the typography `!important`s while here. Astryx's StyleX lives in the earlier astryx-components layer, so product CSS in `components` already wins on layer order whatever the specificity; the one real inline-style opponent was an ejected chevron's `0.75rem`, removed separately. Also updates the comments the 14/1.125 retune left describing 13/1.15.
Eleven renderer headings still sized themselves with `em` multipliers --
2.1333, 1.7333, 1.4667 and friends across hero, onboarding, settings and the
module pages. Those decimals are the tell: each was hand-derived against
whatever the body happened to be when it was written, so every base change
since has silently rescaled them. The hero comment claimed 32px; 2.1333em was
derived from a 15px body and had been rendering 27.7px under a 13px one, and
would now render 29.9px. That is three different sizes for one decision.
Each now names the ladder rung nearest what the page actually shipped, so
this is a convergence rather than a visual redesign: 28 / 22 / 20 / 18 / 16.
With these gone the renderer has no independent font-size authority left.
Two rem lengths in TSX went with them. The earlier sweep only read `.css`,
but removing `html { font-size: 13px }` is a document-level change: the tool
code block's `16rem` max height would have gone 208px -> 256px, and the
reasoning chevron's inline `0.75rem` had been disagreeing with its own
width/height attributes for as long as the root was pinned. The attributes
already carry the size, so the inline style is simply gone.
Three declarations hold the scale up, and reverting any of them was silent: the root staying at the browser default, the generated theme layered after the Astryx component sheet, and the product names staying aliases. Mutating each one left all 24 existing CSS-governance tests green. `type-scale-contract.test.ts` pins them as text, plus the ladder rungs the four aliases point at, the monospace routing, the turn-scoped heading flattening, the role-token rebind, and the absence of em/rem font-sizes. Six mutations were run against it; each fails exactly one test. `e2e/type-scale.spec.ts` measures what text cannot prove. Custom properties resolve by tree distance while rules resolve by layer, and the two disagree at `:root` -- which is exactly how an earlier revision of this work shipped aliases resolving to Astryx's neutral defaults with every file reading correctly. It probes the tokens at `:root` in px, so it sees what a portaled Astryx component sees. Reverting the layer order turns `--font-size-heading` into 17px, the neutral value, and the spec fails on it. Wires `astryx:theme -- --check` into CI: makaTheme.ts is now the type-scale authority, and nothing stopped it from drifting from the artifacts it generates. Corrects the cascade-layers comment while here. It claimed promoting the theme "cannot reach product markup" because the sheet only carries `.astryx-*` rules -- but Astryx components ARE product markup, and 67 such rules now win where they previously lost. Card and Section padding are NOT among them: `--astryx-*-padding` are theming hooks Astryx never declares itself, so those applied under either order. Button, badge and status-dot colors did change.
`html { font-size: 13px }` is gone, so the workarounds that existed to
undo it have nothing left to undo.
The session-list section-title pin is the load-bearing one. It was added
one commit before this branch (#1860) because Astryx declares SideNav
section titles as supporting/sm — 0.75rem, which under a 13px root
collapsed to 9.75px, smaller than caption and unreadable beside 13px nav
labels. Pinning them to the product UI tier fixed the symptom by
flattening the hierarchy: titles and labels ended up the same size, told
apart only by colour.
Measured on the sidebarLongSessions fixture after the root moved back to
the browser default:
section title 14px / 19.25px -> 12px / 20px
nav label 14px -> 14px (unchanged)
So the ladder now says what the pin was trying to say — titles a tier
below labels, and the leading lands on the same 20px grid as the rest of
the product. The `!important` went with it; product CSS sits in the last
cascade layer and never needed it.
The other two are comments that state the old base as fact: the tracking
scale's worked example and the tool-output panel's body-type note. Both
now name the tier instead of a number, so neither can go stale again the
next time the scale moves.
Three independent reviews (two fresh-eye agents, one Codex) landed on the
same defects. Everything below was verified in the live app before being
acted on; two of the reviewers' own claims did not survive that and are
recorded here as rejected.
The one behaviour change: `density="compact"` was hardcoded in
MarkdownBody, whose other caller is the Daily Review panel. That
contradicted this branch's own reasoning — the heading-size flattening is
scoped to `.maka-turn` precisely because a review report is a document —
so the review kept full heading sizes but got transcript block spacing,
the combination neither half of the argument asks for. Density is now a
caller choice defaulting to `default`, with the two chat-turn call sites
opting in.
Comments that stated something false, each replaced with what was
measured:
- "the radius and spacing rem constants Astryx compiles against a 16px
root" (four copies). Astryx's spacing and radius tokens are px
literals and never moved. What is rem is its Icon size atoms, which
it documents as the px-equivalents at a 16px root — so the old pin
was rendering the whole icon set at 9.75/13/16.25/19.5 instead of
12/16/20/24. Measured on main and on this branch.
- "inside the Theme wrapper the theme still won, since custom
properties resolve by tree distance". False. Probed on main at both
<html> and the inner wrapper: --font-size-lg 13.8125px, --font-size-xl
16.25px, i.e. Astryx's neutral values. The generated theme was inert
everywhere, not merely at :root.
- "at 11px on the old one" for the disclosure rows. Measured 9.75px;
11px was a reading of an intermediate commit, not of main.
- "the same absolute leading as before". The transcript was 21px and is
now 20px — 1px tighter, not unchanged.
- "1.125 is the only ratio that keeps 11 and 20 on the ladder". 1.13
also rounds there. It is the natural choice, not the forced one.
- the cascade-layer blast radius, which said 67 class rules change.
Compared declaration by declaration: 44 are contested but resolve to
the same value, 12 are uncontested and already applied, and 11
actually move. Spot-checked live — destructive Button
rgb(165,12,37)/white to rgb(250,206,203)/rgb(165,12,37), accent
StatusDot rgb(38,38,38) to rgb(0,116,226).
DESIGN.md still named maka-tokens.css as the type authority and carried
the 15/13/11 tiers, and maka-tokens.css still claimed to be the single
source of truth for typography. Both now point at makaTheme.ts.
The eight geometry `!important`s went too. The comment four lines below
them already said product CSS wins on layer order alone; deleting them
and re-measuring confirms it — the chevrons still render 14x14 with a
10x10 svg. This file now declares none.
The tests close five holes the reviewers proved by mutation — each of
these edits was previously green:
- `@maka/ui/styles.css` reaches the renderer through a bare specifier,
which the CSS import expander skipped along with node_modules. ~30
font-size declarations were invisible to every scan; the expander now
resolves first-party workspace sheets.
- `--maka-line-body` had no guard at all. Deleting it, or replacing it
with the hand-rolled multiplier this branch exists to retire, passed
everything. Now pinned in the contract and resolved live in e2e.
- the root-pin ban only matched a bare `html`/`:root` selector, so
`html[data-os="darwin"] { font-size: 13px }` slipped through.
- the unscoped-heading ban and the `font-size: !important` ban only
read chat-message.css. Both now scan the whole renderer graph; the
second one would have caught the sidebar pin this branch removed.
- the monospace routing was asserted as text only, on the stated
grounds that it "follows arithmetically". It does not — it is a
layer-order question — and it is now probed live.
Density gets a render contract that fails if the default flips back.
Rejected after checking: that the model-picker mark's `1rem` is an
unnoticed 23% growth to be reverted. It is an icon box, and 1rem is
Astryx's `sm` icon at a 16px root — the old 13px was the anomaly. The
comment now says that instead of the circular "it tracks the root".
Also declined: a live ChatReasoning probe. No fixture renders one, and
seeding it means changing a shared scenario every spec sees; the gap is
now stated in the spec instead of left implicit.
Astro-Han
added a commit
that referenced
this pull request
Aug 2, 2026
PR #1860 rendered sidebar sessions as Astryx SideNavItem rows, which broke four e2e specs: - SideNavItem renders endContent (the row-actions MoreMenu trigger) inside the row's primary <button>, so the row button's accessible name became a composite ending in the trigger's label and name queries hit strict-mode violations. The nesting is SideNavItem's own DOM, and moving the trigger out needs sidebar CSS frozen by PR #1857, so the locators are tightened (project scope .maka-project-item-end; session exact: true). - The geometry spec's 60-row pin selected rows by title^="会话 ", but the rows' title is now session metadata; the pin was deleted — its only purpose (proving the list overflows) is asserted directly at (1a) via scrollHeight - clientHeight > 50. e2e: 70 passed / 0 failed; format/lint/typecheck/dead-css and the desktop (1305) and ui (238) unit suites all pass.
Astro-Han
force-pushed
the
refactor/typography-astryx-type-scale
branch
from
August 2, 2026 03:35
fbb0c03 to
fcd7722
Compare
The leading tiers still carried a "single source" claim from #520. Size now has one authority and a contract that proves it; leading has three vocabularies, and two of them disagree — --leading-normal is 1.5, i.e. 21px at the body tier and off the 4px grid, where Astryx computes 20px. The transcript reads Astryx's and is internally consistent; product chrome sits 1px looser. Not fixing it here. Converging them moves 66 sites, which is a relayout with its own before/after, and nothing about this branch blocks it. Stating it beats leaving a stale claim four lines under the derived token that contradicts it.
Astro-Han
marked this pull request as ready for review
August 2, 2026 03:46
This was referenced Aug 2, 2026
Astro-Han
added a commit
that referenced
this pull request
Aug 2, 2026
This branch pinned Astryx's four icon sizes to px because the product root was 13px and every rem-based icon landed at 81% — 13px next to the 16px lucide icons handed to the same slots. That was true when it was written and is not true now: #1857 landed in the rebase and removed the root pin outright, saying so in maka-tokens.css ("the root font-size stays at the browser default (16px) ON PURPOSE") for exactly the same reason this commit existed. So the pin restates what rem already computes. Measured in Storybook across three stories, with the rule and with it reverted: xsm 12, sm 16, md 20, root 16px, identical either way. Keeping it would leave 34 lines of CSS whose comment asserts a 13px root that main's own token sheet denies — the precise shape of rot the rest of this branch is removing.
Astro-Han
added a commit
that referenced
this pull request
Aug 2, 2026
This branch pinned Astryx's four icon sizes to px because the product root was 13px and every rem-based icon landed at 81% — 13px next to the 16px lucide icons handed to the same slots. That was true when it was written and is not true now: #1857 landed in the rebase and removed the root pin outright, saying so in maka-tokens.css ("the root font-size stays at the browser default (16px) ON PURPOSE") for exactly the same reason this commit existed. So the pin restates what rem already computes. Measured in Storybook across three stories, with the rule and with it reverted: xsm 12, sm 16, md 20, root 16px, identical either way. Keeping it would leave 34 lines of CSS whose comment asserts a 13px root that main's own token sheet denies — the precise shape of rot the rest of this branch is removing.
Astro-Han
added a commit
that referenced
this pull request
Aug 2, 2026
This branch pinned Astryx's four icon sizes to px because the product root was 13px and every rem-based icon landed at 81% — 13px next to the 16px lucide icons handed to the same slots. That was true when it was written and is not true now: #1857 landed in the rebase and removed the root pin outright, saying so in maka-tokens.css ("the root font-size stays at the browser default (16px) ON PURPOSE") for exactly the same reason this commit existed. So the pin restates what rem already computes. Measured in Storybook across three stories, with the rule and with it reverted: xsm 12, sm 16, md 20, root 16px, identical either way. Keeping it would leave 34 lines of CSS whose comment asserts a 13px root that main's own token sheet denies — the precise shape of rot the rest of this branch is removing.
Astro-Han
added a commit
that referenced
this pull request
Aug 2, 2026
…rail (#1876) * fix(desktop): paint column surfaces through a transparent titlebar overlay Stop using AppShell topNav as a full-width chrome row so sidebar canvas and session --background extend to the window top (Codex/Claude/Cursor). Keep a single absolute drag overlay with no-drag action clusters, Windows WCO gutters, and 16px secondary ghost icons aligned with SideNav chrome. * feat(desktop): collapse the session rail into a 48px icon rail Continuous Collapse: the 48px rail stays in flow and paints ink only, with `.appFrame` flipping to the plate background when collapsed, so the transparent titlebar reads as one surface with the column instead of a chrome bar. Session rows share the nav items' left edge: `.maka-list-stackContent` no longer adds a second inline inset on top of SideNav's `scrollable` padding, and its grid track is `minmax(0, 1fr)` so a long title cannot push the row past the rail and get its end actions clipped. `.maka-session-item-label` is block-level flex, not inline-flex. An inline box is shrink-to-fit with a min-content floor, which for a nowrap title is the whole string — the label then outgrew its slot and painted over the row meta instead of ellipsizing. Width easing is scoped to collapse/expand via `data-easing`. A standing width transition also caught the per-pointer-move updates from the resize handle and made the drag lag the cursor. * fix(desktop): restore Astryx's icon scale under the 13px product root Astryx sizes icons in rem so they track the root font-size, and documents the scale as its px-equivalents at a 16px root: 12 / 16 / 20 / 24. Maka's root is 13px, so every icon Astryx renders lands at 81% of that — 13px sitting next to the 16px lucide icons the product hands to Astryx slots by absolute size. The nav rail showed it worst: 新任务 / 扩展 / 定时任务 read visibly smaller than the same-family icons one settings page away. The 13px type scale is deliberate; the icon scale was never meant to follow it, so restate Astryx's own intent in px at the renderer mounts rather than move the rem base. Class-on-class beats StyleX's single-class rules without !important. * fix(desktop): drop the footer hairline on the collapsed rail The rule that was supposed to do this targeted `.maka-session-panel-footer`, whose border is already `0 !important` — it never had a border to clear. The hairline is drawn one level up, on the sticky-bottom host that separates the scrolling history from the footer. At 48px there is no history column left to separate, so the line reads as an orphan rule floating above the Settings icon on an otherwise ink-only rail. Scope the collapsed override to the rule that owns the border in sidebar.css rather than reaching for it from the shell. * docs(desktop): stop citing contracts that no longer exist maka-tokens.css claimed five static guards enforced its scales: motion-token-converge, icon-system-contract, the spacing adjacency pin, and governance contract #406. All of them went out with the 149 source-scanning tests in #1724. A comment that promises a guard nobody runs is worse than no comment — it reads as "the build will catch this" while nothing does. The rules themselves stay; only the enforcement claims change to what actually holds them: convention and review. Also record why chat-message.css reaches for an Astryx duration instead of the product scale. It is not a leak: Astryx ChatToolCalls opens its own group with `grid-template-rows --duration-medium --ease-standard`, so a detail row that expands beside one has to share that clock. Without the note the next reader "fixes" it onto --duration-large and desyncs the two disclosures. check-dead-css: allowlist astryx-icon and astryx-app-shell-sidenav, both themeProps classes that exist only at runtime. * chore(desktop): delete orphan tokens and let check-dead-css see them Sixteen tokens in maka-tokens.css had no reader left anywhere: the --action CTA pair, --toast-accent, --bot-brand-default, --brand-deep-hover, --system-alert-accent, two --color-* alias tiers, --shadow-modal, --focus-glow-width, --scale-hover, --ease-drawer, --font-serif, --h-toolbar, --h-list-header, and --w-sessionlist (session width has been React state for a while). Their surrounding notes claimed live consumers, so the comments went with them. Ten unused tokens stay. A rung of an ordered scale outliving its last consumer is not rot — deleting the middle of the z-index or control-height ladder is exactly what invites the next bare number. RESERVED_SCALE_TOKENS carries them with the series each belongs to. check-dead-css now sweeps tokens alongside classes, so this converges instead of drifting back. Two consumer surfaces the class sweep never needed are in scope for tokens: packages/ui stylesheets, and stories — a story composing a surface out of the design vocabulary is a real reader, though a product class only a story mentions is still dead. Verified both ways: an injected orphan trips --check in either declaration style, and the live renderer resolves every var() after the deletions. * fix(desktop): give the session column a plate you can actually see Nothing draws a line between the session rail and the content column, by design: Astryx ships a divider only on `AppShell variant="section"` and Maka runs "surface" (#1732). The separation is meant to come from the material — `--surface-canvas` behind the rail, `--background` on the content — which is exactly what the token block has claimed since the gray-plate/white-card model landed. The step was too small to deliver it. `l - 0.024` resolves to #f7f7f7 against white: six sRGB levels, below what reads as a second surface on most displays. Until now nobody saw the miss, because Astryx painted its own opaque surface over the frame plate; this branch makes the shell transparent so the plate finally shows, and what showed was almost nothing. 0.045 is not a taste pick. It is the surface→body step Astryx itself ships (#ffffff L1.000 → #f1f1f1 L0.958), and it puts light mode within a hair of the dark step (0.065) this file has carried all along. Dark stays where it is — it was tuned against a dark plate, reads fine, and is not the boundary anyone reported losing. Not variant="section": a hairline plus a wash is two mechanisms for one job, and section's divider is unconditional, so it would draw a rule down the 48px icon rail — the same orphan line this branch just removed above Settings. * refactor(desktop): let AppShell own the column materials The column boundary was a product invention held together by two `!important` rules: paint `--surface-canvas` on `.appFrame`, then force AppShell and its sideNav slot transparent so the plate reads through them. Every layer in between had to stay transparent forever, and Astryx's own material tokens sat unused underneath. Astryx ships this exact model. `variant="elevated"` — its default — paints the nav column `--color-background-body` and the content column `--color-background-surface`; with no topNav there is no radius or backdrop, just the two materials. Maka ran `"surface"`, the one variant that paints both columns the same and draws no divider, which is why there was nothing to see. Taking Astryx's paint means fixing whose colors it paints with. Its neutral pair is static light-dark(), so it cannot follow Maka's six switchable palettes — and in dark mode the two systems disagree outright: Astryx's surface is #262626 against the product's #171719, so the nav column rendered LIGHTER than the content it navigates. makaTheme.ts now points both material tokens back at the palette. Direction is deliberate and opposite to the type scale above it: Astryx's scale covers everything Maka needs from type, while its neutral pair is a fraction of a palette that also carries status, chat, and per-theme colors. Dark's plate step joins light's 0.045. The old 0.065 was never seen on the shell — Astryx painted over the frame plate until now — and against #171719 content it renders a near-black #09090b rail. Continuous Collapse survives as the one rule that overrides a column material: collapsed, the 48px rail takes the content surface, because the traffic lights are wider than the rail and a wash boundary would run a seam through them. Measured in Storybook (Chromium, sampled pixels), both schemes, both states: light expanded nav #f0f0f0 content #ffffff light collapsed nav #ffffff content #ffffff dark expanded nav #0d0d0f content #171819 dark collapsed nav #171719 content #171819 * fix(desktop): stop tooltips from swallowing clicks under them Astryx tooltips are text popovers with no interactive content, but they leave pointer-events at auto, so an open tooltip is a hit target. Anchored to a control in the titlebar there is no room above it, so the layer flips below the window's top edge and lands on the SideNav's first row — 86×28 of it, measured live, directly over 新任务. The E2E caught it deterministically: click 展开侧边栏, and the pointer stays parked on its trigger, so the tooltip that opens never closes and the next click has nowhere to land. A person hits the same edge more briefly on the way down to the first nav row. It surfaced now because 新任务 moved into the rail in this branch; before, the row under that tooltip was not a target. A tooltip is an affordance, never a target. One unscoped rule — the popover renders in the top layer, where a mount-scoped selector is not reliably in the chain — and no !important, since StyleX declares no pointer-events of its own. While here: keep the type-scale comment in makaTheme.ts adjacent to `typography`. The materials block landed between them last commit. * feat(desktop): split the columns with a hairline and a quieter wash Sampled the two apps this shell is measured against, by scanning a pixel row across their column seam: Cursor nav #edeeef line 220 content #f6f6f6 ΔL 0.025 Codex nav ~#eeefef line 210 content #ffffff ΔL ~0.010 Maka nav #f0f0f0 (none) content #ffffff ΔL 0.045 Both draw a 1px rule, and both keep the wash far below what Maka was carrying. Maka's sidebar was never the darker one — it was the lightest of the three — but with no line it had to state the whole boundary through lightness, and a wash that wide reads as a gray area rather than a second panel. So: wash down to 0.025, plus the rule. The line says where the boundary is; the wash only says the two columns are different material. The rule is authored in makaTheme.ts, not as a product override, so AppShell keeps one paint authority — the theme emits it into its own @scope, and the color is the product's --border, the same product → Astryx direction as the materials. Astryx's own divider is not usable here: it is hardcoded to `variant === 'section'`, which also drops the wash entirely, and it is unconditional — it would draw down the 48px icon rail. Collapsed still drops both halves of the edge. The traffic lights measure ~62px against a 48px rail, so any column edge there cuts through the cluster. Live measurements (2x capture, pixel row across the seam): light nav 247 line 224 content 255 dark nav 17 line 38 content 23 * revert(desktop): drop the icon-scale pin, the 13px root is gone This branch pinned Astryx's four icon sizes to px because the product root was 13px and every rem-based icon landed at 81% — 13px next to the 16px lucide icons handed to the same slots. That was true when it was written and is not true now: #1857 landed in the rebase and removed the root pin outright, saying so in maka-tokens.css ("the root font-size stays at the browser default (16px) ON PURPOSE") for exactly the same reason this commit existed. So the pin restates what rem already computes. Measured in Storybook across three stories, with the rule and with it reverted: xsm 12, sm 16, md 20, root 16px, identical either way. Keeping it would leave 34 lines of CSS whose comment asserts a 13px root that main's own token sheet denies — the precise shape of rot the rest of this branch is removing. * refactor(desktop): key the column edge on the elevated variant `base` drew the rule under any variant, including ones that paint the columns differently or draw their own divider. Astryx keys component overrides by prop (`variant:elevated`), which emits `.astryx-app-shell-sidenav.elevated` — verified on the live node, which carries both classes. * test(e2e): lock the column edge in both sidebar states The bug this branch was asked to fix — no visible boundary between the session column and the content column — had nothing holding it. Neither half of the edge is greppable: the material comes from a generated theme file, the rule from a component override inside it, and a CSS grep passes on declarations that never reach the element. So assert the rendered result. Collapsed is asserted too, and asserts the opposite: both halves go away, because the traffic lights are wider than the 48px rail and any column edge there cuts through the cluster. Without that half a reviewer cannot tell the collapsed rail from a regression. Both halves ease between states, so the comparisons poll for the resting value — sampled mid-transition the color comes back as oklab(). Mutation-checked, rebuilding between runs: divider width 1px→0px fails on the border assertion, dropping the collapsed background override fails on the material assertion. * docs(desktop): stop describing tokens this branch deleted The dead-token sweep removed the declarations and left their prose, so the token sheet documented six things that no longer exist: the serif family with its whole section header, a "these three geometry tokens" focus-ring recipe that has two, a transform-amplitude note listing a hover scale nothing uses, and the brand/system-banner tint aliases whose comments dangled above the next section. Also drops the double blank line where --shadow-modal stood. The lucide stroke note is corrected rather than deleted: it justified keeping size per call site by pointing at icon rules in astryx-mount.css, and those were reverted on this same branch when the 13px root that motivated them went away. The reason still holds — Astryx sizes its slot icons through its own props — so the sentence now names that instead of a file that no longer says it. * fix(desktop): give the shell one paint authority and tests that can fail Five independent reviews of this branch converged on two defects it had introduced, both of the same kind: a comment asserting a guarantee the code did not provide. window-titlebar.spec.ts had stopped seeing the class of defect it exists for. The strip became an absolute overlay with default pointer-events, so it is the topmost hit at every point of the band and `elementFromPoint` returns it and nothing else; every control beneath silently failed the "is this reachable" precondition and was skipped. The assertion that used to catch this was inverted in the same change (`band.bottom <= contentTop` became its converse), leaving a form satisfied by exactly the geometry that creates the hazard. Mutation-proved: deleting either column's `padding-top` put 新任务, the workbar tab and 查看上一版本 under the overlay — real 30s click timeouts in three other specs — while this spec passed. The sweep now probes with `elementsFromPoint` minus the strip's own subtree, and splits the judgment: the strip's children are reachable and keep the app-region rules, anything beneath it is a defect outright, since no-drag shapes the OS rect while the click is already lost to the strip's hit-testing. That immediately caught the resize handle, which is positioned against the column box rather than flowing inside it and so kept its top 36px under the strip; it now starts below it. The Astryx neutral ramp was half-migrated. Remapping only the two tokens the shell reads left card, popover, muted and the hairline on the static light-dark() pair, and a ramp with one half palette-driven and the other frozen does not merely drift — it inverts: stock muted is L 0.222 against a --background of 0.18–0.24, so the recessive fill sat at or above the surface it is recessed into, in 8 of 11 dark palettes. Card, Code, ChatToolCalls, Slider and TableRow are the transcript, not chrome. The whole ramp now moves, onto the product's own stated hierarchy; --muted lands on foreground-at-5%, defined relative to whatever it sits on, so the inversion is unrepresentable rather than merely corrected. Also from the reviews, each verified before acting on it: - The tooltip hit-test rule was global, disabling the hover bridge Astryx documents as its WCAG 1.4.13 implementation for all 33 call sites. Scoped to the titlebar, the one place the layer cannot flip into empty space, with the concession stated. - Both new `!important`s rested on "StyleX is unlayered", which this app contradicts: astryx.css is one @layer astryx-base block imported into astryx-components, which product `components` outranks outright. Dropped. The `.astryx-button` colour rule went with them — Icon colours the glyph itself, so a button-level rule never reached it. - The content column had two paint authorities, and the new column-material test sampled the product's copy, so it would have passed with AppShell painting nothing. It samples AppShell's own element now, and the product's background is gone. - `.maka-sidenav-motion` mirrored a width SideNav already owns and then needed `width: 100% !important` to beat it. Deleted; the transition lives on the nav. Its React easing flag went too: cleared on transitionend, it never cleared when no transition ran, leaving every later drag animated — the exact fault it existed to prevent. The gate is now derived from the handle's own `data-resizing`/focus, so it cannot desynchronize. - `data-sidebar-state` had three writers and one reader; `data-modal-background- hidden` had no readers at all. One writer, on the frame. - The Storybook shell omitted `data-sidebar-state`, so the pixel-review surface rendered a collapsed state the app does not have. New locks, each mutation-verified to fail: the 48px collapsed rail (the number the dropped column edge argues from), the transition suppression during resize, and the column materials read off AppShell. Not done, and deliberately: the generated edge selector is a bare `.elevated` class, which Astryx's own themeProps notes advise against — the CLI emits it, so it is an upstream report, not a local fix.
Astro-Han
added a commit
that referenced
this pull request
Aug 2, 2026
* refactor(ui): define text roles as indivisible font shorthands A text style is one role, not four independent picks. #1857 converged the sizes and #1878 the leadings; both removed the divergence of the day without removing the ability to diverge again, because size, leading, weight and family stayed four separate choices at each of 348 call sites. The `font:` shorthand is the only mechanism CSS has that makes them inseparable. Its reset semantics are why it fits rather than a hazard to work around: it resets weight, style, variant and family, and the role token supplies them, so no site can move one and leave the others. Composed from Astryx's generated atoms only, so the ladder stays a pure function of typography.scale. Family is not part of the call-site triplet: `font:` requires a family slot that would override the one monospace authority in this file, so the slot reads --maka-font-family, rebound on :where(code, kbd, samp, pre) and inherited from there. Measured behavior-neutral: 12262 text elements across 86 surfaces (24 live app states, 62 Storybook stories) compared before and after, 0 changed. * refactor(ui): name a text role at every renderer call site 381 declaration blocks across 44 stylesheets now declare one `font: var(--maka-text-<role>)` and nothing else about type. Exactly one raw font longhand survives in the renderer — the monospace stack in maka-tokens.css that owns the family axis. Roles are assigned from MEASURED computed styles, not from declared values, so the ~30 selectors that took their weight from the UA or an ancestor keep it: 187 of the 385 blocks render in at least one of the 100 surfaces the harness reaches and were classified from what the browser reported there. Three things measurement caught that reading the CSS did not: - `.maka-hero h1` declares 28px but every probed instance is also `.maka-hero-empty-chat h1` at 20px, so the measured role described the other rule. Taking it would have shrunk the onboarding hero, the one surface that keeps the large scale and that no fixture renders. A measured role is now rejected when it disagrees with the block's own declared size. - Per-rule redundancy is not compositional: two rules that both pin 12px on one element are each individually removable and jointly load-bearing. Deleting on that basis drifted 85 elements 12px→14px, so this commit only migrates; deletion is decided against the final tree. - A role composed on :root freezes its family slot there — custom property var() references are substituted where DECLARED, and the resolved string is what inherits. A real <pre> whose --maka-font-family read Geist Mono still computed -apple-system. The role table is anchored on `:root, :where(code, kbd, samp, pre)` so code elements compose their own mono-family roles. `.maka-tool-name` also loses a latent defect: measured 16px against a 1.6667 ratio inherited from the supporting role, 26.67px, off the 4px grid. * fix(ui): settle every role that measurement showed diverging The 348-call-site migration was assigned from measured computed values, so the roles it picked were the ones the browser reported — which is exactly why re-measuring the migrated tree found the places where the measured value described something other than the call site's intent. Every change below is a diff bucket from that re-measurement, named and closed. Markup, not a call-site family. A slug, an id, a task key, a capability id, a memory path, a tool name, a summarized error — these are code, and the role table composes the code family for the code element group, so saying so in the markup is also what makes them render monospaced. 8 render sites move to <code>; `SettingRow`'s `mono` prop now picks the element. UA bold is not a role. 17 blocks selected strong/h*/th and declared no weight of their own, so they took the UA's 700 by accident. Naming a body or supporting role resets that to 400 — measured, 21 elements went 700→400 before this. They are section titles at 14px and 12px, which is heading-4 and heading-5: same size, same leading, weight 700→600 (Astryx has no 700). Page titles keep their size, not their weight. `.settingsPageHeader h2`, `.maka-module-main-header h2` and `.maka-plan-heading h2` were display sizes carrying a hand-rolled semibold. Matching on weight picked heading-1 and collapsed 66 page titles to 20px, level with the section headings under them. Display roles keep 25px/22px and take Astryx's display weight of 400. A family-only rule hides its size. `.maka-mcp-*` set only font-family, and its three selectors inherited two different sizes — 14px in the card body, 12px in the description. One role could not express that, so the rule splits: code role for the 14px pair, supporting for the description. `.maka-nav-kbd` opts back out of mono. It read `font-family: inherit` before; as a <kbd> it now composes the mono role table. It names the family axis instead — one declaration, and the role stays whole. Remaining measured delta is 206 of 16,063 elements across 86 surfaces, every bucket a convergence this PR intends: 66 page titles 600→400, 32 badges and 28 rail prompts 500→600, 21 strong 700→600, 30 family changes where mono was decoration rather than code, and 13 hand-rolled 12px/500 sites landing on supporting, their emphasis already carried by color or pill chrome. * test(ui): replace six typography guards with the one the triplet gives Size, leading, weight and family were four independent properties, so each needed its own guard: a ban on product `--leading-*` tiers, on literal ratios, on `!important` sizes and leadings, on em/rem multipliers, and a pairing check that resolved a block's size and leading through the generated theme to prove they named the same tier. Every one of those describes a way for the four to come apart. None of them is expressible now. A call site that cannot write a font longhand cannot write a literal ratio, an em multiplier, a forced size, or a mismatched pair — so they collapse into one assertion: outside the role table, every text style is `font: var(--maka-text-<role>)` and no stylesheet declares font-size, line-height, font-weight or font-family. `findLeadingPairingOffenders` is deleted rather than kept alongside it; two authorities on one question means the weaker one is the one people read. `findFontShorthandOffenders` inverts instead of tightening. The shorthand was banned because it hides bare weights and leadings; it is now the only legal form, because it is the one CSS mechanism that makes the four inseparable. The replacement also checks that every role NAMED is a role DEFINED — a var() that resolves to nothing makes the whole declaration invalid at computed-value time, so the element silently keeps what it inherits. That arm found a live defect: `--maka-text-display-1` was referenced by the hero and defined nowhere, and every other check in the file was green. Two things text cannot prove keep their own guards. The role table's shape is asserted in the contract (each role composed only from its own Astryx atoms, declared on both anchors, family axis pinned once per anchor). What those resolve to in a document is measured in e2e: a <pre> and a <div> naming the same role must agree on size, weight and leading and differ only in family — the substitution-timing fact that made the first, single -anchor role table wrong while reading perfectly in CSS. Seven mutations verified, each restored with cp: a longhand at a call site, a hand-composed shorthand, an undefined role, a role built from a foreign atom, a single-anchor table, a third family authority, and an Astryx atom rebound to a literal. Desktop 1323 pass, ui 239 pass. * refactor(ui): delete the size and family names the roles replace Six product tokens reached zero consumers when call sites stopped naming sizes and families, and check-dead-css found all six: --font-size-heading / stat / ui / caption, --font-sans, --font-default. They existed so a call site could say "this is the caption size" or "this is the mono stack" — a role says both, together with the weight and the leading, so a second set of names for the same rungs is exactly the second authority the convergence exists to remove. The role table now reads --font-family-code directly, and nothing in the renderer aliases an Astryx type name. The contract that pinned those aliases is replaced by the stronger thing they were approximating: there is no product name for a size or a family at all. --font-weight-bold survives as the top rung of the series the generated theme reads, in RESERVED_SCALE_TOKENS with that reason. Also caught rebasing onto #1876: `.maka-session-rename-input` declared `font: inherit` before its longhands, so migrating the longhands to a role left the reset winning and the role line dead. `findTextRoleOffenders` now counts font declarations before reading the last one — a block that declares a role and then overrides it reads as legal to any check that asks only what the browser uses. Eight mutations verified with cp. Measured against 105ec3e rather than the branch point, since #1876 moved the session rail under this work: 118 of 16,076 text elements across 86 surfaces change, every one a bucket this PR intends. * fix(ui): stop a role from resetting or shadowing what it does not name Review found four sites where naming a role changed more than the role. The `font:` shorthand resets `font-variant-*`, `font-style` and the feature settings along with the four axes it names. Within a rule every site already declared those after the role; across rules two did not. `.maka-stat-tile-value` held the tabular figures the tiles exist to align while the role sat in the `-outline` / `-filled` modifiers, so the role won and the digits stopped aligning; reordering makes the base rule the later one. `code.settingsReadOnlyValue` restates the declaration because it is the higher-specificity selector. A grouped rule that hands a selector a role, followed by that selector's own rule handing it another, leaves the first dead — and a later retune of the group moves every other member while this site silently stays put. `.plan-proposal-kicker` had already drifted that way, from the 12px tier to the 14px one; the 12px tier offers 400 and 600, and an eyebrow over a title takes the 600. `.maka-agent-graph-section-label` was the same shape without a size change. `data-mono` said in an attribute what the `<code>` element already says. * test: scan every renderer stylesheet for the text-role vocabulary The collapse to one guard was real for the shape it checks and a regression for what it can see. Three holes, each found by mutation: Scope. `readCallSiteCss()` skipped all of maka-tokens.css so the role table could write its one family longhand. That file is 1400 lines, ~40 of them ordinary component rules, and they were exempt from the only remaining guard — a literal ratio, an em multiplier, an `!important` size and a hand-composed shorthand all passed there while failing everywhere else. The exemption is now the one declaration that needs it: `font-family` on the code element group. Level. `--maka-text-*` and `--maka-font-family` are the vocabulary this branch introduced and nothing checked what they were bound to. One rule rebinding a role to `700 44px/1.05 Impact` re-chose all four axes while its call site still named exactly one role. Astryx's atoms were checked, but only against a leading digit, so `calc(2.5)` and `max(24px, 1rem)` walked through. Both arms are now one rule: a type token may be rebound to another token, never to a value. Parser. The hand-rolled brace walk dropped at-rule bodies whole, so a longhand inside a rule-nested `@media` — the shape Astryx itself uses for coarse pointers — was invisible; it also treated a brace inside a string as structural, and had lost the case-insensitivity its predecessor had. Replaced with postcss, which is already what Vite parses this same CSS with. A rule-nested at-rule is its own cascade context, so a responsive role swap is not counted as a duplicate. One arm is new rather than restored: a selector may be given a role by only one rule per cascade context. That is the cross-rule form of the dead-role bug the in-rule count already caught. The e2e step that read the four deleted size aliases now measures the roles instead. It had been failing since those names were deleted, and because a failing step aborts the test, every later step — the off-grid sweep and both two-anchor probes — had stopped running. The two-anchor probe also pins the three non-family axes to the supporting tier's own values; equality between the two probes alone would hold if the role stopped resolving for both. Mutation-verified (cp backup/restore, no git): longhand / literal ratio / hand-composed shorthand inside maka-tokens.css, role token rebound to a value, family axis rebound to a stack, atom rebound to calc(), longhand in a rule-nested @media, upper-cased longhand, a second role rule for one selector, role table on :root alone, family longhand moved off the code anchor — 11 of 11 caught, baseline clean. * refactor(ui): delete the last product weight name and record what the role costs `--font-weight-bold` had no consumer once call sites stopped declaring weight, and the generated theme reads only normal / medium / semibold. It survived on a check-dead-css allowlist entry whose stated reason was that the scale reads as a scale with it present — an aesthetic, not a constraint, and a product name for an axis the roles own. Four things the role table implies and did not say: the shorthand resets font sub-properties, so any the site needs must come after it; a 12px site that titles something below it takes heading-5 and everything else takes supporting, which is what makes the hand-rolled 500s reviewable; 97 call sites name a heading role for its weight rather than because they are headings, so a `weights.heading` retune moves them; and `large` / `heading-6` are absent because nothing uses them, not because the projection is partial. Astryx does support product roles through defineTheme component overrides — staying a pure projection of the vendor set is a choice, and now says so. Display on a page title is a deliberate override of Astryx's own guidance, recorded where the override is. Every heading role is weight 600 and tops out at 20px, so matching on weight would collapse a 25px page title level with the section headings under it. Also three comments naming tokens this branch deleted.
6 tasks
Astro-Han
added a commit
that referenced
this pull request
Aug 21, 2026
Two paragraphs in a chat turn sat 8px apart against 20px body leading, so a paragraph break was 0.4 of a line — narrower than the line break inside a paragraph, which is the one vertical distance a reader already knows. A long answer read as one slab. Take the block rung to 12px, which is Astryx's own paragraph rhythm at document density (spacingParagraphDefault, --spacing-3). The transcript keeps its own heading scale and its own 4px list rung and only stops being denser than the design system between two paragraphs. Where the fix does NOT belong, because the obvious reading is wrong: `density="compact"` in chat-turn.tsx has not owned transcript block spacing since #2348. That table zeroes every Astryx margin on the document's children and declares the four gaps itself, in @layer components — a Markdown density token, or a makaTheme.ts override of `astryx-markdown-paragraph`, lands in an earlier layer and is inert. `--md-gap-block` is the only authority, so the call site now says so rather than leaving the next reader to re-derive it from commit history. One rung moves, so code blocks and blockquotes move with it: they are already carried by the same `> * + *` step (Astryx skips its codeblock wrapper whenever `components.code` is set, which Maka always does), and splitting a per-element rung out would re-introduce the multi-value spacing this table exists to collapse. What that costs is contrast at the top of the ladder — the section step falls from 2x the block gap to 1.33x — and the heading rungs stay put anyway, because heading spacing is #1857's decision and not this change's business. Measured in Storybook against live computed styles, before and after, on Product/Markdown → TranscriptTurn and Product/Shell Official AppShell → Native Conversation: list rows 4px 4px unchanged blocks 8px 12px paragraph, list, quote, code block section 16px 16px unchanged (h3-h6) chapter 24px 24px unchanged (h1, h2) Daily Review renders at density="default" and is out of the selector, so its ladder is byte-for-byte what it was: 12/12 paragraphs, 16/16 quote and table, straight from Astryx. Generated-by: Claude Code
Astro-Han
added a commit
that referenced
this pull request
Aug 21, 2026
Transcript paragraphs sat 8px apart against 20px body leading — a paragraph break narrower than the line break inside a paragraph. Raise the block rung to 12px, which is Astryx's own `spacingParagraphDefault`, so the transcript stops being denser than the design system between two paragraphs while keeping its own heading scale (#1857) and list rung untouched. The spacing is owned by the compact rhythm table in packages/ui/src/styles.css, not by Astryx's density tokens: the table zeroes Astryx's margins outright and declares every gap itself, so retuning `--md-gap-block` is the only lever. Adds two contract assertions for the ways that table can break silently — the runtime DOM hooks its selectors need still being emitted, and the ListItem control padding still being neutralized (the mechanism that inverted the ladder in #2348). Declared values are deliberately not pinned; a wrong number is visible the moment anyone looks at a transcript. Review caught a third assertion claiming compact markdown was transcript-only. It was already false — the desktop Artifact Preview has rendered `<MarkdownBody density="compact">` since #2506 — and passed anyway. Removed rather than widened, since holding it means a library test grepping application source. The shared surface and the unguarded typography coupling are documented next to the heading rules instead. Generated-by: Claude Code
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The main conversation renders with a broken type hierarchy because Maka's density is expressed as
html { font-size: 13px }. That is not a type scale — it is an implicit ×0.8125 multiplier on every rem in the document, including the radius and spacing constants Astryx compiles against a 16px root. Body copy was then pinned back on the Theme wrapper (astryx-mount.css) to undo the multiplier for prose alone, which left every other tier shrunk: supporting text at 9.75px instead of 12, h1 at 19.5 instead of 24,--radius-elementat 8px instead of 10. One intent, two contradicting expressions, and a compensating patch between them.This moves the intent to where Astryx expects it —
typography.scaleinmakaTheme.ts— and then retunes the values against what shipped agent UIs actually do.Consolidation. The four hand-written tiers (
--font-size-caption/ui/heading/stat) become aliases of the generated ladder instead of independent values. The root returns to the browser default and the compensating pin is deleted. Four more parallel authorities go with it: the font stacks (Astryx's neutral default leads with Figtree, which Maka does not bundle and whose stack carries no CJK face), Astryx's reset hard-coding a monospace stack on:where(code, kbd, samp, pre)that never consults--font-family-code, the staleremliterals in product CSS and TSX, and elevenemheading multipliers across hero, onboarding, settings and the module pages. After this the renderer has no independent font-size authority left, and a contract test says so.Retuning. With one authority in place, the ladder moves from
{base: 13, ratio: 1.15}(a faithful reproduction of the existing tiers, chosen so the consolidation's visual delta stayed attributable) to{base: 14, ratio: 1.125}. Basis: the shipped bundles of Cursor 3.14.7 (CDP against the live Agents window), Claude Code's desktop surface (the Epitaxy layer inside Claude.app) and Codex desktop (openai-codex-electron). All three sit at 14px body; all three keep secondary text at 12–14px, none near the 11px Maka used for reasoning and tool rows. 1.125 is the only ratio that moves base to 14 while keeping 11 and 20 on the ladder, and is what Astryx's ownexpandTypeScaleheader recommends for "Dense/functional". Body leading recomputes to 20px — the same absolute leading as before, so the type grows without the paragraph loosening.Transcript-scoped policy, all taking values from the ladder:
--text-supporting-size/--text-supporting-leadingon the trigger — Astryx's supporting atoms read those two tokens, so inheritance reaches every span that opts into the role at any depth, with no!importantand no dependency on child order.lg, h2–h6 at body, all weight 600, all on one baseline. Astryx's ladder is a document scale and a turn is not a document: an agent emits##every few lines, and 1.4× per level shreds one reply into unrelated slabs. Cursor, Claude Code and Codex all flatten here relative to their own document styles. Scoped to.maka-turn; Daily Review, settings and docs keep the full ladder.density="compact", Astryx's own answer to heading spacing tuned for pages rather than turns.astryx-tokensalso moves afterastryx-componentsin the cascade order — see Review focus.Verification
Measured in the live app via CDP computed styles (Playwright + Electron, fake backend), same transcript across runs:
htmlfont-size--font-size-caption--radius-elementFigtree(unbundled)-apple-systemui-monospace(reset)"Geist Mono Variable"New guards. Three declarations hold the scale up and every one of them was silent — mutating each left all 24 existing CSS-governance tests green:
apps/desktop/src/main/__tests__/type-scale-contract.test.ts(8 cases) pins the unpinned root, the layer order, the four aliases, the ladder rungs they point at, the monospace routing, the turn-scoped heading flattening, the role-token rebind, and the absence ofem/remfont-sizes. Six mutations were run against it; each fails exactly one case.apps/desktop/e2e/type-scale.spec.tsmeasures what text cannot prove: custom properties resolve by tree distance while rules resolve by layer, and the two disagree at:root. It probes the tokens at:rootin px, so it sees what a portaled Astryx component sees. Reverting the layer order turns--font-size-headinginto 17px — Astryx's neutral value — and the spec fails on it. Verified by mutation.astryx:theme -- --check.makaTheme.tsis the type-scale authority and nothing stopped it from drifting from the artifacts it generates.Checks:
@maka/desktopunit (1314 passed),@maka/uiunit (239 passed), repo-widetypecheck(clean),lint,format:check,check-a11y,check-console,check-dead-css --check,astryx:theme -- --check.Playwright: 70 passed, 0 failed. (An earlier revision of this branch reported 4 failures; those were #1860's, and
bb44e37e6fixed them onmain.)Rebased onto
mainafter #1860 and #1862. Two conflicts, both in files this PR rewrites: #1862's user-bubble geometry deletion (kept as-is, alongside this PR's baseline-grid token) and the--font-size-base: inheritpin inastryx-mount.css, which this PR deletes because the name it was unshadowing no longer exists on the product side.#1860 also added a session-list section-title pin whose stated reason was the 13px root — Astryx's supporting/sm collapsing to 9.75px. With the root back at the browser default, the ladder says what the pin was trying to say, so the pin is gone:
Two regressions were caught and fixed rather than shipped:
.modelPickerProviderMark— converting product CSS offremhad swept up the one place the unit is load-bearing; refactor(ci): split Storybook from e2e and demote floor layout to CSS contracts #1854 added a contract test asserting a fixed 1rem square. Fixed in5d943e8a9, now the one deliberateremin product CSS with a comment saying why.> span:not(:last-child)reads as "every span but the chevron", butChatReasoningwraps its label, duration and preview one<div>deeper, so the rule enlarged the leading icon and left the reasoning text at supporting size — the single row this PR exists for. Replaced with the token rebind described above.What three reviews changed
Two fresh-eye agents and Codex reviewed this independently. They converged, and the result is
251423edd. Everything below was measured in the live app before being acted on.One behaviour defect.
density="compact"was hardcoded inMarkdownBody, whose other caller is the Daily Review panel — so the review got transcript block spacing with document heading sizes, contradicting the very argument that scopes the heading flattening to.maka-turn. Density is now the caller's choice, defaulting todefault.Six comments asserted something false. The load-bearing one: this PR claimed the old layer order merely leaked at
:rootand that the theme "still won inside the Theme wrapper by tree distance". Probed onmainat both<html>and the inner wrapper,--font-size-lgis 13.8125px and--font-size-xlis 16.25px — Astryx's neutral values. The generated theme was applying nowhere at all. Also corrected: Astryx's spacing and radius tokens are px and never moved (what is rem is its Icon atoms — see below); the disclosure rows measured 9.75px onmain, not the 11px claimed; the body leading went 21px → 20px rather than staying put; and 1.125 is the natural ratio, not the only one.An undocumented improvement. The 13px root was shrinking Astryx's entire icon set, which it authors in rem as "the px-equivalents at a 16px root":
astryx-icon.mdastryx-icon.smastryx-icon.xsmFive test holes, each proved by mutation.
@maka/ui/styles.cssreaches the renderer through a bare specifier that the CSS import expander skipped along withnode_modules, hiding ~30font-sizedeclarations from every scan.--maka-line-bodyhad no guard at all — deleting it was green. The root-pin ban missedhtml[data-os="darwin"] { font-size: 13px }. The unscoped-heading and!importantbans read one file each. And the monospace routing was text-asserted on the false grounds that it "follows arithmetically" — it is a layer-order question, now probed live.Rejected after checking. That the model-picker mark's
1remis an unnoticed 23% growth: it is an icon box, and 1rem is Astryx'ssmicon at a 16px root, so 13px was the anomaly. Declined: a live ChatReasoning probe — no fixture renders one, and seeding it would change a shared scenario every spec sees; the gap is stated in the spec rather than left implicit.Review focus
The cascade-order change in
cascade-layers.cssreaches beyond typography, and an earlier revision of this description wrongly said it could not.It is required:
astryx.cssships the neutral defaults on:root, so a theme layered before it can never override them there, and the product aliases resolve exactly there. It is also the order Astryx itself prescribes (README.md, integration snippet:reset, theme, base, astryx-base, astryx-theme, components) — the old order was the deviation. Reverting it puts--font-size-headingback to Astryx's 17px, measured.But
maka.csscarries 67.astryx-*class rules besides its tokens, and Astryx components are product markup. "67 rules change" would be wrong, though — what decides is whether a declaration is CONTESTED (both sides set it, so layer order picks) or UNCONTESTED. Compared declaration by declaration: 44 are contested but resolve to the same value (35 Heading/Text atoms, 9 categorical Badges), 12 are uncontested and were already in effect, and 11 actually move — the destructive Button, 5 semantic Badges, 4 StatusDots, the info Banner background. Two spot-checked live:rgb(165, 12, 37)rgb(255, 255, 255)rgb(250, 206, 203)rgb(165, 12, 37)rgb(38, 38, 38)rgb(0, 116, 226)Badge tones and status dots move the same way. These are the neutral theme's own values, generated by
astryx theme buildfrom the base Maka extends — the theme was simply never taking effect. Card and Section padding are NOT affected, despite looking like they would:--astryx-card-paddingand--astryx-section-paddingare theming hooks Astryx never declares itself, so those custom properties were unopposed under either order.Deliberately out of scope: a user-facing text-size setting. All three benchmarked products offer one (Cursor 4 steps, Claude Code 3, Codex continuous 11–16) and Maka does not; that wants its own issue and a mechanism for deriving spacing from the size, not just the size itself.
Deliberately preserved: the
rem → pxconversions freeze the geometry the app ships today rather than restoring each length's pre-0.8125intent. Restoring intent would move those lengths by +23% and turn a typography PR into a relayout.