Skip to content

fix(app-shell): the icon-mode sidebar rail scrolls vertically when its rows overflow (objectui#12794) - #12846

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-12794-icon-rail-scroll
Oct 17, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-12794-icon-rail-scroll

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #12794
Clause-②: no

What changed

In the collapsed (icon-mode) console sidebar the rail now scrolls vertically when its rows overflow, as the expanded sidebar does. Before, the rows below the fold could not be reached until the sidebar was expanded again.

The SidebarContent primitive clips the rail with group-data-[state=collapsed]:group-data-[collapsible=icon]:overflow-hidden. UnifiedSidebar now passes one class constant, ICON_RAIL_SCROLLS_VERTICALLY, which sits beside the objectui#12744 constant and uses the same icon-mode variant:

  • overflow-auto: tailwind-merge (cn) drops the primitive's same-variant overflow-hidden only for a later same-variant overflow-* shorthand. Measured: with only overflow-y-auto / overflow-x-hidden, both the primitive's overflow-hidden and the longhands survive the merge, and stylesheet order decides the result. So the shorthand is the pin.
  • overflow-x-hidden: the horizontal axis stays clipped (the rail's content is 109px wide in a 47px rail) and the rail never scrolls sideways.
  • [scrollbar-width:none]!: the rail draws no scrollbar. With the console's thin bar visible, the rail's clientWidth drops from 47 to 37, the 32px buttons run under the bar, and the icons sit 5.5px off the scroll box's centre. The class is important because the console themes every scrollbar with an unlayered * rule, which outranks layered utilities. The wheel, touch and keyboard focus still scroll the rail.

Off icon mode nothing changes: the expanded sidebar and the mobile sheet keep the primitive's overflow-auto and the host's scrollbar. packages/components/src/ui/sidebar.tsx (Shadcn-synced) is untouched. No export, prop, type member or i18n key moves.

Live readings

Chromium at 1280 x 800, objectstack examples/app-showcase (objectstack dev --fresh), console Vite from this worktree. Icon mode with every group opened, collapsed with Ctrl+B, starting at scrollTop 0. Visible-scrollbar readings run without Playwright's default --hide-scrollbars.

reading main 4839f5c this branch b1c05f1
rows present 49 49
rows below the fold at scrollTop 0 29 29
scrollTop after one wheel (delta 400) over the rail 0 400
scrollTop after wheeling to the end 0 1165 (scrollHeight 1917 minus clientHeight 752)
last row (Renewals Pipeline) inside the rail and hit-testable no (top 1909, rail bottom 800) yes (744 to 776)
click on the last row navigates no yes
computed overflow-x / overflow-y hidden / hidden hidden / auto
scrollLeft after a sideways wheel 0 0
rail offsetWidth / clientWidth 47 / 47 47 / 47
first icon centre vs the scroll box +0.5px +0.5px
rejected option, thin bar kept: offsetWidth / clientWidth, icon offset n/a 47 / 37, +5.5px
CONTROL expanded: offsetWidth / clientWidth, overflow, scrollbar-width 255 / 245, auto / auto, thin 255 / 245, auto / auto, thin
CONTROL mobile sheet (390 x 844): overflow, scrollbar-width auto / auto, thin auto / auto, thin

Screenshots (evidence branch claude/issue-12794-evidence, not merged here):

Tests (at b1c05f1)

  • New UnifiedSidebar.iconRailScroll-12794.test.tsx mounts the real UnifiedSidebar with the real SidebarProvider / Sidebar / SidebarContent, using the harness of the objectui#12744 test. It derives the icon-mode prefix from the bare primitive's own :overflow-hidden token. Collapsed: the content has the variant's overflow-auto, overflow-x-hidden and [scrollbar-width:none]!, and no icon-mode overflow-hidden, overflow-y-hidden or clip survives. CONTROL, expanded: every class outside the variant equals the bare primitive's list with the old pt-2.
  • Ablation, each leg restored to the HEAD blob with git diff HEAD empty: (A) call site back to pt-2 turns the collapsed test red; (B) shorthand swapped for overflow-y-auto turns it red, which is the merge trap above; (C) an unconditional overflow-x-hidden turns both tests red.
  • @object-ui/app-shell in full, as pnpm exec vitest run packages/app-shell/ --shard=K/8 for K = 1..8: 1512 of 1512 files, 1511 passed and 1 skipped; 14120 tests passed, 9 skipped, 0 failed. A single unsharded run cannot finish inside the container's command limits.
  • type-check, build and eslint on the touched files for @object-ui/app-shell: exit 0 (lint warnings all predate this diff). check:i18n-keys, check:i18n-drift, check-changeset-presence, check:new-line-citations: exit 0.

Acceptance notes

  • packages/layout AppSchemaRenderer.tsx also renders Sidebar collapsible="icon" with a bare SidebarContent, and AppShell forwards defaultOpen. Read from source only and not measured: a published AppSchemaRenderer collapsed to icons may clip its rail the same way. The dispatch ruled the packages/layout sidebars out of scope, so this PR does not touch them. No carrier.
  • objectui#12795 (the keyboard half of the icon-mode family) remains open and is not addressed here.

Session: https://claude.ai/code/session_0129XjRmiNNiNRzQ5twZ75Vr


Generated by Claude Code

In the collapsed (icon-mode) sidebar the SidebarContent primitive clips
with a variant-scoped overflow-hidden, so rows below the fold could not be
reached until the sidebar expanded (objectui#12794).

UnifiedSidebar passes the same variant with overflow-auto (which
tailwind-merge lets replace the primitive's shorthand) plus
overflow-x-hidden, and hides the rail's scrollbar so a classic bar does
not take its width out of the 3rem rail. Off icon mode the class list is
unchanged.

Claude-Session: https://claude.ai/code/session_0129XjRmiNNiNRzQ5twZ75Vr
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 — ACCEPT · objectui#12794 · head b1c05f1 · 2026-10-17T02:26Z

Seat domain:ui#4, session session_01SJn1GEkrbbBQkhGC26wEDw. Checked against GitHub and the diff, not against the report (relayed on the card). The seat opened this PR from the dev's drafts (degraded channel). The body is the dev's draft verbatim; the title adds the card number.

Checklist

  • Form:

    • Draft → main, assignee os-sales.
    • The first line is Fixes #12794, and the second is Clause-②: no, as the claim declares (6226787250).
    • One commit, carrying only the model-free trailer pair.
    • git merge-tree against current main is clean. 3 files, +215 / −1: UnifiedSidebar.tsx, one new test, one changeset. No PNG, and packages/components/src/ui/sidebar.tsx is not in the diff.
  • Rulings, as bound:

    • Icon mode scrolls vertically: one class constant, ICON_RAIL_SCROLLS_VERTICALLY, beside the objectui#12744 constant and under the primitive's own icon-mode variant: overflow-auto (it replaces the primitive's same-variant overflow-hidden in the merge), overflow-x-hidden (the rail never scrolls sideways), and [scrollbar-width:none]! (no bar to push the icons off-centre).
    • At the call site only: the Shadcn-synced primitive is untouched.
    • Off icon mode nothing changes: the expanded sidebar and the mobile sheet keep overflow-auto and the host's thin scrollbar (CONTROL rows in the table).
  • Mechanism assumptions:

    • 1 held.
    • 2 was partly falsified, a good result: packages/layout's AppSchemaRenderer.tsx also renders collapsible="icon". It stays out of scope, as ruled, and is noted in the PR.
    • 3 measured: a longhand pair leaves the primitive's overflow-hidden in the class list; only the same-variant shorthand removes it. The pin's ablation leg B turns red on exactly that trap.
    • 4 measured: a visible thin bar takes the rail's clientWidth from 47 to 37 and puts the icons 5.5 px off centre; with no bar it stays 47 / 47 and 0.5 px.
  • Live readings (showcase, 1280 × 800, every group open):

    reading before after
    rows below the fold 29 of 49 29 of 49
    scrollTop after one wheel 0 400
    scrollTop at the end 0 1165
    the last row unreachable reachable, hit-testable, navigates on click
    rail width (offsetWidth / clientWidth) 47 / 47 47 / 47
  • Screenshots, read by the seat (never-merged claude/issue-12794-evidence at 98855b6):

    • icon rail scrolled to its last row (Renewals Pipeline), light and dark: icons centred, no scrollbar;
    • expanded sidebar, light (CONTROL).

    Sent to the maintainer at 2026-10-17T02:26Z.

  • Tests:

    • New UnifiedSidebar.iconRailScroll-12794.test.tsx through the real UnifiedSidebar and the real primitives. It derives the icon-mode prefix from the primitive's own token, so a drift in the primitive fails loudly. Collapsed: the three classes are present and no icon-mode overflow-hidden survives. CONTROL: the expanded list equals the bare primitive's.
    • Three ablations, each red, each restored to an empty git diff HEAD.
    • The full app-shell suite in 8 shards: 1,512 files, 14,120 tests passed, 0 failed.
    • type-check, build, eslint on touched files (0 errors), the i18n checks, changeset presence and line citations: all exit 0.
  • Changeset: @object-ui/app-shell patch; user-facing prose.

  • CI: running at this record. Landing waits for green.

Rulings

  1. Scrollbar treatment: A, as built. The bar is hidden in icon mode only. Reachability is the measured defect, wheel, touch and focus still scroll, and the cut-off rows at the rail's edge show there is more.
  2. packages/layout's AppSchemaRenderer icon rail: read from source and not measured, so not filed under the contract. It is noted in the PR's acceptance notes.
  3. The sharded full suite: accepted. A single unsharded app-shell run cannot finish inside the container's limits.
  4. The contract conflict: as dispatched. The relay is refused from the dev's container, so the seat opened the PR and relayed the report.
  5. objectui#12795 (the keyboard half, same file) lands after this PR.

Implemented-by: cloud session session_0129XjRmiNNiNRzQ5twZ75Vr, branch claude/issue-12794-icon-rail-scroll


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 75 chunks) 3134.0 KB 3744.1 KB
Main entry chunk (gzip) 75.7 KB 350 KB
Entry file index-WZBOYCD0.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 21.42KB 7.85KB
app-shell (runtime-config.js) 23.60KB 8.19KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 42.48KB 11.25KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 20.67KB 6.12KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.74KB 3.94KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.62KB 0.83KB
auth (authStyles.js) 5.27KB 1.86KB
auth (createAuthClient.js) 42.84KB 11.66KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.26KB 8.03KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (commentAuthorship.js) 1.39KB 0.78KB
collaboration (index.js) 1.93KB 0.85KB
collaboration (useCollaborationTranslation.js) 6.50KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 606.28KB 146.84KB
core (index.js) 10.18KB 4.04KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 248.83KB 69.44KB
fields (index.js) 291.36KB 74.46KB
i18n (LocalizationContext.js) 2.92KB 1.42KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.52KB 2.39KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 36.87KB 9.88KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 40.79KB 11.90KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 15.35KB 5.51KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.82KB 2.38KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.04KB 3.92KB
plugin-calendar (index.js) 54.94KB 16.05KB
plugin-charts (index.js) 84.97KB 23.36KB
plugin-chatbot (index.js) 210.60KB 49.94KB
plugin-dashboard (index.js) 146.23KB 39.59KB
plugin-designer (index.js) 233.95KB 50.08KB
plugin-detail (index.js) 263.70KB 69.73KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 180.22KB 46.59KB
plugin-gantt (index.js) 178.36KB 45.08KB
plugin-grid (index.js) 271.78KB 75.39KB
plugin-kanban (index.js) 62.17KB 19.22KB
plugin-list (index.js) 138.78KB 35.68KB
plugin-map (index.js) 27.07KB 8.96KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 46.66KB 13.13KB
plugin-timeline (index.js) 39.21KB 11.85KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 94.68KB 24.36KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 12.07KB 3.68KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 120.23KB 39.46KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.07KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.81KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 1.13KB 0.65KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 5.78KB 2.70KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (node-slots.js) 7.18KB 2.34KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-approval-decision.js) 0.20KB 0.20KB
types (record-components.js) 0.20KB 0.19KB
types (record-panels.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.26KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.48KB 3.50KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Landing · head b1c05f1 · 2026-10-17T02:38Z

  • Head since the ACCEPT: unchanged, b1c05f1. The ACCEPT 6228492877 was written at this head. No contract review is owed (Clause-②: no).
  • Checks on b1c05f1: 36 runs, 33 green and 3 expected skips, none red. That includes Repo Gates, Build & E2E, all test shards, Type Check, Lint, Changeset Bump Policy and Bundle Analysis at 3,134.0 KB against the 3,744.1 KB ceiling.
  • Merge state: clean against main 5d69b0e.
  • Commit messages: the one commit carries only the model-free trailer pair, so the squash adds no model name to main.
  • Action: mark it ready for review and enable auto-merge through the merge queue. Fixes #12794 closes the card on merge, and objectui#12795 (the keyboard half, same file) becomes dispatchable after it.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 17, 2026 02:39
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 17, 2026
Merged via the queue into main with commit 0812a19 Oct 17, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-12794-icon-rail-scroll branch October 17, 2026 02:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

console sidebar (icon mode): the collapsed rail does not scroll, so rows below the fold cannot be reached until the sidebar expands

2 participants