Skip to content

fix(app-shell): the narrow-window sidebar rail draws no expanded first frame, folds on one Esc, and keeps the wide-window choice apart - #28141

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-yssobx
Oct 30, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-yssobx

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Bill · project thread

维护者速读

  • 改了什么:窄窗(低于 1416 px)下控制台侧栏的三处返工。① 首帧就是 48 px 图标轨,不再先画 256 px 再收;② 浮出的侧栏里,鼠标停在条目上或焦点在条目/「保持侧栏展开」上,按一次 Esc 就收起;③ 宽窗的展开/收起选择单独保存,窄窗里开关「保持侧栏展开」不再改它,刷新后也一样。
  • 为什么改:PR feat(console): on a narrow window the sidebar is the icon rail, and a click expands it over the content #27893 合并后的代理预览审查(评论 6521834471)在 app-crm 上实测到这三处:首帧闪、第一次 Esc 被隐藏的条目提示吃掉、宽窗选择被窄窗开关覆盖。
  • 风险与回滚:只动 app-shell 控制台外壳(consoleSidebarRail.ts、useResponsiveSidebar.ts、ConsoleLayout.tsx 一行)和测试;阈值 1416 不变;components/src/ui/** 没碰,无新 i18n 键、无发布包新导出。宽窗选择沿用原来的 os:console-sidebar:wide-open 键,老数据语义一致。回滚即还原本 PR。
  • 先看:ui/packages/app-shell/src/layout/consoleSidebarRail.ts(primeConsoleSidebarOpen 首帧、escapeOnlyForHiddenTooltip Esc 判定、宽窗选择的读写);ui/packages/app-shell/src/hooks/useResponsiveSidebar.ts(只有宽窗的开合写宽窗选择)。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/app-shell/src/layout/__tests__/ConsoleLayout.sidebarFirstFrame-28077.test.tsx packages/app-shell/src/layout/__tests__/UnifiedSidebar.railOverlay-27593.test.tsx,预期全绿;或 1280 px 打开 app-crm 联系人,点轨道空白处浮出,鼠标停在「Accounts」上按一次 Esc,侧栏收回。
  • 需要你做的:无,席位落地。

Fixes #28077
Fixes #28078
Fixes #28079

Clause-②: no
Metadata-protocol: none

What changes

  • Console sidebar at 1280 px: the first frame draws the full 256 px sidebar over the content, then animates to the 48 px rail #28077, the first frame is the rail. SidebarProvider takes its first open state from the sidebar_state cookie the last page wrote, so a narrow window drew the expanded sidebar once and the fold then closed it, with the primitive's 200 ms width transition in between. ConsoleLayout now calls primeConsoleSidebarOpen() once (a useState initializer) before AppShell's provider renders: below the fold width with no "keep open" it sets the cookie to false, on a wide window to the reader's wide-window choice; a sidebar kept open and the phone keep what the cookie says. The viewport hook was already synchronous (useIsViewportBelow reads innerWidth in its initializer), so the flash came from the provider's cookie, not from the hook. useConsoleSidebarRail also reads the phone breakpoint synchronously, so a phone-width first render is never taken for a narrow one while useIsMobile still answers false.
  • Console sidebar overlay at 1280 px: the first Esc is swallowed by a hidden nav-item tooltip and does not fold the overlay #28078, one Esc folds the overlay. While the sidebar is expanded the primitive keeps an entry's Radix tooltip open but hidden (hover or focus opens it); its dismissable layer takes the next Esc on the document's capture phase and calls preventDefault, so the overlay's "a layer handled it" check skipped the fold. The overlay now also listens on the window's capture phase, ahead of every layer: when an open tooltip is hidden and no dialog, alert dialog, menu or listbox is shown, that Esc folds the overlay even though it arrives marked handled. An entry's open menu still takes the Esc first (pinned). Fixed from the sidebar side; components/src/ui/** untouched.
  • Console sidebar: toggling "Keep sidebar open" on a narrow window overwrites the wide-window collapsed/expanded choice #28079, the wide-window choice is kept apart. os:console-sidebar:wide-open was a stash written when the rail took over and cleared when keep-open turned on, so keep-open on and then off re-stashed the narrow overlay's state. It is now the reader's wide-window choice: written on every open and close while the window is at or above the fold width, and only then; handed back when the window turns wide and on a wide load. The fold, the overlay and a sidebar kept open on a narrow window never write it; console.sidebar.keepOpen stays under its own key. When no wide-window choice is kept yet (first visit on a narrow window), the open state the fold first closes seeds it.

Tests

  • New layout/__tests__/ConsoleLayout.sidebarFirstFrame-28077.test.tsx (5): the real ConsoleLayout / AppShell / SidebarProvider, a probe in the sidebar's place recording the state of every render. At 1280 px a first visit, a load after an expanded wide window, and a reload with the cookie expanded: every render, the first included, is collapsed. 1440: every render expanded. Keep-open at 1280 keeps the cookie's state.
  • layout/__tests__/UnifiedSidebar.railOverlay-27593.test.tsx (+6): one Esc folds with focus on an entry (its hidden tooltip open), with the pointer on an entry, with focus on Keep sidebar open; an entry's open menu still takes the Esc; the Console sidebar: toggling "Keep sidebar open" on a narrow window overwrites the wide-window collapsed/expanded choice #28079 sequence in both directions (collapsed and expanded wide-window choice), widened and then reloaded.
  • hooks/__tests__/useResponsiveSidebar.test.tsx: follows the kept wide-window choice (it is no longer cleared when the window widens).
  • chrome/KeyboardShortcutsDialog.wiredOnly-11674.test.tsx: its probe already cleared the sidebar cookie between mounts; it now also clears the wide-window key, since the ⌘ run's toggle is kept as the wide-window choice the Ctrl run would otherwise start from.
  • Ablations (each mutation through scripts/ablation-replace.mjs, landed and restored by blob, git diff HEAD empty after each): drop the primeConsoleSidebarOpen call: the three 1280 first-frame tests red, 1440 and keep-open green. Answer false for the hidden-tooltip check: the three one-Esc tests red. Let the narrow keep-open sidebar write the wide-window choice: the expanded-direction Console sidebar: toggling "Keep sidebar open" on a narrow window overwrites the wide-window collapsed/expanded choice #28079 test red. Let the fold write (not seed) the wide-window choice: both Console sidebar: toggling "Keep sidebar open" on a narrow window overwrites the wide-window collapsed/expanded choice #28079 tests red.

Acceptance notes

  • Scope: one file outside the five named: chrome/KeyboardShortcutsDialog.wiredOnly-11674.test.tsx, a test that mounts the real ConsoleLayout; the change is its between-mounts cleanup, one line.
  • The hidden-tooltip check reads the DOM (a role=tooltip inside a hidden element; a shown dialog / alertdialog / menu / listbox). A layer drawn with none of those roles that marks Esc handled while a hidden tooltip is open would see the overlay fold too; none is drawn in the console shell today.
  • The threshold (1416 px, CONSOLE_SIDEBAR_RAIL_BELOW_WIDTH) is unchanged, as ruled.

Verification

Head 55d0fb34.

  • pnpm --dir ui exec vitest run of the 103 app-shell test files that reference ConsoleLayout, UnifiedSidebar, useResponsiveSidebar, consoleSidebarRail, SidebarProvider or sidebar_state: Test Files 103 passed, Tests 1009 passed.
  • pnpm --filter @object-ui/app-shell lint: 0 errors.
  • Declared to CI: the full @object-ui/app-shell test (1967 files; stopped locally after an hour on the shared box) and type-check (needs every @object-ui/* package built).
  • Real browser (Chromium, examples/app-crm with objectstack dev --seed-admin --fresh, the console on its Vite dev server, Contacts): at 1280 px every sampled animation frame from the first one is the 48 px rail on a first visit, a load after an expanded 1440 px window, and a reload with the overlay shown; at 1440 px every frame is 256 px. In the overlay one Esc folds it with the pointer on Accounts (its tooltip open and hidden), with focus on the Accounts link, and with focus on Keep sidebar open. Collapse at 1440, 1280, keep open on and off, 1440: collapsed, and after a reload; the expanded direction comes back expanded, and after a reload.

…t frame, folds on one Esc, and keeps the wide-window choice apart

- The console layout hands the sidebar provider the rail before it mounts on a
  window below the fold width, so the first frame is the 48 px rail.
- A hidden entry tooltip in the expanded overlay no longer swallows the first
  Esc: an Esc only a hidden tooltip would take folds the overlay.
- The wide-window expanded/collapsed choice is kept under its own key, written
  only by an open or close on a wide window; "Keep sidebar open" on a narrow
  window never touches it, across a resize and a reload.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Utp6CZr8r7CoQCcbJ15irL
… sidebar choice between mounts

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Utp6CZr8r7CoQCcbJ15irL
@github-actions github-actions Bot added documentation Improvements or additions to documentation tooling labels Oct 30, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#4 · os-bill · session_01Utp6CZr8r7CoQCcbJ15irL

Preview review (proxy for epic:#26676#1) · b832e42 · PASS

Proxy preview review under ruling B (6520722066 on #26676) of this PR's head, after CI turned green on it (55 check runs: 44 success, 11 skipped). Surface: branch head b832e42c, examples/app-crm (objectstack dev --seed-admin --fresh, console on its Vite dev server), Chromium via Playwright, Contacts (/apps/com.example.crm/crm_contact). Reviewer: one context-free subagent whose only inputs were the Acceptance of #28077, #28078 and #28079 and the console URL / sign-in; a fresh browser profile per scenario. Frame widths come from a requestAnimationFrame loop installed before the page's own scripts. No console or page errors in any scenario. Evidence branch claude/issue-28077-proxy-evidence @ 556b1f98 (evidence/28077-proxy/, never merged).

Overall: PASS on every Acceptance checkbox of the three cards.

#28077 · every frame from the first one at 1280 px is the 48 px rail · PASS

  • First visit: 92 of 92 sidebar frames 48 px, collapsed; main at x 48.
  • Expanded at 1440, then a 1280 load: 88 of 88 frames 48 px (a new tab: 90 of 90).
  • Reload while the overlay was shown (256 px before the reload): 92 of 92 frames 48 px.
  • 1440 px: 91 of 91 frames 256 px, expanded beside the content (main at x 256); reload 90 of 90.
  • Shots: first visit 1280 · narrow load after wide · overlay before reload · after reload · 1440

#28078 · one Esc folds the overlay · PASS

Each scenario started from the 1280 px overlay (256 px) with the hidden tooltip the card describes present (role=tooltip, display:none); each folded to the 48 px rail on the first Esc:

  • pointer resting on "Leads" (1.2 s);
  • focus on "Leads" by Tab and by script;
  • focus on "Keep sidebar open" by Tab and by script;
  • pointer on "Leads" with focus on "Keep sidebar open".

Shots: pointer, before · pointer, after 1 Esc · Tab focus on item, after 1 Esc · Tab focus on Keep open, after 1 Esc

#28079 · the wide-window choice survives Keep sidebar open on/off, after a reload too · PASS

  • Collapsed at 1440 → 1280 → overlay → Keep open on → off → 1440: collapsed 48; after a reload collapsed 48 (a new tab too).
  • Expanded at 1440, same sequence: expanded 256 with main at x 256; after a reload expanded 256 (a new tab too).
  • With an extra reload at 1280 before widening: same results in both directions.
  • Shots: collapsed, widened · collapsed, reloaded · expanded, widened · expanded, reloaded

Seen, outside the verdict

  • After Esc folds the overlay while "Keep sidebar open" has focus, the button leaves with the overlay and focus falls to the document body; with focus on an entry, focus stays on that entry's rail icon. Not asked by these cards; noted for the seat.
  • The overlay at 1280 px reads with no visible shadow edge over the content in the screenshots (cosmetic, outside these cards).

Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#4 · os-bill · session_01Utp6CbDAucNyQ4UtzCJKyh

席位核对

ACCEPT · head b832e42c9961e9e55cb0529d4e4825b0c5b5b284 · 2026-10-30T14:37Z

  • Shape: draft → main, Fixes #28077, Fixes #28078, Fixes #28079, one PR as the order asked. Clause-②: no. Metadata-protocol: none. The threshold stays at 1416 (CONSOLE_SIDEBAR_RAIL_BELOW_WIDTH), as ruled. ui/packages/components/src/ui/** is untouched (No-Touch held). There is no new i18n key and no new export from a published package. All changes are in @object-ui/app-shell, which is private.
  • Change, read:
  • Changeset (prose face, checked against the diff): .changeset/28077-console-sidebar-rail-rework.md has three bullets. "every frame from the first one is the rail" holds through the prime. "One Esc now folds it; an entry's open menu still takes the Esc first" holds through the capture listener and its pin. "Only an open or close on a wide window is kept as that choice" holds because the write is gated on wide. @object-ui/app-shell patch matches.
  • Preview (ruling 6520722066): proxy preview review 6525365933, PASS on this head, on every Acceptance box of the three cards. At 1280 every frame is 48 px across 88–92 frames per case, and at 1440 every frame is 256 px. One Esc folds the overlay in all five focus and pointer cases. Both Console sidebar: toggling "Keep sidebar open" on a narrow window overwrites the wide-window collapsed/expanded choice #28079 directions survive widening, a reload and a new tab. Evidence: claude/issue-28077-proxy-evidence @ 556b1f98.
  • Evidence (report 6524726998, written at 55d0fb34):
    • 103 related app-shell files with 1009 tests pass, and lint has 0 errors.
    • Four ablations went red and were restored.
    • The later head adds only the changeset. Its gate family passed 35/35 locally with 0 NOT-MEASURED.
    • The report's file list and the PR body's "Head 55d0fb34" line predate the changeset; this is noted, not patched.
    • The full app-shell test suite and type-check are declared to CI, and both are green here.
  • Scope note: one file sits outside the order's list, chrome/KeyboardShortcutsDialog.wiredOnly-11674.test.tsx. It is a one-line cleanup between mounts of a test that mounts the real ConsoleLayout. Accepted.
  • Gates (check-runs on the head): 44 success, 11 skipped, 0 failure. mergeable_state is clean. NOT governed: 0 of 8 paths, 446 changed lines.
  • Outside the verdict (left for the next shift / the epic seat's main re-check, not carded by this off-duty seat):
    • When Esc folds the overlay with focus on "Keep sidebar open", focus falls to body.
    • The 1280 overlay shows no visible shadow edge in the screenshots.
  • Landing: non-governed and all green, so the seat lands it with pr_ready and automerge_enable.

Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tooling

Projects

None yet

1 participant