Skip to content

console shell: the sidebar collapse toggle moves from the sidebar footer to the app bar's left slot at every width, where the drawer button already sits below lg #12429

Closed

Description

@objectstack-fleet

Filing gate ③, a task the maintainer dispatched in person (chat, 2026-10-15, session session_019YFzJp4o8Ck4NsT1toC4UP, seat account os-bill). The maintainer's words, verbatim: 「还有左侧菜单的折叠按钮原来在左下角,是否建议调整位置」, and, on the seat's proposal to move it to the app bar's left slot: 「接受你的建议」. The design is the prototype at https://claude.ai/artifact/8JLqDFmcuK82RS1Cb8219f (private to the maintainer; the main artboard's app bar and the 1024 artboard's collapsed rail).

Blocked-by: #12427

Serial after #12427: both cards edit UnifiedSidebar.tsx.

Reader who acts: the domain:ui lane, one small dev round in @object-ui/app-shell (plus the ten @object-ui/i18n packs only if a label changes), after #12427 merges.

Dedupe (MCP search_issues, scope objectstack-ai/objectui, all states, read 2026-10-15T05:02Z): "sidebar collapse toggle button position footer app bar" → 1 (#1000, closed, the mobile duplicate-toggle cleanup; it is the reason the app bar's trigger is hidden at lg and above today). None is this card.

The problem

The desktop control that collapses the sidebar sits at the sidebar's bottom-left, inside its footer. It is far from where the pointer works, it is the last thing in a long navigation, and it is the only navigation control that lives in a different place from its phone counterpart (the drawer button in the app bar). The agreed shape: the app bar's left slot is the navigation control at every width: the drawer button below lg, the collapse/expand toggle at lg and above, and the collapsed icon rail keeps the toggle in that same spot so it never moves.

Readings (objectui origin/main fc528c3, read 2026-10-15T05:00Z)

  1. packages/app-shell/src/layout/UnifiedSidebar.tsx line 434: <Sidebar collapsible="icon" …>, so the collapsed form is already the icon rail; lines 689–691: the only desktop trigger is a LocalizedSidebarTrigger inside SidebarFooter at the sidebar's bottom.
  2. packages/app-shell/src/layout/AppHeader.tsx lines 577–578: the app bar already carries a LocalizedSidebarTrigger, but lg:hidden, so it is the drawer button below lg only (the duplicate-toggle cleanup of [Mobile UI] Clean Up Action Buttons, Layout, and Remove Duplicate Sidebar Toggle Button #1000).
  3. packages/components/src/ui/sidebar.tsx lines 42 and 112–126: the keyboard shortcut (Cmd or Ctrl + B) already toggles the sidebar; nothing here changes it.

What to build

  1. The app bar's trigger renders at every width, in the left slot before the app switcher (the slot it already uses below lg), with aria-expanded reflecting the sidebar state and a tooltip that names the shortcut.
  2. The SidebarFooter trigger is removed; the footer goes with it if nothing else is in it.
  3. The collapsed icon rail carries no trigger of its own; the app bar's trigger expands it, from the same spot.
  4. No change to how the collapsed state is persisted, to the icon rail's contents, or to the shortcut.

Acceptance

  • At lg and above: the app bar renders exactly one sidebar trigger at its far left and the sidebar footer renders none; one click collapses to the icon rail, a second expands; Cmd/Ctrl + B unchanged.
  • Below lg: exactly one trigger, as today ([Mobile UI] Clean Up Action Buttons, Layout, and Remove Duplicate Sidebar Toggle Button #1000's pin stays green).
  • Existing tests touching the footer trigger are updated deliberately; pnpm test from the repo root green on the affected files; a changeset (minor) for app-shell.

Out of scope

  • Sidebar width, a resizable sidebar, the icon rail's contents, the mobile drawer.

Attribution: written by the domain:ui seat in session session_019YFzJp4o8Ck4NsT1toC4UP (account os-bill) on the maintainer's instruction quoted above.

Activity

  1. added
    enhancementNew feature or request
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    area:devpathThe road — create, dev, verify, publish/install, connect an agent, iterate
    on Oct 15, 2026
  2. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    Regrade 2026-10-15T07:57Z on the maintainer's word (chat, 2026-10-15, verbatim: 「那后续的任务你放回队列,按 p1 标记,开发完了合并之后你验收。」): priority:p3 → priority:p1. The card stays pm:blocked only until #12427's PR #12443 merges (both edit UnifiedSidebar.tsx and NavigationRenderer.tsx), and returns to pm:queue in that landing stroke for the lane's next seat to dispatch; this session no longer dispatches it. Acceptance after merge is the maintainer's, through the session they designate, against the prototype on #12444. Parent: #12444.
    Thread-read: none


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    Design reference 2026-10-15T08:08Z from the filing seat, so the dev and the reviewing seat build and check against the same picture: https://claude.ai/artifact/JCzrzHxTxzD4EEKCTtDaNA (page files main-1440.png, laptop-1024.png; the interactive prototype is linked from it). Board region: the app bar's leftmost button on both boards, and the 56 px icon rail on the 1024 board. Targets, measured on the HotCRM demo as admin: at lg and above exactly one sidebar trigger renders and it is the app bar's leftmost control (x ≈ 12–36 px, the spot the phone ☰ already uses), the sidebar footer renders none; one click collapses the sidebar to the 56 px icon rail and the next expands it, from the same spot; Cmd/Ctrl+B unchanged; below lg exactly one trigger as today. The acceptance list in the card body stands; this comment names the picture and the numbers it is checked against. Parent: #12444.
    Thread-read: 6187355791


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    Unblocked 2026-10-15T08:14Z on the maintainer's word (chat, 2026-10-15: 「12429 为什么不能并行?」): pm:blocked → pm:queue, dispatchable now in parallel with #12427's PR #12443. The overlap is the same file, different regions: this card edits the SidebarFooter trigger in UnifiedSidebar.tsx (around lines 651–653 on main a2225f9) and the trigger's visibility in AppHeader.tsx; PR #12443 edits that file's Favorites block (around 524–530) and the NavigationRenderer call site, plus packages/layout/src/NavigationRenderer.tsx. Dev rule for whoever takes this: branch from origin/main, merge origin/main once more before opening the PR, and whichever of the two lands second merges main again (a merge commit, never a rebase). The earlier "Blocked-by #12427" line is withdrawn. Parent: #12444; design reference in the comment above.
    Thread-read: 6187520506


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1
    Session: session_01XVp19E1MYUTFM3dnkwz8gB
    Account: baozhoutao (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-12429-sidebar-trigger-app-bar
    Worktree: objectui-issue-12429
    Domain: domain:ui
    Seat: domain:ui#2
    File surface: packages/app-shell/src/layout/AppHeader.tsx (the left-slot trigger's visibility, aria-expanded, tooltip), packages/app-shell/src/layout/UnifiedSidebar.tsx (the SidebarFooter trigger only, :651–:653 on main a2225f9), packages/app-shell/src/layout/LocalizedSidebarTrigger.tsx if the tooltip or aria-expanded belongs there, the tests beside each, packages/i18n/src/locales/*.ts only if a label changes, and one .changeset/12429-*.md (stop on breach; explain in the report)
    Container & model: S, mode:subagent, model: opus (default tier; dispatch-gates --tier --repo objectstack-ai/objectui reads no path-derived mandate)
    Clause-②: no
    Responsibility: n/a — not a defect card
    Thread-read: 6187638681
    Serial constraints cleared: objectui#12427's PR #12443 edits UnifiedSidebar.tsx's Favorites block (:524–:530) and the NavigationRenderer call site. The maintainer ruled the two parallel (6187638681); whichever lands second merges main again (a merge, never a rebase). objectui#12418, objectui#12459 and objectui#12440 (claimed alongside) do not touch AppHeader.tsx, UnifiedSidebar.tsx or LocalizedSidebarTrigger.tsx.


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
     "issue": 12429,
     "status": "done",
     "branch": "claude/issue-12429-sidebar-trigger-app-bar",
     "pr": "https://github.com/objectstack-ai/objectui/pull/12466",
     "session": "session_01XVp19E1MYUTFM3dnkwz8gB",
     "premise_still_valid": true,
     "summary": "The app bar's LocalizedSidebarTrigger loses lg:hidden and renders at every width as the left slot's first control inside an app (before the workspace switcher and the app pill); the SidebarFooter trigger and the now-empty footer are removed from UnifiedSidebar, so the icon rail carries no trigger. The trigger reports aria-expanded (openMobile below the Shadcn mobile breakpoint, open above) and shows a Radix tooltip with the label and the shortcut drawn with the shortcuts dialog's keycaps (shortcutKeycaps: 'Toggle sidebar' + the command-B keycap); no new i18n string, no persistence/shortcut/rail-content change. A1 measured partly false: the trigger calls useSidebar().toggleSidebar() directly rather than wrapping shadcn SidebarTrigger (same effect), and the sidebar's mobile breakpoint is 768 px (use-mobile.tsx MOBILE_BREAKPOINT), not lg; on main before this PR both triggers rendered between 768 and 1023 px. After the PR exactly one trigger renders at every width; in that gap one click collapses to the icon rail (pinned at 900 px). There is no #1000 test pin in the tree: PR #1001 touched packages/layout and plugin-detail with no test; the one-trigger invariant is now pinned by the two new tests. PR #12443 landed before the PR opened; origin/main cf430cd10 merged in cleanly (merge commit 71f636061).",
     "tests": "New: packages/app-shell/src/layout/__tests__/AppHeader.sidebarTrigger-12429.test.tsx (3) and LocalizedSidebarTrigger.appBar-12429.test.tsx (5, real SidebarProvider + real UnifiedSidebar). Pre-merge e763cb811: app-shell src/layout/ under lock 'Test Files 69 passed (69) / Tests 483 passed (483)', VERDICT command-exit 0. Post-merge head 71f636061 (narrowed, unlocked, declared): the two new pins + UnifiedSidebar.favoritesMenu-12427, pinnedOrder-12059, mobileAppHeader-12201, KeyboardShortcutsDialog.wiredOnly-11674: 'Test Files 6 passed (6) / Tests 31 passed (31)' exit 0. Closure build turbo app-shell^... 'Tasks 28 successful' VERDICT 0; app-shell type-check (tsc --noEmit + tsconfig.test.json, --listFilesOnly counts both new tests) exit 0 on e763cb811; post-merge type-check NOT MEASURED (lock queue-timeout x4, holder pid 7816 objectui-issue-12440 suite held 2576s+). Full app-shell project NOT MEASURED: my locked run held the lock 45 min unfinished with two siblings queued, so I killed my own PID tree; declared to CI. Ablation (fix committed first, src-aliased tests so no dist): base AppHeader.tsx + UnifiedSidebar.tsx checked out, on-disk proof 'lg:hidden shrink-0 ml-1' count 1 and SidebarFooter count 1, run: 6 failed / 2 passed (tooltip + Home control stay green as expected), restored via git checkout HEAD, git diff HEAD empty. Lint: eslint --no-inline-config on the 5 changed ts/tsx (population: config block **/*.{ts,tsx}; count 5 from --format json): 0 errors, 29 warnings (base AppHeader+UnifiedSidebar: 0 errors / 30 warnings); no type-aware linting in eslint.config.js, so untouched files' verdicts cannot move.",
     "mcp_calls": "0",
     "api_writes": "2 — fleet-write pr_create (POST /repos/objectstack-ai/objectui/pulls draft + POST /repos/objectstack-ai/objectui/issues/12466/assignees), and this os-dev-report comment POST /repos/objectstack-ai/objectui/issues/12429/comments; git pushes not counted",
     "gates": {
      "check:phantom-deps": 0,
      "check:self-import": 0,
      "check:unreferenced-sources": 0,
      "check:doc-example-readers": 0,
      "check:handler-key-reads": 0,
      "check:side-effects-array": 0,
      "check:element-data-source-declaration": 0,
      "check:spec-symbols": 0,
      "check:action-forward-parity": 0,
      "check:designer-field-key-parity": 0,
      "check:icon-record-names": 0,
      "check:i18n-keys": 0,
      "check:i18n-drift": 0,
      "check:i18n-designer-parity": 0,
      "check:control-bytes": 0,
      "check:vi-mock-specifiers": 0,
      "check:vi-mock-inherit": 0,
      "check:vi-mock-override-shape": 0,
      "check:new-line-citations": 0,
      "check:test-path-roots": 0,
      "check-changeset-fixed": 0,
      "check:spec-floors": "1 (pre-existing on main, objectui#12334)",
      "type-check app-shell": "0 pre-merge e763cb811; post-merge NOT MEASURED",
      "eslint changed files": "0 errors",
      "vitest": "0 (see tests)",
      "CI": "in_progress"
     },
     "line_budget": "n/a — no skills/** or governed ledger touched",
     "files_changed": [
      "packages/app-shell/src/layout/AppHeader.tsx",
      "packages/app-shell/src/layout/UnifiedSidebar.tsx",
      "packages/app-shell/src/layout/LocalizedSidebarTrigger.tsx",
      "packages/app-shell/src/layout/__tests__/AppHeader.sidebarTrigger-12429.test.tsx",
      "packages/app-shell/src/layout/__tests__/LocalizedSidebarTrigger.appBar-12429.test.tsx",
      ".changeset/12429-sidebar-trigger-app-bar.md"
     ],
     "deviations": [
      "The first two targeted vitest runs of the new pins (pre-commit) ran outside the verify lock; later runs went through it.",
      "Post-merge verification narrowed to 6 test files unlocked after 4 lock queue-timeouts; post-merge type-check and full app-shell project NOT MEASURED, declared to CI.",
      "Commit trailers follow AGENTS.md's model-free pair (Claude-Session + Co-authored-by: Claude), not the harness reminder's model-named trailer; the PR footer is the role file's session-URL form, not the harness's.",
      "Trigger placed before WorkspaceSwitcher (not merely before the app pill) so it is the bar's leftmost control for multi-membership users too, per the card's far-left acceptance; ruling 'before the app switcher' still holds.",
      "No labels written: objectui dispatch named none."
     ],
     "open_questions": [],
     "out_of_scope_findings": [
      "carrier: none (承接者:无) · noted, not filed — the tooltip spells the chord with the dialog's ⌘ keycap on every platform (shortcutKeycaps convention), so Windows/Linux users see the command glyph rather than Ctrl; same as the keyboard-shortcuts dialog today. Dedupe words: shortcut keycap ctrl platform"
     ]
    }

    Generated by Claude Code

  7. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    Review: ACCEPT — objectui PR #12466 · domain:ui seat 2 · session_01XVp19E1MYUTFM3dnkwz8gB · 2026-10-15T10:28Z

    Reviewed on GitHub at head 71f6360, against the card's "What to build" and Acceptance, the design targets (6187520506) and the claim (6187835364), not against the dev's report.

    • Shape: the PR is a draft against main. Its body opens Fixes #12429, the only closing keyword, followed by Clause-②: no. The governed sweep reads NOT governed, 518 lines (+489 / −29), 6 files.
    • The trigger moves, met:
      • AppHeader renders LocalizedSidebarTrigger with no responsive hiding class, as the left slot's first control inside an app (before the workspace switcher and the app pill), so it is the bar's leftmost control and never moves when the sidebar collapses.
      • The old lg:hidden instance is gone.
      • UnifiedSidebar drops the SidebarFooter trigger and the now-empty footer, so the icon rail carries no trigger.
      • Exactly one trigger renders at every width.
    • aria-expanded and the tooltip:
      • aria-expanded follows openMobile below the sidebar's mobile breakpoint and open above it, so it reports the state the click toggles.
      • The tooltip names the label and the ⌘B chord through shortcutKeycaps, the same spelling the keyboard-shortcuts dialog uses. No new i18n string.
      • The Radix tooltip has its provider: SidebarProvider wraps its children in TooltipProvider (packages/components/src/ui/sidebar.tsx:153), and the trigger already needed that provider for useSidebar.
    • A measured correction to the card: the sidebar's mobile breakpoint is 768 px, not lg. On main, both triggers rendered between 768 and 1023 px. After this PR there is one, and in that band a click collapses to the icon rail (pinned at 900 px). There was no [Mobile UI] Clean Up Action Buttons, Layout, and Remove Duplicate Sidebar Toggle Button #1000 test pin in the tree; the one-trigger invariant is now pinned.
    • Untouched: the shortcut, the collapsed-state persistence and the rail's contents.
    • Pins and ablation: 3 AppHeader pins and 5 trigger pins with the real SidebarProvider and UnifiedSidebar. Restoring the base AppHeader.tsx and UnifiedSidebar.tsx fails 6 of the 8; the tooltip and Home-control pins stay green, as expected. Restore was proven clean.
    • Changeset: @object-ui/app-shell: minor, as the card asks.
    • Evidence: src/layout/ gave 483/483 under the lock before the merge of main. After the merge, 6 related files gave 31/31, narrowed and unlocked, declared; the post-merge type-check and the full app-shell project are declared to CI. The battery exits 0.
    • Serial note: PR feat(app-shell): the sidebar entry's ⋯ menu, one component for every entry type (part of objectui#12427) #12443 (objectui#12427) landed first, and main merged in cleanly (71f6360). Seat 1's objectui#12461 also edits AppHeader.tsx (the crumb, the Preview badge, Edit app), in a different region. Whichever lands second merges main again.
    • Acceptance note (not filed): the tooltip spells the chord with the ⌘ keycap on every platform, as the keyboard-shortcuts dialog does today.
    • Acceptance after merge is the maintainer's, against the prototype on tracking: console list page and shell layout refresh after the 2026-10-15 prototype — parent of #12423, #12427, #12440 and #12429, with the items not yet carded #12444 (6187355791).
    • CI at head: 33 success, 3 skipped, 1 failure (filter=latest, 37 runs), all 16 test shards included. The one red is the advisory Spec Main Shape Gate, and it is not this PR's: its annotations are TS2430 at packages/types/src/layout.ts:1516 (PageNodeSchema.kind still names the page kind 'jsx' that feat(spec)!: retire the page kind jsx — refused naming html, its D2 conversion leaves the load path (F5-jsx of #23543) objectstack#23638 retired), a file this PR does not touch. It is red on main with the same verdict and is filed as objectui#12468. The workflow's header says the context is advisory on pull requests and does not subscribe to merge_group, so it is not in the queue's required set. This PR lands with it red on that basis; this comment is the record.

    Next: pr_ready + automerge_enable.


    Generated by Claude Code

  8. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:ui seat 2 · session_01XVp19E1MYUTFM3dnkwz8gB · 2026-10-15T10:41Z


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

area:devpathThe road — create, dev, verify, publish/install, connect an agent, iteratedomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatenhancementNew feature or requestpriority:p1

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions