') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); refactor(ui): name every renderer line-height after its Astryx role by Astro-Han · Pull Request #1878 · apache/maka · GitHub
Skip to content

refactor(ui): name every renderer line-height after its Astryx role - #1878

Merged
Astro-Han merged 4 commits into
mainfrom
refactor/ui-leading-role-tokens
Aug 2, 2026
Merged

refactor(ui): name every renderer line-height after its Astryx role#1878
Astro-Han merged 4 commits into
mainfrom
refactor/ui-leading-role-tokens

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Summary

Leading had three vocabularies: four product semantic ratios in maka-tokens.css, Astryx's per-role tiers, and --maka-line-body. Two of them disagreed about body copy — 21px against 20px at the 14px tier — and both looked deliberate. #1857 converged size and left this deliberately for its own PR.

They were not different numbers but different methods, which is why retuning could not work. A unitless ratio is one value spread across every tier it is used at; Astryx snaps each tier's line box to the 4px grid and reports the quotient (expandTypeScale.ts: target 1.5 under 20px, 1.4 through 31px, 1.25 above, snapped, floor size + 4), so 1.4286 is 20/14 rather than a chosen ratio. No single ratio sits on the grid at 12, 14, 16 and 22px at once. Astryx removed the same named tiers in its own v0.0.8 codemod, its docs say "you should never need to set line-height manually", and its Badge, Kbd, Button and Item carry role leadings inside boxes sized off the same 4px scale — which is why it ships no line-height: 1 escape hatch and this file no longer needs one.

So the four tiers are deleted and all 130 declaring sites — including the eight bare literals in packages/ui/src/styles.css — name the role sized like the size they declare. The leading is a pure function of size, so the role name is chosen for readability and carries no visual consequence.

Two further changes came out of measurement rather than the stylesheets:

  • Astryx's reset puts line-height: 1.5 on :where(html), so every element that declared no leading inherited it — measured 21px at the body tier across hundreds of nodes, owned by no stylesheet in this repo. body now declares the body role next to the body size it already had.
  • A unitless leading still inherits as a ratio, so an element that inherits one and overrides its own size lands off the grid again. Measured, .maka-attachment-file-content held one 1.25 for a 14px name and a 12px size at once, and .maka-onboarding-setup header h1 took a 16px rule's leading at 18px. Leading therefore belongs on the element that sets the size, and the 212 blocks that declared a size without one now declare both.

One deliberate move worth flagging: the transcript's Markdown h1 was pinned to --maka-line-body so a turn read as one rhythm. At 16px that 20px box is the 14px tier's, one step tight for the tier the heading sits at, so it takes --text-large-leading (24px) — the next step of the same grid, not a different one.

Refs #1875 — the follow-up that makes the call site name a role instead of restating values, which cannot ride along here because the CSS-native form resets weight and family.

Verification

npm run lint, npm run format:check, npm run typecheck, npm --workspace @maka/desktop run test (1321 pass), npm --workspace @maka/ui run test (239 pass), npm --workspace @maka/desktop run e2e (71 pass), node scripts/check-dead-css.mjs --check, npm run astryx:theme -- --check — all green.

One earlier full e2e run failed 4 specs (providers ×3, quote-companion) inside the window fixture; both subsequent full runs passed 71/71, and each of those specs passes in isolation. Flagging it rather than hiding it — nothing in the diff touches those paths.

Measured, not inferred. A temporary harness booted the existing fixtures and swept every rendered element with its own text against Astryx's grid rule, in six scenarios:

off-grid text elementsdistinct shapes
before54450
after the 130 declaring sites26020
after body + the 212 size-only blocks11

The remaining one is upstream: Astryx's own TextArea counter row declares --text-supporting-size with no matching leading (TextArea.tsx, against TabMenu.tsx which declares both).

Live perturbation — applying the proposed leading in the running document and remeasuring the box — is what separated the chips that grow from the ones whose height is pinned: .maka-plan-card-run did not move, .maka-message-time-inline grew 8px inside a parent that did not, .settingsPageHeader h2 moved +4.5px with its parent. No padding was touched; where a box grows, it grows.

Screenshot diffs (ImageMagick compare -metric AE, six scenarios captured before and after from the live app): permission-settings 2.7% of pixels, plan-reminders 1.7%, bot-settings 0.9%, sandbox-boundary 0.4%, skills 0.01%, transcript 0 px — the transcript already read Astryx's leading, which is exactly what the token comment claimed before this PR and is now confirmed rather than assumed.

Every guard added here was mutation-verified and restored: redefining a --leading-* tier, referencing one, a literal ratio, a leading from the wrong tier, a size declared with no leading, and body losing its leading each fail the contract; moving the document default off the body tier and giving a rendered element an off-grid leading through a block with no font-size each fail e2e while the contract stays green — which is what shows the two guards are not covering for each other.

