Repository navigation
fix(layout,app-shell): in the icon-mode sidebar a hidden group label no longer takes the click on the row above it (objectui#12744) - #12793
Merged
objectstack-fleet[bot] merged 3 commits intoOct 16, 2026
Conversation
…events in icon mode (objectui#12744) In the collapsed icon sidebar the SidebarGroupLabel primitive hides a label with opacity-0 and -mt-8 only, so the invisible label stays hit-testable over the lower half of the row just above its group. A centre click on that row's icon toggled the group (or Recent) instead of navigating. The labels measured to intercept now carry the primitive's own hiding condition as a pointer-events-none variant, through the className each call site passes (the primitive is a synced Shadcn file): NavigationRenderer's collapsible group label and its Favorites label, and UnifiedSidebar's Recent label. Claude-Session: https://claude.ai/code/session_01Vk15bz8xHtKGUfXfCm16h4 Co-authored-by: Claude <noreply@anthropic.com>
…ts variant (objectui#12744) Collapsed to icons, NavigationRenderer's collapsible group label and Favorites label and UnifiedSidebar's Recent label carry pointer-events-none under the variant the primitive hides them with (derived from its opacity-0 token). In the expanded sidebar none carries an unconditional pointer-events-none, a group label still toggles its group by mouse and keyboard, and the Recent label still opens and closes Recent. Claude-Session: https://claude.ai/code/session_01Vk15bz8xHtKGUfXfCm16h4 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vk15bz8xHtKGUfXfCm16h4 Co-authored-by: Claude <noreply@anthropic.com>
Contributor
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
This was referenced Oct 16, 2026
Contributor
Author
席位核对 — ACCEPT ·
|
objectstack-fleet
Bot
deleted the
claude/issue-12744-icon-sidebar-label-click
branch
October 16, 2026 21:24
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #12744
Clause-②: no (a class at the call sites; if the fix adds an exported wrapper to
@object-ui/components, the dev reads the built declaration closure and this line is corrected)What was wrong
In the collapsed (icon-mode) sidebar, the
SidebarGroupLabelprimitive hides a label withopacity-0and-mt-8and nothing else. The invisible label stays hit-testable.-mt-8moves it over the lower half of the row just above its group, andopacity-0makes it a stacking context that paints, and hit-tests, above that row. So a centre click on that row's icon landed on the label:asChildbutton) toggled its group;A note on the card's wording: the label covers the row above its group, not the group's first item. In the stock showcase that row is the top-level Page Authoring, under the Workspace group's label. The card's own evidence (Page Authoring's centre hits the Workspace label) is the same reading.
The fix
The labels measured to intercept now carry
group-data-[state=collapsed]:group-data-[collapsible=icon]:pointer-events-none. That variant is the primitive's own hiding condition, so a label stops taking pointer events exactly while it is hidden. It rides on theclassNameeach call site passes;packages/components/src/ui/sidebar.tsxis Shadcn-synced and is not touched (AGENTS.md #7).@object-ui/layout,NavigationRenderer: the collapsible group label, and the Favorites label.@object-ui/app-shell,UnifiedSidebar: the Recent label.The class sits in one module-local constant per file. It is not exported: both files are re-exported whole by their package entries.
cnkeeps the class, because the primitive sets nopointer-eventsclass (hypothesis 2). The pins read it on the rendered class list of the real primitive, and the browser reads computedpointer-events: none(below).The expanded sidebar is unchanged: a group's label still toggles its group by mouse and keyboard, and the Recent label still opens and closes Recent. Nothing is added to either package entry.
Which call sites intercept (hypothesis 1, measured)
Every
SidebarGroupLabeloutsideui/**is hit-testable while hidden. It intercepts only when a row sits directly above its group inside the same sidebar content.NavigationRenderer, collapsible groupasChildbuttonNavigationRenderer, FavoritesUnifiedSidebar, RecentUnifiedSidebar, AreaUnifiedSidebar, Starred (home arm)AppSchemaRenderer,AreaSwitcherAppSchemaRenderer. Its label is first in its content, the same position asUnifiedSidebar's Area label. ItsNavigationRendererlabels get this fixSidebarNav(@object-ui/layout) also rendersSidebarGroupLabel. It is not on the claim's file surface, and the repo has no mount of it (Acceptance notes).Shape 3: a shared fix was considered, not taken
The one fix shared by every call site that needs no
ui/**edit and no new export is a plain-CSS rule. It would go inpackages/components/src/sidebar-fixes.css, the sidebar override sheetsrc/index.tsalready imports, keyed on[data-sidebar="group-label"]under the collapsed icon.group. It was not taken for two reasons:custom/or the call site'sclassName, and a global stylesheet override would be a third.A
custom/wrapper would have to be exported from@object-ui/components, becauselayoutandapp-shellimport it by package name. That would widen a published surface. So the class goes at three call sites, and Clause-② staysno. The report puts the CSS route to the seat as an open question.Browser readings (before =
main0171c24, after = this branch)The console ran on Vite from this worktree, against
objectstack66d456d01examples/app-showcase(objectstack dev --fresh, backend only, no edits). Chromium was at 1280 px wide, with the sidebar collapsed to icons. For every visible sidebar icon the probe read three things:document.elementFromPointat its centre;The probe is a one-off script, not committed.
A. Showcase, groups as authored. The spec defaults
expandedtofalse, so every group starts closed and the rail shows 2 icons.B. Showcase, every group opened in the expanded sidebar, then collapsed through the app bar's toggle: 49 icons. Before: 40 navigate and 9 do not. After: 49 of 49 have their own centre and 0 dead rows, and all navigate. These are the 9 that failed before, each with the label that took its click:
The 9 hidden labels read computed
pointer-events: autobefore andnoneafter, withopacity: 0both times.C. A two-area app, with one row pinned: 7 icons. The app was created at runtime in the
--freshtemporary database withPUT /api/v1/meta/app/probe_areas_app, and no file was edited. Its areas are Work and Money. Work holds Projects, Tasks and a People group (Accounts, Contacts). Contacts was pinned through its row menu.Area label:
pointer-events: autobefore and after; it covers no row.Expanded sidebar, after. Workspace label: a mouse click opens the group (My Work shows), a second click closes it, and focus plus Enter opens it again. Space closes it. The label reads
pointer-events: auto,opacity: 1. A mouse click on the Recent label opens Recent: 0 rows, then 5.Tests
Two new pin files. Each renders the real
SidebarProvider/Sidebar/SidebarGroupLabel, which are source-aliased in the root vitest config:packages/layout/src/__tests__/NavigationRenderer.iconModeLabelHitTest-12744.test.tsxpackages/app-shell/src/layout/__tests__/UnifiedSidebar.iconModeRecentLabel-12744.test.tsxEach file holds two pins:
pointer-events-noneunder the variant the primitive hides them with. The expected token is derived from the primitive's own…:opacity-0token, so a primitive that changes its hiding condition turns the pin red. The test DOM computes no Tailwind, so the pin reads the class list; the browser readings above are the behavioural proof.pointer-events-none. The group label toggles its group by user-event click and by Enter. The Recent label opens and closes Recent. This pin guards against a regression: it is green onmaintoo, by design, since the expanded behaviour must not change.Ablations ran through
ablation-replace.mjsfrom../objectstack. Every mutation was confirmed on disk (anchor count 1 to 0, blob changed), and every restore was proven (blob equalsHEAD,git diff HEADempty). The subjects are imported fromsrc, so no build sits between the mutation and the run.NavigationRenderer: the class constant emptied (themainshape)NavigationRenderer: the class made unconditionalpointer-events-noneUnifiedSidebar: the class constant emptiedUnifiedSidebar: the class made unconditionalLocal gates (all on
6a571d0)turbo run build --filter='@object-ui/layout^...', then--filter='@object-ui/app-shell^...',--concurrency=2. Exit 0 both times; the second run: 28 of 28 tasks successful.pnpm --filter @object-ui/layout --filter @object-ui/app-shell run type-check: exit 0. Both scripts echoedtsc --noEmit && tsc -p tsconfig.test.json.--listFilesshows each new test in its package's test program.pnpm exec vitest run --maxWorkers=2 packages/layout/ packages/app-shell/src/layout/: exit 0,Test Files 132 passed (132),Tests 982 passed (982).eslinton the four changed source and test files: exit 0, 0 errors. Every warning sits on an unchanged line; the new test files have none.node scripts/check-changeset-presence.mjs: exit 0, "4 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)".pnpm check:new-line-citations: exit 0,0 new citation(s).The rest of the farm is CI's.
Acceptance notes
These are not filed from this PR. The two that read as defects are in the report for the seat to judge.
SidebarContentisoverflow-hiddenin icon mode, and a mouse wheel over the rail leftscrollTopat 0, so those rows cannot be reached until the sidebar expands.opacity: 0, and Enter then toggles a group the user cannot see. This fix leaves keyboard behaviour as it was, as the seat's shape asks.expandedtofalse, so the stock showcase rail shows 2 icons. Onmain, a stray click on the row above happened to open a group; that accident is gone.UnifiedSidebarhas no mount in the console. That arm holds the Starred label.HomeLayout,OrganizationsLayoutandAiChatPageset the navigation context tohomebut render noUnifiedSidebar.ConsoleLayout, its only mount, setsapp. Its Starred label therefore has no reachable reading.SidebarNav(@object-ui/layout). It defaults tocollapsible="icon", and labels after the first sit under a separator below the previous group's last row. Reading the geometry, the overlap would be the lower part of that row, short of its centre. It has no mount in this repo, and nothing here measured it.The work was done in session
https://claude.ai/code/session_01Vk15bz8xHtKGUfXfCm16h4.Generated by Claude Code