diff --git a/packages/components/shadcn-components.json b/packages/components/shadcn-components.json index bbb07fef0..188ddc909 100644 --- a/packages/components/shadcn-components.json +++ b/packages/components/shadcn-components.json @@ -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": [ @@ -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": [] diff --git a/packages/components/src/__tests__/resizable-orientation.test.tsx b/packages/components/src/__tests__/resizable-orientation.test.tsx index b1942e2fd..b331a7044 100644 --- a/packages/components/src/__tests__/resizable-orientation.test.tsx +++ b/packages/components/src/__tests__/resizable-orientation.test.tsx @@ -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 @@ -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. @@ -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); }); diff --git a/packages/components/src/custom/resizable.tsx b/packages/components/src/custom/resizable.tsx index 60c9436dd..a0a2ed1cb 100644 --- a/packages/components/src/custom/resizable.tsx +++ b/packages/components/src/custom/resizable.tsx @@ -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; +import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '../ui/resizable'; -const ResizableHandle = ({ className, ...props }: ResizableHandleProps) => ( - -); +/** 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; -/** - * `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 }; diff --git a/packages/components/src/ui/index.ts b/packages/components/src/ui/index.ts index e77bc2016..31ac4de1e 100644 --- a/packages/components/src/ui/index.ts +++ b/packages/components/src/ui/index.ts @@ -36,11 +36,9 @@ export * from './pagination'; export * from './popover'; export * from './progress'; export * from './radio-group'; -// NOT re-exported: `./resizable` ships v3-era `data-[panel-group-direction]` -// styles that react-resizable-panels v4 never triggers, so its handle has no -// stacked-group appearance. `custom/resizable.tsx` wraps it and owns the public -// `Resizable*` names — exporting both here would also collide (TS2308), since -// `src/index.ts` star-exports `./ui` and `./custom` side by side. +// NOT re-exported: `custom/resizable.tsx` owns the public `Resizable*` names, +// and exporting them from both places would collide (TS2308) — `src/index.ts` +// star-exports `./ui` and `./custom` side by side. export * from './scroll-area'; export * from './select'; export * from './separator'; diff --git a/packages/components/src/ui/resizable.tsx b/packages/components/src/ui/resizable.tsx index 866f30e2c..471044d58 100644 --- a/packages/components/src/ui/resizable.tsx +++ b/packages/components/src/ui/resizable.tsx @@ -6,6 +6,34 @@ * LICENSE file in the root directory of this source tree. */ +/** + * ⚠️ NOT a synced Shadcn file, despite living in `ui/` (AGENTS.md #7). + * + * `shadcn-components.json` lists `resizable` under `customComponents`, so + * `pnpm shadcn:update`/`update-all` deliberately skip it. Re-syncing would + * **break the build**: upstream still ships the react-resizable-panels **v3** + * file, whose `import * as ResizablePrimitive` reaches for `PanelGroup` and + * `PanelResizeHandle` — two names v4 (the installed major) does not export at + * all. It renamed them to `Group` / `Panel` / `Separator`, which is what the + * imports below use. That break already happened once, in `00b61d6d8` + * ("upgrade shadcn components and fix build issues"). + * + * The same rename reached the DOM: v4 emits **no** `data-panel-group-direction` + * anywhere, so every `data-[panel-group-direction=vertical]:` utility upstream + * still carries is a dead selector. What v4 does emit on the separator is + * `aria-orientation`, so the stacked-group styles below key on that instead. + * + * ⚠️ That attribute is the **inverse** of the group's `orientation`, and + * deliberately so: `aria-orientation` describes the separator, not the group. + * A stacked group's divider is a *horizontal* line. + * + * orientation="horizontal" → panels side-by-side → separator aria-orientation="vertical" (base classes) + * orientation="vertical" → panels stacked → separator aria-orientation="horizontal" (the variants) + * + * Pinned by `src/__tests__/resizable-orientation.test.tsx`. Restore this file + * to `components` in the manifest only once upstream regenerates for v4. + */ + "use client" import { GripVertical } from "lucide-react" @@ -19,7 +47,9 @@ const ResizablePanelGroup = ({ }: React.ComponentProps) => ( ( div]:rotate-90", + // Base — the side-by-side case (separator aria-orientation="vertical"): + // a 1px-wide rule with a 4px-wide drag strip centred across it. + "relative flex w-px items-center justify-center bg-border", + "after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2", + "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1", + // Stacked — the rule itself: a full-width hairline, not a 1px sliver. + "aria-[orientation=horizontal]:h-px aria-[orientation=horizontal]:w-full", + // Stacked — the drag strip runs ALONG the rule rather than 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-y-1/2 aria-[orientation=horizontal]:after:translate-x-0", + // Stacked — the grip reads as a drag affordance only when it crosses the rule. + "[&[aria-orientation=horizontal]>div]:rotate-90", className )} {...props}