imageimageimageimage

After review

Three independent reviews ran against the first two commits. None found a rendering defect; all three converged on the same hole in the guards, and one found a factual overreach in a comment. Both are fixed here, each mutation-verified.

The pairing contract was one-directional. It checked size → leading and deferred leading-without-size to the e2e grid sweep. Measured, that sweep renders 129 elements with their own text out of the 323 classes whose rules declare a leading, and none of the three leading-only blocks in the tree appear in it — so the class had no coverage in either place, while maka-tokens.css bans it in prose. The check is now bidirectional and the three blocks are gone: .maka-stat-tile-value was dead (stat-tile.tsx always appends -outline or -filled, and both declare their own pair), the other two were redundant with what they inherit. Rendering is unchanged — e2e stays 71/71 with the sweep still empty.

Five further bypasses were demonstrated and closed, each with a mutation that fails now and passed before: an Astryx leading role rebound to a literal (the likeliest regression shape, since token rebinding is this branch's own mechanism); font: 12px/1.9 sans smuggling both halves past every longhand scan, through a backstop helper that had no caller anywhere outside its own unit test; font-size: 18px skipping the pairing check entirely — the exact drift the test's own comment cites; the scanner reading the first declaration where CSS takes the last; and parseCssBlocks matching only innermost blocks, so one nested rule erased its parent's declarations from every scan built on it. line-height : 1.9 was also unseen, because biome excludes both directories and nothing normalizes the whitespace.

The e2e sweep recomputed Astryx's floor wrongmax(snapped, size + 4) against computeLeading's max(snapped, ceil((size + 4) / 4) * 4). Recomputing rather than copying a table only tracks an Astryx change if it is the same arithmetic; at a 9px tier the two disagree, and the sweep would have accepted 13px and rejected Astryx's own 16px. Verified identical across 8–40px after the fix.

One comment claimed more than the source supports.maka-tokens.css said Astryx ships no line-height: 1 escape hatch. It ships exactly one — the sortable table's 10px rank glyph, behind a lint exception reading "no token for lineHeight:1 (tight badge)". The comment now names it and says why nothing here is in that position, instead of resting on a false absolute. The e2e vendor-gap note also listed one TextArea gap where there are two: under (pointer: coarse) it raises the input to max(1rem, --text-body-size) without raising the leading, rendering 22.86px at 16px where the grid wants 24. Both are upstream, and no scan in this repo covers vendor component styles.

Considered and rejected: declaring an absolute 20px leading on body and deleting the ~326 declarations at the 12px and 14px tiers, which today share one 20px line box. The identity is a coincidence of the current scale — 12 × 1.5 and 14 × 1.5 both snap to 20 — not an invariant, so an Astryx retune would silently mis-render every site that had no declaration of its own to move. Naming the role at each site is what makes a scale change propagate. It also makes a leaf's correctness independent of what any ancestor declares, which inherited length cannot.

Review focus

This moves 342 declaration blocks. The direction is uniform: at the 14px body tier text tightens 1px, and everywhere else it loosens — 12px body copy +2px, 12px dense zones +3.5px, 16px titles +4px, the large headings +4.5 to +5px, and the former line-height: 1 chips +6 to +8px inside boxes that mostly do not move. If any of those reads as too airy, the argument to have is about Astryx's density opinion, not about this PR's mechanism.

Leading had three vocabularies: four product semantic ratios in
maka-tokens.css, Astryx's per-role tiers, and --maka-line-body. Two of
them disagreed about body copy — 21px against 20px at the 14px tier —
and both looked deliberate.
They were not different numbers but different methods. A unitless ratio
is one value spread across every tier it is used at; Astryx snaps each
tier's line box to the 4px grid and reports the quotient, so 1.4286 is
20/14 rather than a chosen ratio. No single ratio sits on the grid at
12, 14, 16 and 22px at once, so the product tiers could not be retuned
into agreement — only replaced. Astryx removed the same named tiers in
its own v0.0.8 codemod, and its Badge, Kbd, Button and Item carry role
leadings inside boxes sized off the same 4px scale, which is why it
ships no `line-height: 1` escape hatch and this file no longer needs
one.
The four tiers are deleted; all 130 declaring sites (including eight
literals in packages/ui) name the role sized like the size they
declare. Measured in the live app, the leading is a pure function of
size, so the role name is chosen for readability and carries no visual
consequence.
Two further sites came out of measurement rather than the stylesheets.
Astryx's reset puts `line-height: 1.5` on `:where(html)`, so every
element that declared no leading inherited it — 21px at the body tier
across hundreds of nodes; `body` now declares the body role next to the
body size it already had. And a unitless leading still inherits as a
ratio, so an element that inherits one and overrides its own size lands
off the grid again: `.maka-attachment-file-content` held 1.25 for a
14px name and a 12px size at once. Leading therefore moves onto the
element that sets the size, and the 212 blocks that declared a size
without one now declare both.
Measured before and after in the live app: off-grid text elements
across six scenarios fall from 544 to 1, the remaining one being
Astryx's own TextArea counter row, which declares --text-supporting-size
with no matching leading upstream.
Refs #1875
Two guards, deliberately non-overlapping, both mutation-verified.
The contract test reads co-located declarations: no product --leading-*
may be defined or referenced, no line-height may be a literal ratio, and
every block that declares a font-size must declare the leading of that
size's own tier. The pairing resolves both sides through the generated
theme rather than a copied table, so an Astryx scale change moves the
expectation instead of failing it. It is one-directional on purpose — a
block may declare a leading and inherit its size, and no text says what
it inherits.
The e2e step measures what text cannot: the resolved document. It sweeps
every rendered element with its own text and recomputes Astryx's grid
rule from the element's own computed size, which is where an inherited
ratio meets an overridden size — the failure `.maka-attachment-file-
content` and `.maka-onboarding-setup header h1` both had. A second step
pins the document default leading, which lives in Astryx's reset and no
stylesheet here used to own.
Mutation-verified, each restored after: redefining a --leading-* tier,
referencing one, a literal ratio, a leading from the wrong tier, a size
declared with no leading, and body losing its leading each fail the
contract; moving the document default off the body tier and giving a
rendered element an off-grid leading through a block with no font-size
each fail e2e while the contract stays green, which is what shows the
two are not covering for each other.
Three independent reviews of this branch converged on the same gap: the
pairing contract checked size -> leading only, and deferred
leading-without-size to the e2e grid sweep. Measured, that sweep renders
129 elements with their own text out of the 323 classes whose rules
declare a leading, and none of the three leading-only blocks in the tree
appear in it. The class had no coverage in either place, while
maka-tokens.css bans it in prose.
Make the pairing check bidirectional and delete the three blocks it
finds. `.maka-stat-tile-value` was dead — stat-tile.tsx always appends
`-outline` or `-filled`, and both declare their own pair; the other two
are redundant with what they inherit. Rendering is unchanged: e2e is
71/71 with the grid sweep still empty.
Close the bypasses the same reviews demonstrated, each mutation-verified:
- an Astryx leading role rebound to a literal re-established a second
authority for a whole subtree, and this branch's own mechanism is
token rebinding, so it is the likeliest regression shape
- `findFontShorthandOffenders` had no caller anywhere outside its own
unit test, though it documents itself as this contract's backstop;
`font: 12px/1.9 sans` smuggles both halves past every longhand scan
- `font-size: 18px` skipped the pairing check entirely, which is the
exact drift the test's own comment cites
- the scanner read the first declaration where CSS takes the last
- `parseCssBlocks` matched only innermost blocks, so one nested rule
erased its parent's declarations from every scan built on it
- biome excludes both directories, so `line-height : 1.9` was unformatted
and unseen
Add the parseCssBlocks unit tests whose absence hid the nesting bug, and
replace the e2e scope comment's claim that the pairing contract covers
this repo-wide with the measured numbers.
External review found the e2e grid sweep recomputing Astryx's rule with a
different floor: `max(snapped, size + 4)` where `computeLeading` uses
`max(snapped, ceil((size + 4) / 4) * 4)`. The point of recomputing rather
than copying a table is that an Astryx scale change moves the expectation
with it, and that only holds if it is the same arithmetic. At a 9px tier
the two disagree — the sweep would have accepted 13px and rejected
Astryx's own 16px. Verified identical across 8-40px after the fix.
Two comments asserted more than the source supports:
maka-tokens.css claimed Astryx ships no `line-height: 1` escape hatch. It
ships exactly one — the sortable table's 10px rank glyph, behind a lint
exception reading "no token for lineHeight:1 (tight badge)". Name it and
say why nothing here is in that position, rather than resting the
argument on an absolute that is false.
The e2e vendor-gap note listed one TextArea gap; there are two. Under
`(pointer: coarse)` TextArea raises its input to `max(1rem, body)` without
raising the leading, rendering 22.86px at 16px where the grid wants 24.
Both are upstream, and no scan in this repo covers vendor component
styles — worth stating where the first-party guard stops.
@Astro-Han
Astro-Han marked this pull request as ready for review August 2, 2026 05:22
@Astro-Han
Astro-Han merged commit 77b1d4c into mainAug 2, 2026
5 checks passed
@Astro-Han
Astro-Han deleted the refactor/ui-leading-role-tokens branch August 2, 2026 05:22
Astro-Han added a commit that referenced this pull request Aug 2, 2026
#1878 made every line box name an Astryx role; it did not stop a line box
from deciding a box. Where a chip declares no height, leading and box
height are one decision, so the next leading change is a layout change.
Each single-line chip now takes its height from `--h-control-*` and centres
its line box, which is the Astryx Badge/Kbd identity expressed with maka's
own control ruler. Tiers were chosen by measuring what each chip aligns
with, not by rounding: the two settingsUsage rows sit beside a 32px
TextInput and Selector, the sidebar update button under 32px nav rows.
Measured live with CDP by bumping each element's own leading to 40px and
remeasuring the box:
- `.maka-message-time-inline`, `.maka-nav-kbd`, `.maka-plan-card-countdown`
held 20px and doubled to 40px; pinned to `--h-control-xs`, they move 0.
- `.settingsUsage{DetailToggle,RecordCount}` had `min-height: 34px`, which
the same bump still pushed to 40px; now 32px (`--h-control-lg`).
- `.maka-composer-revision-notice` 26px -> 24px, its cancel button
18px -> 20px (an 18px box was rendering a 20px line box, and `display:
block` left it uncentred), `.maka-sidebar-update-button` 34px -> 32px.
Two rows are deliberately left unpinned, which is the opposite of what
#1879 assumed: it read `.maka-plan-card-run` as already pinned and offered
it as the shape for the rest. It declares no height at all, and it must not
gain one — measured at the 480px window floor, a long run message takes it
to 220px. `.maka-plan-card-schedule` wraps to 42px at the same width. A
pinned height is only sound for content that cannot wrap.
`.maka-plan-card-countdown` is not in #1879's table; it was found by
measuring rather than by reading the table.
`.maka-composer-skill-trigger-count` keeps its 16px literal: it is a
`pointer-events: none` overlay on a button's corner, aligned with no
control, so no neighbour names a tier and the nearest rung would grow a
decorative badge by 25%. It is already pinned, which is what #1879 asked
for. The reasoning sits at the rule.
The maka-tokens.css note that tracked this as a follow-up is now the
measured result, and it corrects one detail: the `lineHeight: '1'` tight-badge
exception it cites lives in `@astryxdesign/core`, so the first-party CSS
contracts never had it in reach and no exemption is needed to keep it legal.
First-party code holds no inline `lineHeight` at all.
Astro-Han added a commit that referenced this pull request Aug 2, 2026
#1878 made every line box name an Astryx role; it did not stop a line box
from deciding a box. Where a chip declares no height, leading and box height
are one decision, so the next leading change is a layout change.
Every single-line chip now takes its height from `--h-control-*` and centres
its line box — the Astryx Badge/Kbd identity expressed with maka's own
control ruler. Measured live with CDP by bumping each element's own leading
to 40px and remeasuring, both the box and its parent.
The guard is derived, not enumerated. An earlier revision of this branch
listed eight selectors, which can only fail for a chip someone remembers to
add: measured against that list, fourteen identically-defective chips were
shipping, one of them sixteen lines from a selector the list did contain.
The contract now scans for the visual definition of a chip — pill radius,
its own padding, a role leading — and requires a `--h-control-*` height,
with a named exemption set that is shorter than the table it replaces.
Two probe defects found while measuring, both of which had produced wrong
conclusions:
- A `display: inline` chip's own border-box height is its font content area
and does not track leading, so a probe reading only the chip reports "did
not move" while its line box pushes the row open. Seven chips were
misread this way. The parent delta is the measure that covers every
display type.
- `.maka-plan-card-run > span` .first() is the Astryx Badge, not the run
message. The earlier "220px with a long message" was an artifact of
blowing that nowrap badge out sideways, and stayed 220px regardless of
content length. The row does grow with its message, measured on the right
element, so the conclusion holds and the evidence for it did not.
Boxes deliberately left unpinned, because a pinned height is only sound for
content that cannot wrap: `.maka-plan-card-run`, `.maka-plan-card-schedule`,
`.settingsMemoryBackupCandidate` (its buttons flow to a second line inside
the pill) and the code-block `pre`. A contract guards that too, repo-wide
and aware of `block-size`, so the ban cannot be evaded from another file.
`.maka-sidebar-update-button` is fixed at its owner instead: it is an Astryx
`Button`, so it moves to `size="md"` and drops its CSS height entirely.
Astryx sizes controls off `--size-element-*` (28/32/36), which maka's
`--h-control-*` shadows at shifted names — a relationship nothing recorded
until now, and the reason a chip pinned to `--h-control-lg` beside an Astryx
control at `--size-element-md` lines up by coincidence rather than by
derivation. maka-tokens.css now states it.
Tests assert containment as well as stability: a pin converts "the row
grows" into "the content spills", and a box that clips satisfies a
"did not move" assertion identically to one that contains.
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.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Astro-Han