Skip to content

Commit 9969e9f

Browse files
os-zhuangclaude
andauthored
fix(components): resizable is a diverged file, not a synced one — stop 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>
1 parent 4a74ea6 commit 9969e9f

5 files changed

Lines changed: 90 additions & 93 deletions

File tree

packages/components/shadcn-components.json

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -230,13 +230,6 @@
230230
],
231231
"registryDependencies": []
232232
},
233-
"resizable": {
234-
"source": "https://ui.shadcn.com/r/styles/default/resizable.json",
235-
"dependencies": [
236-
"react-resizable-panels"
237-
],
238-
"registryDependencies": []
239-
},
240233
"scroll-area": {
241234
"source": "https://ui.shadcn.com/r/styles/default/scroll-area.json",
242235
"dependencies": [
@@ -421,6 +414,13 @@
421414
"description": "Custom ObjectUI component - Keyboard key display",
422415
"dependencies": []
423416
},
417+
"resizable": {
418+
"description": "Diverged from Shadcn - hand-migrated to react-resizable-panels v4. Upstream still ships v3 (imports PanelGroup/PanelResizeHandle, which v4 does not export), so re-syncing would break the build. Restore to `components` only once upstream regenerates for v4.",
419+
"dependencies": [
420+
"react-resizable-panels"
421+
],
422+
"divergedFrom": "https://ui.shadcn.com/r/styles/default/resizable.json"
423+
},
424424
"spinner": {
425425
"description": "Custom ObjectUI component - Loading spinner",
426426
"dependencies": []

packages/components/src/__tests__/resizable-orientation.test.tsx

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,22 @@
77
*/
88

99
/**
10-
* The stacked resizable divider — `custom/resizable.tsx`.
10+
* The stacked resizable divider — `ui/resizable.tsx`.
1111
*
12-
* `ui/resizable.tsx` is the react-resizable-panels **v3** Shadcn file: its
13-
* whole stacked-group appearance hangs off `data-[panel-group-direction=vertical]`,
12+
* That file was the react-resizable-panels **v3** Shadcn file: its whole
13+
* stacked-group appearance hung off `data-[panel-group-direction=vertical]`,
1414
* an attribute v4 (the installed major) never emits. So in a stacked group the
1515
* divider kept the base `w-px` and came out a 1px-wide, 16px-tall sliver — its
1616
* only height being the grip — instead of a full-width hairline, with a 4x16px
1717
* drag target pinned to the group's left edge and an unrotated grip.
18-
* `custom/resizable.tsx` re-keys those styles onto `aria-orientation`, which v4
19-
* does emit on the separator.
18+
*
19+
* #3024 first fixed this from a `custom/resizable.tsx` wrapper, to avoid editing
20+
* a Shadcn-synced file (AGENTS.md #7). That file turned out not to be synced at
21+
* all — it is hand-migrated to v4 and upstream still ships v3 — so the styles
22+
* now live in `ui/resizable.tsx` directly, keyed on the `aria-orientation`
23+
* attribute v4 does emit, and the wrapper is a plain re-export. These tests are
24+
* unchanged by that move: they assert on the rendered separator, not on which
25+
* module supplied the classes.
2026
*
2127
* These tests pin the *class contract* and the attribute it depends on; happy-dom
2228
* has no Tailwind, so resolved geometry (478x1px rule, 478x4px hit strip, grip at
@@ -67,7 +73,7 @@ function ariaVariantsOn(el: HTMLElement): Array<[string, string]> {
6773
});
6874
}
6975

70-
describe('custom/resizable — the stacked divider (react-resizable-panels v4)', () => {
76+
describe('ui/resizable — the stacked divider (react-resizable-panels v4)', () => {
7177
it('gives the stacked divider aria-orientation=horizontal, not vertical', () => {
7278
// The inversion that makes this bug easy to reintroduce: the group stacks
7379
// vertically, so the rule between the panels runs horizontally.
@@ -123,10 +129,10 @@ describe('custom/resizable — the stacked divider (react-resizable-panels v4)',
123129
}
124130
});
125131

126-
it('documents why the wrapper exists: v4 emits no panel-group-direction', () => {
127-
// If this ever fails, the library started emitting the v3 attribute again
128-
// and `ui/resizable.tsx`'s own variants would carry the stacked case — at
129-
// which point this wrapper is redundant rather than load-bearing.
132+
it('documents why the styles were re-keyed: v4 emits no panel-group-direction', () => {
133+
// If this ever fails, the library started emitting the v3 attribute again,
134+
// at which point upstream Shadcn's own `data-[panel-group-direction]`
135+
// variants would work and this file could go back to being synced.
130136
const { container } = renderGroup('vertical');
131137
expect(container.querySelectorAll('[data-panel-group-direction]')).toHaveLength(0);
132138
});

packages/components/src/custom/resizable.tsx

Lines changed: 21 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -8,78 +8,30 @@
88

99
'use client';
1010

11-
import * as React from 'react';
12-
13-
import { cn } from '../lib/utils';
14-
import {
15-
ResizableHandle as ShadcnResizableHandle,
16-
ResizablePanel,
17-
ResizablePanelGroup,
18-
} from '../ui/resizable';
19-
2011
/**
21-
* Orientation-correct `ResizableHandle`.
22-
*
23-
* `ui/resizable.tsx` (a no-touch Shadcn-sync file, AGENTS.md #7) is from the
24-
* `react-resizable-panels` **v3** generation: every one of its stacked-group
25-
* styles is keyed on `data-[panel-group-direction=vertical]`. v4 — the
26-
* installed major — never emits that attribute. The only data attributes it
27-
* puts in the DOM are `data-group`, `data-panel`, `data-separator`,
28-
* `data-disabled` and `data-testid`, so all seven variants plus the grip's
29-
* `[&[data-panel-group-direction=vertical]>div]:rotate-90` are dead selectors.
30-
*
31-
* The visible consequence: in a **stacked** group the divider kept the base
32-
* `w-px` and rendered as a 1px-wide, 16px-tall sliver (its whole height came
33-
* from the grip) instead of a full-width 1px-tall rule, and the grip never
34-
* rotated. Measured in a browser before this wrapper existed: 1×16px, with a
35-
* 4×16px `::after` hit area pinned to the group's left edge.
36-
*
37-
* What v4 *does* emit on the separator is `aria-orientation`, so that is what
38-
* these variants key on — matching upstream Shadcn, which has since been
39-
* regenerated for v4 the same way.
40-
*
41-
* ⚠️ The attribute value is the **inverse** of the group's `orientation`, and
42-
* deliberately so: `aria-orientation` describes the separator, not the group.
43-
* A `orientation="vertical"` group stacks its panels, so the divider between
44-
* them is a *horizontal* line — hence `aria-orientation="horizontal"`.
45-
*
46-
* group orientation="horizontal" → panels side-by-side → separator aria-orientation="vertical" (base classes)
47-
* group orientation="vertical" → panels stacked → separator aria-orientation="horizontal" (these classes)
48-
*
49-
* Each variant below is the live counterpart of a dead `data-[…]` one still
50-
* carried by the base class string. They win on specificity: an attribute
51-
* selector scores above the bare utility class it overrides, which is the same
52-
* mechanism upstream relies on within its single file.
12+
* The public `Resizable*` export site.
13+
*
14+
* This module used to wrap `ResizableHandle` to re-key its stacked-group styles
15+
* from the dead `data-[panel-group-direction=vertical]` onto the
16+
* `aria-orientation` attribute react-resizable-panels v4 actually emits (#3024).
17+
* That fix now lives in `../ui/resizable` itself, so there is nothing left to
18+
* wrap and all three components pass straight through.
19+
*
20+
* Editing `ui/` is normally off-limits (AGENTS.md #7), but that file is no
21+
* longer upstream-synced: it is hand-migrated to v4 and listed under
22+
* `customComponents` in `shadcn-components.json`, because upstream still ships
23+
* the v3 file and re-syncing would break the build. See its header.
24+
*
25+
* This module stays as the single public export site rather than folding into
26+
* `ui/index.ts`: `src/index.ts` star-exports `./ui` and `./custom` side by side,
27+
* so exporting `Resizable*` from both would collide (TS2308).
5328
*/
54-
const HORIZONTAL_SEPARATOR_CLASSES = [
55-
// The rule itself: a full-width hairline instead of a 1px-wide sliver.
56-
'aria-[orientation=horizontal]:h-px',
57-
'aria-[orientation=horizontal]:w-full',
58-
// The `::after` drag hit area: a 4px strip along the rule, not across it.
59-
'aria-[orientation=horizontal]:after:left-0',
60-
'aria-[orientation=horizontal]:after:h-1',
61-
'aria-[orientation=horizontal]:after:w-full',
62-
'aria-[orientation=horizontal]:after:translate-x-0',
63-
'aria-[orientation=horizontal]:after:-translate-y-1/2',
64-
// The grip glyph reads as a drag affordance only when it crosses the rule.
65-
'[&[aria-orientation=horizontal]>div]:rotate-90',
66-
].join(' ');
29+
import type * as React from 'react';
6730

68-
export type ResizableHandleProps = React.ComponentProps<typeof ShadcnResizableHandle>;
31+
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '../ui/resizable';
6932

70-
const ResizableHandle = ({ className, ...props }: ResizableHandleProps) => (
71-
<ShadcnResizableHandle className={cn(HORIZONTAL_SEPARATOR_CLASSES, className)} {...props} />
72-
);
33+
/** Kept on the public surface (it is exported from the package root) even though
34+
* the wrapper it was introduced for is gone. */
35+
export type ResizableHandleProps = React.ComponentProps<typeof ResizableHandle>;
7336

74-
/**
75-
* `ResizablePanelGroup` and `ResizablePanel` need no wrapping and are
76-
* re-exported as-is: the group's `flex-direction` comes from an inline style
77-
* the library writes itself (`row` / `column` from `orientation`), so its own
78-
* dead `data-[panel-group-direction=vertical]:flex-col` is inert but harmless.
79-
* They pass through here purely so callers have one import site for the trio.
80-
*
81-
* These are the *only* public `Resizable*` exports — `ui/index.ts` deliberately
82-
* does not re-export `./resizable`, so no consumer can reach the unpatched
83-
* handle through `@object-ui/components` and silently lose the stacked case.
84-
*/
8537
export { ResizableHandle, ResizablePanel, ResizablePanelGroup };

packages/components/src/ui/index.ts

Lines changed: 3 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/components/src/ui/resizable.tsx

Lines changed: 43 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)