From ba66872f705dac2e0ae84381fb58741be2c8f695 Mon Sep 17 00:00:00 2001 From: Nathan Esquenazi Date: Sun, 10 May 2026 21:57:47 -0700 Subject: [PATCH] fix(sidebar): align collapse CSS breakpoint with JS _isDesktopWidth (641px) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `_isDesktopWidth()` in boot.js gates every collapse path on `matchMedia('(min-width:641px)')` — matching where the rail itself becomes visible. The CSS rules driving the actual visual collapse were nested inside the workspace-panel block at `@media(min-width:901px)` — a threshold copied from the right-panel collapse but with no functional reason to apply here. Behavioural consequence in the 641–900 px band (tablet portrait + small laptop windows): - Rail is visible, user clicks the active icon - JS adds `.layout.sidebar-collapsed` and writes localStorage='1' - JS sets aria-expanded='false' on the active rail button - CSS at min-width:901px does NOT apply → sidebar stays at 300 px width - User sees no visual change; screen reader announces collapsed state for a sidebar that is still visible; localStorage silently persists - Resize to ≥901 px later → sidebar suddenly collapses (surprise state) Fix: hoist the three `.sidebar-collapsed` / flash-prevention rules out of the workspace-panel @media block and into their own `@media(min-width:641px)` block. The rail visibility breakpoint, the JS gate, and the CSS gate now all agree. `:not(.mobile-open)` is preserved on both selectors so the mobile slide-in overlay (handled in the `max-width:640px` block) is never targeted — the new @641 boundary doesn't change that contract. Verified breakpoint matrix end-to-end (Node harness over real boot.js + style.css): Width | JS desktop | CSS applies | Effect ------|------------|-------------|------------ 640 | no | no | no-op (mobile overlay) 641 | yes | yes | collapses ✓ 700 | yes | yes | collapses ✓ 768 | yes | yes | collapses ✓ 900 | yes | yes | collapses ✓ 1024 | yes | yes | collapses ✓ Regression test added: `test_css_breakpoint_matches_js_isdesktopwidth` parses boot.js for the `_isDesktopWidth` matchMedia query, walks CSS to find the @media block enclosing `.layout.sidebar-collapsed`, and asserts the thresholds match. Locks the invariant so a future refactor can't re-introduce the asymmetric-band silent-state-leak. Test counts: - tests/test_sidebar_collapse_toggle.py: 35/35 pass (was 34, +1 regression) - Full suite (Python 3.14, local): 5040 passed, 0 failed Co-Authored-By: Claude Opus 4.7 (1M context) --- static/style.css | 12 +++++-- tests/test_sidebar_collapse_toggle.py | 48 +++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/static/style.css b/static/style.css index a162e2db..0c2f6ea2 100644 --- a/static/style.css +++ b/static/style.css @@ -1314,9 +1314,15 @@ @media(min-width:901px){ html[data-workspace-panel="closed"] .rightpanel{width:0 !important;opacity:0;transform:translateX(14px);border-left-color:transparent;pointer-events:none;} .layout.workspace-panel-collapsed .rightpanel{width:0 !important;opacity:0;transform:translateX(14px);border-left-color:transparent;pointer-events:none;} - /* Sidebar collapse — same shape as workspace panel collapse, mirrored on the left. - :not(.mobile-open) so the mobile slide-in overlay (handled in the max-width:640px - block above) is never targeted by these rules. */ + } + + /* Sidebar collapse breakpoint matches `_isDesktopWidth()` (min-width:641px) so + clicking the active rail icon in the tablet-portrait band (641–900px) actually + produces a visual change rather than silently flipping a class while CSS sits + out at @901. The rail itself becomes visible at min-width:641px, so any width + where the user can click the rail should also be a width where the collapse + rule applies. :not(.mobile-open) excludes the slide-in overlay below 641px. */ + @media(min-width:641px){ .layout.sidebar-collapsed .sidebar:not(.mobile-open){width:0 !important;min-width:0;opacity:0;transform:translateX(-14px);border-right-color:transparent;pointer-events:none;overflow:hidden;} .layout.sidebar-collapsed .sidebar .resize-handle{display:none;} /* Flash prevention: an inline