Skip to content

fix(desktop): ease the rail collapse, even the hairlines, switch grouping inline - #1892

Merged
Astro-Han merged 5 commits into
mainfrom
fix/sidenav-collapse-regressions
Aug 2, 2026
Merged

fix(desktop): ease the rail collapse, even the hairlines, switch grouping inline#1892
Astro-Han merged 5 commits into
mainfrom
fix/sidenav-collapse-regressions

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

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% !important on 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 :focus half 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-duration measured 0s at 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 Divider placed last in 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 — 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's border-bottom makes 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 supporting step (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 grid e2e/type-scale.spec.ts holds the app to.

Grouping moves from a dropdown to an inline switch.SegmentedControl is what 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.

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 compact spends 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. balanced spends 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 _electron with CDP computed styles, not inferred from source.

  • npm run lint / format:check / typecheck — clean
  • node scripts/check-dead-css.mjs --check — clean
  • desktop unit — 1321 pass, 0 fail
  • @maka/ui unit — 239 pass, 0 fail
  • E2E — 75/75 (macOS)

Locks, each mutation-verified to turn the suite red on its own:

mutationtest that failed
drop the wrapper transitionsidebar-geometry collapse round trip
drop the :has([data-resizing]) gatesidebar-geometry ease gate
restore the :focus half of the gatesidebar-geometry ease gate
unequalize either hairlinesidebar-geometry rail rhythm
revert the section type tiersidebar-geometry rail rhythm
revert the chat density to compactscroll-geometry composer rest

The 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% !important in shell-layout.css is 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.

…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
Astro-Han marked this pull request as ready for review August 2, 2026 09:22
@Astro-Han
Astro-Han merged commit 006314b into mainAug 2, 2026
5 checks passed
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
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
fix(desktop): ease the rail collapse, even the hairlines, switch grouping inline by Astro-Han · Pull Request #1892 · apache/maka · GitHub
Skip to content

fix(desktop): ease the rail collapse, even the hairlines, switch grouping inline - #1892

Merged
Astro-Han merged 5 commits into
mainfrom
fix/sidenav-collapse-regressions
Aug 2, 2026
Merged

fix(desktop): ease the rail collapse, even the hairlines, switch grouping inline#1892
Astro-Han merged 5 commits into
mainfrom
fix/sidenav-collapse-regressions

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

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% !important on 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 :focus half 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-duration measured 0s at 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 Divider placed last in 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 — 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's border-bottom makes 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 supporting step (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 grid e2e/type-scale.spec.ts holds the app to.

Grouping moves from a dropdown to an inline switch.SegmentedControl is what 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.

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 compact spends 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. balanced spends 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 _electron with CDP computed styles, not inferred from source.

  • npm run lint / format:check / typecheck — clean
  • node scripts/check-dead-css.mjs --check — clean
  • desktop unit — 1321 pass, 0 fail
  • @maka/ui unit — 239 pass, 0 fail
  • E2E — 75/75 (macOS)

Locks, each mutation-verified to turn the suite red on its own:

mutationtest that failed
drop the wrapper transitionsidebar-geometry collapse round trip
drop the :has([data-resizing]) gatesidebar-geometry ease gate
restore the :focus half of the gatesidebar-geometry ease gate
unequalize either hairlinesidebar-geometry rail rhythm
revert the section type tiersidebar-geometry rail rhythm
revert the chat density to compactscroll-geometry composer rest

The 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% !important in shell-layout.css is 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.

…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
Astro-Han marked this pull request as ready for review August 2, 2026 09:22
@Astro-Han
Astro-Han merged commit 006314b into mainAug 2, 2026
5 checks passed
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
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(desktop): ease the rail collapse, even the hairlines, switch grouping inline by Astro-Han · Pull Request #1892 · apache/maka · GitHub
Skip to content

fix(desktop): ease the rail collapse, even the hairlines, switch grouping inline - #1892

Merged
Astro-Han merged 5 commits into
mainfrom
fix/sidenav-collapse-regressions
Aug 2, 2026
Merged

fix(desktop): ease the rail collapse, even the hairlines, switch grouping inline#1892
Astro-Han merged 5 commits into
mainfrom
fix/sidenav-collapse-regressions

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

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% !important on 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 :focus half 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-duration measured 0s at 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 Divider placed last in 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 — 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's border-bottom makes 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 supporting step (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 grid e2e/type-scale.spec.ts holds the app to.

