Uh oh!
There was an error while loading. Please reload this page.
fix(desktop): ease the rail collapse, even the hairlines, switch grouping inline - #1892
Merged
Conversation
…ping 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.
…ion 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.
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.
It was instrumentation for diagnosing the four post-#1876 reports against the live app, committed by mistake. Its own header calls it throwaway.
…ts 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.
Astro-Han
marked this pull request as ready for review
August 2, 2026 09:22
Uh oh!
There was an error while loading. Please reload this page.
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 freeto 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
Four reports against the 48px session rail #1876 shipped, plus three follow-ups from the review pass on the same surface. Only the first is a regression from that PR; the rest were already there and are fixed here because they are the same surface and the same review pass.
The collapse stopped animating. The transition sat on the nav, and Astryx SideNav swaps its own root element type across the toggle — expanded it wraps the
<nav>in a positioned div for the overlay resize handle (showResizeHandle = isResizable && !collapsed), collapsed it renders the bare<nav>. React sees a different type in the same position, unmounts that subtree and mounts a fresh one, and a transition declared there has no start value to interpolate from. Measured against the live app: all 144 sampled frames after the click were 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% !importanton its child is load-bearing rather than leftover: SideNav sets an inline px width when expanded, and inline style outranks any layered rule without it.The ease gate is
:has([data-resizing])only. The:focushalf it shipped with answered "the handle was used at some point", not "the handle is being used now": with focus parked on the separator,transition-durationmeasured0sat the collapse toggle, and it survived in practice only because clicking the titlebar button steals focus first. Dropping it costs a keyboard resize at most 13px of lag behind the target at a 60ms key repeat.The rail's two hairlines are now drawn the same way. The top one was an Astryx
Dividerplaced last intopContent:--color-borderis 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 — measured x=8 w=244 against the footer's x=0 w=260. Read together, one was a separator and the other a box edge. Drawing it as the sticky zone'sborder-bottommakes both the same 1px at the same alpha across the same full width, with the sticky padding supplying the clearance the divider needed a margin for, and drops an element and a component from the tree.Group labels were a tier too small. SideNavSection draws its title from Astryx's
supportingstep (12px), one below the rows it labels; against Maka's 14px body that reads as a caption, and CJK loses more apparent size at 12px than Latin does. The tier is rebound on the section rather than pinned on the title span, so the size still flows through Astryx's own rule — both size AND leading, or the supporting multiplier lands the line box at 23.33px and off the 4px gride2e/type-scale.spec.tsholds the app to.Grouping moves from a dropdown to an inline switch.
SegmentedControlis what Astryx spends on exactly this in its ownfile-explorerandidetemplates. 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.Session and project more-menus carry leading icons, so the menu reads as a list of actions rather than a list of strings.
The composer card rested on the window edge. ChatLayout's density sets the dock's own gutters, and
compactspends spacing-2 on them — 8px between the card's rounded bottom edge and the frame, measured identical at 860, 617, and 500px window heights, so it was the tier and not a broken flex contract.balancedspends spacing-3 there 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.Refs #1876.
Verification
Diagnosis and every measurement above were taken against the built app driven through Playwright
_electronwith CDP computed styles, not inferred from source.npm run lint/format:check/typecheck— cleannode scripts/check-dead-css.mjs --check— clean@maka/uiunit — 239 pass, 0 failLocks, each mutation-verified to turn the suite red on its own:
sidebar-geometrycollapse round trip:has([data-resizing])gatesidebar-geometryease gate:focushalf of the gatesidebar-geometryease gatesidebar-geometryrail rhythmsidebar-geometryrail rhythmcompactscroll-geometrycomposer restThe predecessor assertion this replaces was reading the nav rather than the element that survives the toggle, which is why it passed while the animation was gone. The rhythm test now compares the two hairlines against each other — thickness, color, clearance, and width — rather than pinning one of them to a constant.
Review focus
width: 100% !importantinshell-layout.cssis deliberate and load-bearing — it is the only thing that beats SideNav's inline width so the child follows the wrapper's eased width. Removing it silently restores the snap.