Skip to content

fix(components): a stacked resizable group gets a divider, not a 1px sliver - #3024

Merged
os-zhuang merged 1 commit into
mainfrom
claude/suspicious-sanderson-07baa0
Jul 30, 2026
Merged

fix(components): a stacked resizable group gets a divider, not a 1px sliver#3024
os-zhuang merged 1 commit into
mainfrom
claude/suspicious-sanderson-07baa0

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

The bug

packages/components/src/ui/resizable.tsx is the react-resizable-panels v3 Shadcn file: every stacked-group style on ResizableHandle is keyed on data-[panel-group-direction=vertical]. The installed major is v4.12.2, which never emits that attribute — the only ones it writes are data-group, data-panel, data-separator, data-disabled and data-testid.

So all seven variants (h-px, w-full, after:left-0, after:h-1, after:w-full, after:-translate-y-1/2, after:translate-x-0) plus [&[data-panel-group-direction=vertical]>div]:rotate-90 were dead selectors, and a stacked divider kept the base w-px. (ResizablePanelGroup's data-[…]:flex-col is dead too but harmless — the library writes flex-direction as an inline style.)

Confirmed in a browser first

Rendered through the unpatched ui/resizable in a zero-backend console Vite entry, orientation="vertical", in a 480px group:

separator box ::after drag target grip grabbable off-grip
stacked, before 1 × 16 px (height is only the grip) 4 × 16 px, pinned to the group's left edge not rotated ✗ — at 85% across the split the hit target is a panel
stacked, after 478 × 1 px 478 × 4 px rotate: 90deg ✓ — a real off-grip drag moves the split 50 → 75.21
side-by-side, after 1 × 238 px 4 × 238 px not rotated unchanged

document.querySelectorAll('[data-panel-group-direction]').length was 0 for the whole document.

Visually, before: no rule at all between the stacked panels — just an orphaned, unrotated grip floating at the far-left edge. After: a full-width hairline with the grip rotated and centred on it.

The fix

packages/components/src/ui/** is a no-touch Shadcn-sync zone (AGENTS.md #7), so this is a wrapper — packages/components/src/custom/resizable.tsx — that re-keys those styles onto aria-orientation, which v4 does emit on the separator.

Upstream Shadcn has since been regenerated for v4 and moved to exactly this attribute, so the class strings here match upstream rather than being hand-rolled. (Its file is otherwise a full Tailwind-v4/data-slot generation that only 2 of this repo's 48 ui/ files have adopted, so a wholesale re-sync was not the smaller change.)

The value reads backwards, deliberately — aria-orientation describes the separator, not the group:

group orientation="horizontal" → panels side-by-side → separator aria-orientation="vertical"
group orientation="vertical"   → panels stacked      → separator aria-orientation="horizontal"  ← styled here

ui/index.ts no longer re-exports ./resizable; custom/ owns the public Resizable* names. That keeps a single canonical handle (no consumer can reach the unpatched one through @object-ui/components) and avoids a TS2308 collision, since src/index.ts star-exports ./ui and ./custom side by side. The public export surface is unchanged — verified by type-checking @object-ui/app-shell against a freshly built dist, since it consumes all three names via the package barrel.

Consumers repointed: the form renderer (fieldPanes), the SDUI resizable renderer, and custom/navigation-overlay.tsx.

Affected surfaces

Tests

packages/components/src/__tests__/resizable-orientation.test.tsx (5 tests) pins the class contract and the attribute it depends on, including a guard that asserts every aria-[…] variant on the handle matches the real rendered DOM — the check that would have caught the original bug and that catches the next rename. Plus one case in form-field-panes.test.tsx tying it to the real consumer.

Both mutations go red:

  • keying on aria-[orientation=vertical] (the plausible silent mistake — reading it as the group's orientation) → 4 failures
  • reverting the wrapper to a pass-through (pre-fix state) → 3 failures

Resolved geometry isn't asserted in vitest — happy-dom has no Tailwind — so that half is the browser verification above.

Verification

  • packages/components: 53 files / 410 tests pass
  • app-shell/views/metadata-admin + plugin-grid + plugin-form: 176 files / 1526 tests pass
  • type-check clean for @object-ui/components and @object-ui/app-shell
  • eslint clean on every changed file (0 errors; only pre-existing warnings in form.tsx)

No changeset — pure bug fix, per AGENTS.md.

Noted, not fixed here

custom/navigation-overlay.tsx:466 and app-shell/.../ResourceEditPage.tsx:2064 still pass the v3 prop name direction="horizontal", which v4 ignores (it wants orientation) and which leaks to the DOM as an attribute. Both are horizontal-only and horizontal is v4's default, so there is no visual bug today — but the as React.FC<any> casts at both sites, and the comment blaming vite-plugin-dts for "not resolving the direction prop type", are really this same half-finished v3→v4 migration. Left out to keep this PR to the visual defect.

🤖 Generated with Claude Code

…sliver

`ui/resizable.tsx` is the react-resizable-panels **v3** Shadcn file: every
stacked-group style on its handle is keyed on
`data-[panel-group-direction=vertical]`. v4 — the installed major (4.12.2) —
never emits that attribute; the only ones it writes are `data-group`,
`data-panel`, `data-separator`, `data-disabled` and `data-testid`. So all seven
variants plus `[&[data-panel-group-direction=vertical]>div]:rotate-90` were dead
selectors, and a stacked divider kept the base `w-px`.

Measured in a browser before the fix: the divider rendered 1px wide × 16px tall
— its whole height coming from the grip — with a 4×16px `::after` drag target
pinned to the group's left edge and an unrotated grip. Off the grip, the
separator was not the hit target anywhere along the split, so 99% of the divider
could not be grabbed.

`ui/**` is a no-touch Shadcn-sync zone (AGENTS.md #7), so the fix is a wrapper in
`custom/resizable.tsx` that re-keys those styles onto `aria-orientation`, which
v4 *does* emit on the separator — the same attribute upstream Shadcn moved to
when it regenerated this component for v4.

The value reads backwards, deliberately: `aria-orientation` describes the
separator, not the group, so an `orientation="vertical"` group (stacked panels)
has an `aria-orientation="horizontal"` divider.

`ui/index.ts` no longer re-exports `./resizable`; `custom/` owns the public
`Resizable*` names, so no consumer can reach the unpatched handle through
`@object-ui/components` (and the two barrels cannot collide on TS2308). The
public export surface is unchanged.

After, in a browser: 478×1px rule, 478×4px hit strip, grip at `rotate: 90deg`,
and a real off-grip drag moves the split 50 → 75.21. Side-by-side is untouched
(1×238px, unrotated grip).

Affected surfaces: `FormSchema.fieldPanes` with `fieldPanesOrientation:
'vertical'` (#2153) and the SDUI `resizable` renderer with
`direction: 'vertical'`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectui Ignored Ignored Jul 30, 2026 11:03am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 27.9 KB 350 KB
Entry file index-DNsYwB6L.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 8.20KB 2.97KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 7.57KB 2.97KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 22.10KB 4.37KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.12KB 3.41KB
auth (LoginForm.js) 17.86KB 5.29KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.43KB 2.09KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 35.76KB 9.11KB
auth (createAuthenticatedFetch.js) 4.37KB 1.69KB
auth (index.js) 2.25KB 1.01KB
auth (org-roles.js) 6.72KB 2.85KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 4.91KB 0.87KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 18.38KB 4.49KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 3.65KB 1.42KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.25KB 0.53KB
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) 458.27KB 100.20KB
core (index.js) 2.16KB 0.78KB
create-plugin (index.js) 9.28KB 2.98KB
data-objectstack (index.js) 134.67KB 34.24KB
fields (index.js) 222.07KB 54.35KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 2.46KB 0.96KB
i18n (pickLocalized.js) 1.70KB 0.83KB
i18n (provider.js) 5.37KB 1.72KB
i18n (useObjectLabel.js) 25.17KB 5.80KB
i18n (useSafeTranslation.js) 3.26KB 1.44KB
layout (index.js) 38.45KB 10.67KB
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.74KB
mobile (index.js) 1.50KB 0.62KB
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) 2.53KB 0.85KB
mobile (useResponsive.js) 0.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 4.05KB 1.53KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 6.84KB 2.42KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 3.67KB 1.12KB
permissions (evaluator.js) 4.41KB 1.44KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.55KB 0.71KB
plugin-ai (index.js) 15.71KB 3.79KB
plugin-calendar (index.js) 44.90KB 12.35KB
plugin-charts (index.js) 60.52KB 17.11KB
plugin-chatbot (index.js) 180.09KB 42.72KB
plugin-dashboard (index.js) 111.59KB 28.74KB
plugin-designer (index.js) 210.56KB 42.56KB
plugin-detail (index.js) 216.52KB 53.02KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 104.83KB 25.30KB
plugin-gantt (index.js) 162.26KB 39.53KB
plugin-grid (index.js) 180.86KB 47.45KB
plugin-kanban (index.js) 47.82KB 13.18KB
plugin-list (index.js) 102.39KB 24.18KB
plugin-map (index.js) 16.80KB 5.24KB
plugin-markdown (index.js) 13.65KB 4.67KB
plugin-report (index.js) 40.32KB 10.53KB
plugin-timeline (index.js) 25.75KB 7.32KB
plugin-tree (index.js) 8.36KB 2.81KB
plugin-view (index.js) 85.88KB 20.99KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.71KB 3.53KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.67KB 2.37KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 19.28KB 6.38KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.02KB 0.55KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 3.47KB 1.54KB
sdui-parser (parse.js) 10.04KB 2.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 0.77KB 0.41KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (index.js) 2.00KB 0.96KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 0.20KB 0.18KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.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 (spec-report.js) 5.04KB 1.93KB
types (system-fields.js) 2.39KB 1.17KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 1.08KB 0.64KB
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

@os-zhuang
os-zhuang merged commit 8eee174 into main Jul 30, 2026
16 checks passed
@os-zhuang
os-zhuang deleted the claude/suspicious-sanderson-07baa0 branch July 30, 2026 11:07
os-zhuang added a commit that referenced this pull request Jul 30, 2026
… rename to `orientation` (#3025)

react-resizable-panels v4 renamed `PanelGroup`'s `direction` prop to
`orientation`. Two call sites were still passing the v3 name, so v4
ignored it and React forwarded the unknown prop to the DOM as a stray
`direction="horizontal"` attribute on the group div.

Both groups are horizontal-only and `horizontal` is v4's default, so
nothing rendered wrong — this is dead-prop cleanup, the tail of the
migration whose visible half was #3024.

Each site also carried `const PanelGroup = ResizablePanelGroup as
React.FC<any>`, and those casts are why the dead prop survived the v4
bump: they erased the props to `any`, so `direction` type-checked
against nothing. Both comments blamed the toolchain — vite-plugin-dts
"not resolving the direction prop type correctly", and a prop that
"does not always narrow cleanly in our TS config" — but neither was a
narrowing problem. `GroupProps` simply has no `direction` key in v4.

Dropping the casts makes the rename self-enforcing: restoring
`direction` on the un-cast `ResizablePanelGroup` now fails with
TS2322 rather than silently reaching the DOM.

ResourceEditPage's `ResizablePanel` props are unaffected — `panelRef`,
`collapsible`, `collapsedSize` and `onResize` are all valid v4
`PanelProps` and were never covered by the cast (it only ever wrapped
the group), so no narrowly-typed workaround is needed.

Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
os-zhuang added a commit that referenced this pull request Jul 30, 2026
…p the sync from breaking the build, and finish the v4 migration in it (#3029)

`ui/resizable.tsx` looks like a Shadcn-synced no-touch file (AGENTS.md #7).
It is not one, and has not been since `00b61d6d8` ("upgrade shadcn
components and fix build issues") hand-migrated its imports from v3's
`import * as ResizablePrimitive` to v4's `{ Group, Panel, Separator }`.

Upstream never followed. Both registry endpoints — `styles/default` and
`styles/new-york` — still serve the v3 file, which reaches for
`ResizablePrimitive.PanelGroup` and `PanelResizeHandle`. Neither name
exists in react-resizable-panels v4.12.2: it exports only `Group`,
`Panel` and `Separator`. So `pnpm shadcn:update-all` — an advertised
script — overwrites this file with code that cannot compile. The commit
title above is what that looks like after the fact; it has already
happened once.

Defused by listing `resizable` under `customComponents` in
`shadcn-components.json`, which is the manifest's existing mechanism for
this: `--update-all` iterates `Object.keys(manifest.components)`, and
`--update resizable` now refuses with "not found in Shadcn registry".

With the file no longer synced, the v4 fix belongs in it rather than in a
wrapper built to avoid touching it. #3024 added
`custom/resizable.tsx` to re-key the stacked-divider styles off the dead
`data-[panel-group-direction=vertical]` and onto the `aria-orientation`
attribute v4 actually emits — 8 compensating classes shadowing 8 dead
ones in a file one import away. Those 8 now live in `ui/resizable.tsx`
directly and `custom/resizable.tsx` is a plain re-export, kept as the
sole public export site because `src/index.ts` star-exports `./ui` and
`./custom` side by side and exporting `Resizable*` from both collides
(TS2308).

The group's `data-[panel-group-direction=vertical]:flex-col` is dropped
outright: v4 writes `flex-direction: row | column` as an inline style off
`orientation`, which outranks any class.

Behaviour is unchanged, and provably so — diffing the merged class string
before against after yields exactly the 8 dead selectors removed and
nothing added, with `tailwind-merge` dropping none of the 23 survivors
now that base and variants share one `cn()` call.

Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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.

1 participant