Grouping moves from a dropdown to an inline switch.SegmentedControl is what 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.

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 compact spends 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. balanced spends 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 _electron with CDP computed styles, not inferred from source.

  • npm run lint / format:check / typecheck — clean
  • node scripts/check-dead-css.mjs --check — clean
  • desktop unit — 1321 pass, 0 fail
  • @maka/ui unit — 239 pass, 0 fail
  • E2E — 75/75 (macOS)

Locks, each mutation-verified to turn the suite red on its own:

mutationtest that failed
drop the wrapper transitionsidebar-geometry collapse round trip
drop the :has([data-resizing]) gatesidebar-geometry ease gate
restore the :focus half of the gatesidebar-geometry ease gate
unequalize either hairlinesidebar-geometry rail rhythm
revert the section type tiersidebar-geometry rail rhythm
revert the chat density to compactscroll-geometry composer rest

The 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% !important in shell-layout.css is 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.

…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
Astro-Han marked this pull request as ready for review August 2, 2026 09:22
@Astro-Han
Astro-Han merged commit 006314b into mainAug 2, 2026
5 checks passed
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
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', '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('^' + ".*" + ' fix(desktop): ease the rail collapse, even the hairlines, switch grouping inline by Astro-Han · Pull Request #1892 · apache/maka · GitHub
Skip to content

fix(desktop): ease the rail collapse, even the hairlines, switch grouping inline - #1892

Merged
Astro-Han merged 5 commits into
mainfrom
fix/sidenav-collapse-regressions
Aug 2, 2026
Merged

fix(desktop): ease the rail collapse, even the hairlines, switch grouping inline#1892
Astro-Han merged 5 commits into
mainfrom
fix/sidenav-collapse-regressions

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

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% !important on 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 :focus half 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-duration measured 0s at 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 Divider placed last in 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 — 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's border-bottom makes 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 supporting step (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 grid e2e/type-scale.spec.ts holds the app to.

Grouping moves from a dropdown to an inline switch.SegmentedControl is what 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.

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 compact spends 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. balanced spends 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 _electron with CDP computed styles, not inferred from source.

  • npm run lint / format:check / typecheck — clean
  • node scripts/check-dead-css.mjs --check — clean
  • desktop unit — 1321 pass, 0 fail
  • @maka/ui unit — 239 pass, 0 fail
  • E2E — 75/75 (macOS)

Locks, each mutation-verified to turn the suite red on its own:

mutationtest that failed
drop the wrapper transitionsidebar-geometry collapse round trip
drop the :has([data-resizing]) gatesidebar-geometry ease gate
restore the :focus half of the gatesidebar-geometry ease gate
unequalize either hairlinesidebar-geometry rail rhythm
revert the section type tiersidebar-geometry rail rhythm
revert the chat density to compactscroll-geometry composer rest

The 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% !important in shell-layout.css is 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.

…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
Astro-Han marked this pull request as ready for review August 2, 2026 09:22
@Astro-Han
Astro-Han merged commit 006314b into mainAug 2, 2026
5 checks passed
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
, '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" + ' fix(desktop): ease the rail collapse, even the hairlines, switch grouping inline by Astro-Han · Pull Request #1892 · apache/maka · GitHub
Skip to content

fix(desktop): ease the rail collapse, even the hairlines, switch grouping inline - #1892

Merged
Astro-Han merged 5 commits into
mainfrom
fix/sidenav-collapse-regressions
Aug 2, 2026
Merged

fix(desktop): ease the rail collapse, even the hairlines, switch grouping inline#1892
Astro-Han merged 5 commits into
mainfrom
fix/sidenav-collapse-regressions

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

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% !important on 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 :focus half 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-duration measured 0s at 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 Divider placed last in 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 — 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's border-bottom makes 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 supporting step (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 grid e2e/type-scale.spec.ts holds the app to.

Grouping moves from a dropdown to an inline switch.SegmentedControl is what 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.

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 compact spends 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. balanced spends 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 _electron with CDP computed styles, not inferred from source.

  • npm run lint / format:check / typecheck — clean
  • node scripts/check-dead-css.mjs --check — clean
  • desktop unit — 1321 pass, 0 fail
  • @maka/ui unit — 239 pass, 0 fail
  • E2E — 75/75 (macOS)

Locks, each mutation-verified to turn the suite red on its own:

mutationtest that failed
drop the wrapper transitionsidebar-geometry collapse round trip
drop the :has([data-resizing]) gatesidebar-geometry ease gate
restore the :focus half of the gatesidebar-geometry ease gate
unequalize either hairlinesidebar-geometry rail rhythm
revert the section type tiersidebar-geometry rail rhythm
revert the chat density to compactscroll-geometry composer rest

The 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% !important in shell-layout.css is 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.

…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
Astro-Han marked this pull request as ready for review August 2, 2026 09:22
@Astro-Han
Astro-Han merged commit 006314b into mainAug 2, 2026
5 checks passed
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
, '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('^' + ".*" + ' fix(desktop): ease the rail collapse, even the hairlines, switch grouping inline by Astro-Han · Pull Request #1892 · apache/maka · GitHub
Skip to content

fix(desktop): ease the rail collapse, even the hairlines, switch grouping inline - #1892

Merged
Astro-Han merged 5 commits into
mainfrom
fix/sidenav-collapse-regressions
Aug 2, 2026
Merged

fix(desktop): ease the rail collapse, even the hairlines, switch grouping inline#1892
Astro-Han merged 5 commits into
mainfrom
fix/sidenav-collapse-regressions

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

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% !important on 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 :focus half 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-duration measured 0s at 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 Divider placed last in 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 — 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's border-bottom makes 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 supporting step (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 grid e2e/type-scale.spec.ts holds the app to.

Grouping moves from a dropdown to an inline switch.SegmentedControl is what 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.

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 compact spends 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. balanced spends 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 _electron with CDP computed styles, not inferred from source.

  • npm run lint / format:check / typecheck — clean
  • node scripts/check-dead-css.mjs --check — clean
  • desktop unit — 1321 pass, 0 fail
  • @maka/ui unit — 239 pass, 0 fail
  • E2E — 75/75 (macOS)

Locks, each mutation-verified to turn the suite red on its own:

mutationtest that failed
drop the wrapper transitionsidebar-geometry collapse round trip
drop the :has([data-resizing]) gatesidebar-geometry ease gate
restore the :focus half of the gatesidebar-geometry ease gate
unequalize either hairlinesidebar-geometry rail rhythm
revert the section type tiersidebar-geometry rail rhythm
revert the chat density to compactscroll-geometry composer rest

The 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% !important in shell-layout.css is 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.

…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
Astro-Han marked this pull request as ready for review August 2, 2026 09:22
@Astro-Han
Astro-Han merged commit 006314b into mainAug 2, 2026
5 checks passed
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
, '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); } })(); })(); fix(desktop): ease the rail collapse, even the hairlines, switch grouping inline by Astro-Han · Pull Request #1892 · apache/maka · GitHub
Skip to content

fix(desktop): ease the rail collapse, even the hairlines, switch grouping inline - #1892

Merged
Astro-Han merged 5 commits into
mainfrom
fix/sidenav-collapse-regressions
Aug 2, 2026
Merged

fix(desktop): ease the rail collapse, even the hairlines, switch grouping inline#1892
Astro-Han merged 5 commits into
mainfrom
fix/sidenav-collapse-regressions

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

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% !important on 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 :focus half 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-duration measured 0s at 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 Divider placed last in 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 — 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's border-bottom makes 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 supporting step (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 grid e2e/type-scale.spec.ts holds the app to.

Grouping moves from a dropdown to an inline switch.SegmentedControl is what 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.

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 compact spends 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. balanced spends 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 _electron with CDP computed styles, not inferred from source.

  • npm run lint / format:check / typecheck — clean
  • node scripts/check-dead-css.mjs --check — clean
  • desktop unit — 1321 pass, 0 fail
  • @maka/ui unit — 239 pass, 0 fail
  • E2E — 75/75 (macOS)

Locks, each mutation-verified to turn the suite red on its own:

mutationtest that failed
drop the wrapper transitionsidebar-geometry collapse round trip
drop the :has([data-resizing]) gatesidebar-geometry ease gate
restore the :focus half of the gatesidebar-geometry ease gate
unequalize either hairlinesidebar-geometry rail rhythm
revert the section type tiersidebar-geometry rail rhythm
revert the chat density to compactscroll-geometry composer rest

The 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% !important in shell-layout.css is 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.

…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
Astro-Han marked this pull request as ready for review August 2, 2026 09:22
@Astro-Han
Astro-Han merged commit 006314b into mainAug 2, 2026
5 checks passed
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