Uh oh!
There was an error while loading. Please reload this page.
feat(desktop): collapse the session rail into a continuous 48px icon rail - #1876
Conversation
c9905a7 to
eebd1d4Compare…erlay 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.
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.
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.
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.
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.
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.
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.
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
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.
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
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.
`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.
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.
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.
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.
eebd1d4 to
054a119CompareUh oh!
There was an error while loading. Please reload this page.
It was instrumentation for diagnosing the four post-#1876 reports against the live app, committed by mistake. Its own header calls it throwaway.
* 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.
…ping inline (#1892) * fix(desktop): ease the rail collapse, even the hairlines, switch grouping inline Four reports against the shipped 48px rail. Only the first is a regression from #1876; the other three were already there and are fixed here because they are the same surface. The collapse stopped animating. The transition sat on the nav, and SideNav swaps its own root element type across the toggle — expanded it wraps the <nav> in a div for the overlay resize handle (`showResizeHandle = isResizable && !collapsed`), collapsed it renders the bare <nav> — so React unmounts that subtree and mounts a fresh one with no start value to interpolate from. Measured live: every one of 144 sampled frames after the click was already 48px. The ease moves to `.maka-sidenav-motion`, a product wrapper outside SideNav that is the same node before and after; 30 distinct intermediate widths now. `width: 100% !important` on its child is load-bearing rather than leftover — SideNav sets an inline px width, and inline style outranks any layered rule without it. The rule under 定时任务 sat flush against it (nav item bottom 140, rule top 140) while the footer's rule cleared its icon by 9px, because the footer line is drawn on the sticky-bottom host, which owns padding, and this one is the last child of `topContent`, whose wrapper is a bare block with neither gap nor padding. Group labels came from Astryx's `supporting` tier, a step below the rows they label; at Maka's 14px body that reads as a caption, and CJK loses more apparent size at 12px than Latin. Rebind the tier on the section — both size AND leading, or the supporting multiplier lands the line box at 23.33px, off the 4px grid — so 会话 / 最近 / 置顶 sit on the rows' step with weight and color carrying the hierarchy. Grouping moves from a dropdown of radio items to an inline SegmentedControl, the control Astryx spends on exactly this in its own file-explorer and ide templates. Both axes stay on screen, the current one is readable without opening anything, and the start slot stops carrying a radio dot that repeated what the row already said. New locks, each mutation-verified to fail: the ease is declared on the element that survives the toggle and suppressed during resize, the rule clears the nav item above it, and a group label matches its rows' size at a heavier weight. * fix(ui): draw the rail's two hairlines the same way and icon the session menus The top hairline was an Astryx Divider inside topContent: --color-border is twice the footer rule's alpha, and it sat inside the sticky shell's 8px inline padding, so it stopped 8px short at each end (x=8 w=244 against the footer's x=0 w=260). Read together, one was a separator and the other a box edge. Draw it as the sticky zone's border-bottom instead — same 1px, same alpha, same full width, one element and one component fewer. Give every session and project more-menu item a leading icon, so the menu reads as a list of actions rather than a list of strings. Narrow the ease gate to :has([data-resizing]). The :focus half answered "the handle was used at some point", not "the handle is being used now": with focus parked on the separator, transition-duration measured 0s at the collapse toggle. Without it, keyboard resize at a 60ms key repeat trails the target by at most 13px. * fix(ui): rest the composer card above the window edge ChatLayout's density sets the dock's own gutters, and `compact` spends spacing-2 on them: 8px between the composer card's rounded bottom edge and the window frame, measured identical at 860, 617, and 500px window heights. The card read as pushed against the frame rather than resting above it. `balanced` spends spacing-3 on the same gutters and lengthens the fade over the transcript to match (blur layer 80px → 100px, mask ramp 24px → 36px). The message-area styles are byte-identical between the two tiers, so this moves the dock and nothing else. Astryx's own ai-chat template runs spacious, a step further still. The gutter comes from a StyleX atom inside ChatLayout, invisible to a CSS read, so the lock measures it live at three window heights — a short window is where a wrong flex contract would let the card slide into the edge. * chore: drop the throwaway sidebar diagnostic harness It was instrumentation for diagnosing the four post-#1876 reports against the live app, committed by mistake. Its own header calls it throwaway. * fix(ui): scope the rail type tier to its header and lock what the tests missed Five reviews on this branch converged on two real defects and a set of comments that documented decisions the code no longer made. The type-tier rebind sat on the SideNavSection root, and SideNavSection wraps the entire list body in the same element as its title. Custom properties inherit, so it reached every row: measured live, the project session-count Badge rendered at 14px instead of 12px, and `.maka-list-row-stale-pill` would have taken a 17.14px line box on its 12px font — off the same 4px grid the comment invoked three paragraphs earlier. The comment claimed the override "cannot reach past the section header", which the cascade falsifies. Bind it on the header, which is the section's first child and holds exactly the title and subtitle. The motion assertions could not fail for the bug they were written for. They read `transitionProperty` off the wrapper — but the nav on main declared the same property and still snapped, because it was being remounted. And they ran in an e2e-fixture window, where base.css caps every transition to 0.01ms, so no ease could have been observed there at all. Two locks replace them: a brand on the wrapper node that must survive the round trip, and a new test on the `window` fixture — the one with the product's real durations — that reads `getAnimations()` after the click and requires a width CSSTransition with a duration over 50ms. A zeroed duration, a suppressed gate, and a remounted node each produce no such object. Also locked: the collapsed rail's hairline suppression, which no test read at any layer; the tier's leading, whose stated guard (`type-scale.spec.ts`) runs on a window whose sidebar is collapsed and so never saw the element; and the composer's containment in the window, where the old assertion measured the dock's padding against a layout box that could itself be overflowing — a 150vh mutation leaves the gutter at 12 and pushes the card 454px past the edge. Simplifications the reviews found: one custom property carries the hairline, so the two literals cannot drift and the collapsed state retunes one value instead of nulling two borders; `data-collapsed` is gone from both the wrapper and the nav — the nav's copy had no reader at all, and the wrapper's duplicated the `.appFrame[data-sidebar-state]` flag the hairlines already use; the dead `maka-session-grouping` class is deleted; the E2E stops pinning Astryx's spacing-3 and a unit test pins the density tier instead. Corrected comments: `width: 100% !important` beats `rootCollapsed`'s specificity-padded StyleX atom in the collapsed direction, not an inline width that sits on a nav one level deeper; `balanced` is Astryx's own default, so the density change drops a `compact` override rather than selecting a new tier; and the paragraph about a `data-easing` React flag two revisions gone is removed.
Summary
The sidebar collapsed into a chrome bar: a titleband strip sat above the column, the rail kept its own canvas surface under the traffic lights, and every container overlapping the titlebar declared its own drag region and then reserved room for its neighbours with a hand-summed margin ruler. Those rulers encoded "how many titlebar buttons exist right now" — and when a third button appeared only while the sidebar was collapsed, it landed in the space nobody reserved and its clicks reached the OS as window drags.
This makes the window chrome one transparent absolute overlay — the app's only
-webkit-app-region: drag— so the sidebar canvas and the session--backgroundboth paint to the window top, and the collapsed state is a continuous 48px icon rail rather than a second surface..appFrameis the backplate only; AppShell paints both columns over it.Four fixes ride along, each with its own commit:
makaTheme.tsso AppShell keeps one paint authority. Astryx's own divider is hardcoded tovariant === 'section', which drops the wash entirely and is unconditional — it would draw down the 48px icon rail.pointer-events: auto; anchored in the titlebar they flip below the window's top edge onto the SideNav's first row (86×28 over 新任务, measured live). Clicking 展开侧边栏 parks the pointer on its trigger, and the hover bridge keeps the tooltip alive over the button the pointer is travelling to. Surfaced now because 新任务 moved into the rail on this branch. Scoped to the titlebar — see the review-response section.0 !important. The line is drawn one level up, on the sticky-bottom host.AppShell variant="surface", the one variant that paints both columns the same material and draws no divider, then hand-rolled a plate underneath it (--surface-canvason.appFrame) held in place by two!importanttransparency rules. Astryx ships this model natively —variant="elevated", its default, paints nav--color-background-bodyand content--color-background-surface. Switching to it means fixing whose colors it paints with: Astryx's neutral pair is staticlight-dark()and cannot follow Maka's eleven palettes, and in dark mode the two disagree outright (Astryx surface#262626vs the product's#171719, so the nav column rendered lighter than the content).makaTheme.tsnow points both material tokens back at the product palette, and the two!importantrules are gone. Continuous Collapse survives as the single 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.maka-tokens.csscited four static guards that went out with the 149 source-scanning tests in chore(desktop): drop the source-scanning contract test suite #1724. The rules stay; the claims now say what actually holds them.check-dead-cssnow sweeps tokens alongside classes so this converges instead of drifting back.Verification
@maka/desktop— 1314 pass / 0 fail;@maka/ui— 239 pass / 0 failnpm run lint,npm run format:check,npm run typecheck— cleannode scripts/check-dead-css.mjs --check— no dead classes or tokens. The new token sweep was verified both ways: an injected orphan trips--checkin either declaration style, and removing it returns to zero.Live app, before the rebase: resize drag tracks the cursor frame-for-frame (the ease is suppressed while the handle is being driven, so it no longer restarts on every pointer move); collapse eases 423 → 48px; every
var()resolves after the token deletions (unresolvedCount: 0).Live app (macOS, 2x capture, pixel row scanned across the column seam):
Collapsed drops both the wash and the line: rail 48px, nav surface == content surface, border transparent.
The Playwright E2E suite passes 72/72, including the sidebar and titlebar specs. Live-app screenshots (macOS, traffic lights included) are attached below.
Review response
Five independent reviews (four subagents on separate dimensions, plus Codex) found two P1 defects this branch had introduced. Both were the same failure: a comment asserting a guarantee the code did not provide.
window-titlebar.spec.tshad stopped seeing its own defect class. The strip became an absolute overlay with defaultpointer-events, so it is the topmost hit at every point of the band;elementFromPointreturns it and nothing else, and every control beneath silently failed the reachability precondition and was skipped. The assertion that used to catch this was inverted in the same change —band.bottom <= contentTopbecame its converse, a form satisfied by exactly the geometry that creates the hazard. Mutation-proved: deleting either column'spadding-topput 新任务, the workbar tab and 查看上一版本 under the overlay, producing real 30s click timeouts in three other specs, while this spec passed. It now probes withelementsFromPointminus the strip's subtree and splits the judgment — the strip's children keep the app-region rules, anything beneath it is a defect outright. That immediately caught the resize handle, positioned against the column box rather than flowing inside it, whose top 36px was unreachable.The Astryx neutral ramp was half-migrated. Remapping only the two tokens the shell reads left
card,popover,mutedand the hairline on the staticlight-dark()pair — and a ramp with one half palette-driven and the other frozen inverts rather than drifts. Stockmutedis L 0.222 against a--backgroundof 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,TableRoware the transcript, not chrome. The whole ramp now moves;--mutedlands on foreground-at-5%, defined relative to whatever it sits on, so the inversion is unrepresentable rather than merely corrected.Also acted on, each verified before being accepted:
!importants rested on "StyleX is unlayered", which this app contradicts:astryx.cssis one@layer astryx-baseblock imported intoastryx-components, which productcomponentsoutranks outright. Dropped. The.astryx-buttoncolour rule went with them —Iconcolours the glyph itself, so a button-level rule never reached it..maka-sidenav-motionmirrored a widthSideNavalready owns and then neededwidth: 100% !importantto beat it. Deleted. Its React easing flag went too: cleared ontransitionend, it never cleared when no transition ran, leaving every later drag animated — the exact fault it existed to prevent. The gate is derived from the handle's owndata-resizing/focus now, so it cannot desynchronize.data-sidebar-statehad three writers and one reader;data-modal-background-hiddenhad none at all.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.
Known gaps, stated rather than closed. The generated column-edge selector is a bare
.elevatedclass, which Astryx's ownthemePropsnotes advise against; the CLI emits it, so it is an upstream report, not a local fix. Three further Astryx gaps this branch works around are worth filing upstream:AppShellhardcodes dividers tovariant === 'section'with no passthrough,SideNavships no collapse transition, andtopNavhas no transparent/overlay mode. WindowstitleBarOverlayand Linux paths remain measured by reasoning, not run.One commit here is not this branch's work.
chore: restore Biome formatting on main's check-console entryfixes aformat:checkfailure #1880 left onmain— one allowlist entry written on a line Biome wraps across four. It has been failing thetypecheckjob for every branch since that merge, and this PR cannot go green without it. Mechanicalbiome format --writeon that one file.Review focus
scripts/check-dead-css.mjswidens its consumer surface for tokens only:packages/uistylesheets andstories/count as readers for a token, because a story composing a surface out of the design vocabulary really is one. A product class that only a story mentions is still dead — that asymmetry is deliberate.