Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions packages/components/shadcn-components.json
Original file line number Diff line number Diff line change
Expand Up @@ -230,13 +230,6 @@
],
"registryDependencies": []
},
"resizable": {
"source": "https://ui.shadcn.com/r/styles/default/resizable.json",
"dependencies": [
"react-resizable-panels"
],
"registryDependencies": []
},
"scroll-area": {
"source": "https://ui.shadcn.com/r/styles/default/scroll-area.json",
"dependencies": [
Expand Down Expand Up @@ -421,6 +414,13 @@
"description": "Custom ObjectUI component - Keyboard key display",
"dependencies": []
},
"resizable": {
"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.",
"dependencies": [
"react-resizable-panels"
],
"divergedFrom": "https://ui.shadcn.com/r/styles/default/resizable.json"
},
"spinner": {
"description": "Custom ObjectUI component - Loading spinner",
"dependencies": []
Expand Down
26 changes: 16 additions & 10 deletions packages/components/src/__tests__/resizable-orientation.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,22 @@
*/

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

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

it('documents why the wrapper exists: v4 emits no panel-group-direction', () => {
// If this ever fails, the library started emitting the v3 attribute again
// and `ui/resizable.tsx`'s own variants would carry the stacked case — at
// which point this wrapper is redundant rather than load-bearing.
it('documents why the styles were re-keyed: v4 emits no panel-group-direction', () => {
// If this ever fails, the library started emitting the v3 attribute again,
// at which point upstream Shadcn's own `data-[panel-group-direction]`
// variants would work and this file could go back to being synced.
const { container } = renderGroup('vertical');
expect(container.querySelectorAll('[data-panel-group-direction]')).toHaveLength(0);
});
Expand Down
90 changes: 21 additions & 69 deletions packages/components/src/custom/resizable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,78 +8,30 @@

'use client';

import * as React from 'react';

import { cn } from '../lib/utils';
import {
ResizableHandle as ShadcnResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from '../ui/resizable';

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

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

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

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

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

45 changes: 43 additions & 2 deletions packages/components/src/ui/resizable.tsx

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading