@octanejs/resizable-panels 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/LICENSE.upstream +21 -0
  2. package/README.md +22 -0
  3. package/UPSTREAM.md +64 -0
  4. package/package.json +61 -0
  5. package/src/components/group/Group.tsrx +244 -0
  6. package/src/components/group/GroupContext.ts +25 -0
  7. package/src/components/group/auto-save/getStorageKey.ts +3 -0
  8. package/src/components/group/auto-save/readLegacyLayout.ts +37 -0
  9. package/src/components/group/sortByElementOffset.ts +20 -0
  10. package/src/components/group/types.ts +50 -0
  11. package/src/components/group/useDefaultLayout.ts +152 -0
  12. package/src/components/group/useGroupCallbackRef.ts +6 -0
  13. package/src/components/group/useGroupContext.ts +12 -0
  14. package/src/components/group/useGroupImperativeHandle.ts +46 -0
  15. package/src/components/group/useGroupRef.ts +6 -0
  16. package/src/components/panel/Panel.tsrx +130 -0
  17. package/src/components/panel/types.ts +66 -0
  18. package/src/components/panel/usePanelCallbackRef.ts +6 -0
  19. package/src/components/panel/usePanelImperativeHandle.ts +42 -0
  20. package/src/components/panel/usePanelRef.ts +6 -0
  21. package/src/components/separator/Separator.tsrx +126 -0
  22. package/src/components/separator/types.ts +22 -0
  23. package/src/constants.ts +25 -0
  24. package/src/global/cursor/getCursorStyle.ts +102 -0
  25. package/src/global/cursor/supportsAdvancedCursorStyles.ts +25 -0
  26. package/src/global/cursor/updateCursorStyle.ts +77 -0
  27. package/src/global/dom/calculateAvailableGroupSize.ts +11 -0
  28. package/src/global/dom/calculateHitRegions.ts +200 -0
  29. package/src/global/dom/calculatePanelConstraints.ts +84 -0
  30. package/src/global/event-handlers/onDocumentContextMenu.ts +9 -0
  31. package/src/global/event-handlers/onDocumentDoubleClick.ts +31 -0
  32. package/src/global/event-handlers/onDocumentKeyDown.ts +133 -0
  33. package/src/global/event-handlers/onDocumentPointerDown.ts +56 -0
  34. package/src/global/event-handlers/onDocumentPointerLeave.ts +21 -0
  35. package/src/global/event-handlers/onDocumentPointerMove.ts +89 -0
  36. package/src/global/event-handlers/onDocumentPointerOut.ts +19 -0
  37. package/src/global/event-handlers/onDocumentPointerUp.ts +14 -0
  38. package/src/global/mountGroup.ts +213 -0
  39. package/src/global/mutable-state/groups.ts +104 -0
  40. package/src/global/mutable-state/interactions.ts +49 -0
  41. package/src/global/mutable-state/types.ts +31 -0
  42. package/src/global/styles/convertEmToPixels.ts +6 -0
  43. package/src/global/styles/convertRemToPixels.ts +6 -0
  44. package/src/global/styles/convertVhToPixels.ts +3 -0
  45. package/src/global/styles/convertVwToPixels.ts +3 -0
  46. package/src/global/styles/parseSizeAndUnit.ts +28 -0
  47. package/src/global/styles/sizeStyleToPixels.ts +48 -0
  48. package/src/global/utils/adjustLayoutByDelta.ts +339 -0
  49. package/src/global/utils/adjustLayoutForSeparator.ts +54 -0
  50. package/src/global/utils/calculateDefaultLayout.ts +37 -0
  51. package/src/global/utils/calculateSeparatorAriaValues.ts +67 -0
  52. package/src/global/utils/compareLayoutNumbers.ts +9 -0
  53. package/src/global/utils/completeActivePointerResize.ts +37 -0
  54. package/src/global/utils/doRectsIntersect.ts +5 -0
  55. package/src/global/utils/findClosestHitRegion.ts +43 -0
  56. package/src/global/utils/findClosestRect.ts +36 -0
  57. package/src/global/utils/findMatchingHitRegions.ts +41 -0
  58. package/src/global/utils/findSeparatorGroup.ts +13 -0
  59. package/src/global/utils/formatLayoutNumber.ts +3 -0
  60. package/src/global/utils/getDistanceBetweenPointAndRect.ts +14 -0
  61. package/src/global/utils/getImperativeGroupMethods.ts +68 -0
  62. package/src/global/utils/getImperativePanelMethods.ts +228 -0
  63. package/src/global/utils/isCoarsePointer.ts +8 -0
  64. package/src/global/utils/isViableHitTarget.ts +53 -0
  65. package/src/global/utils/layoutNumbersEqual.ts +5 -0
  66. package/src/global/utils/layoutsEqual.ts +17 -0
  67. package/src/global/utils/notifyPanelOnResize.ts +33 -0
  68. package/src/global/utils/objectsEqual.ts +14 -0
  69. package/src/global/utils/preserveFixedPanelSizes.ts +74 -0
  70. package/src/global/utils/updateActiveHitRegion.ts +132 -0
  71. package/src/global/utils/validateLayoutKeys.ts +19 -0
  72. package/src/global/utils/validatePanelGroupLayout.ts +92 -0
  73. package/src/global/utils/validatePanelSize.ts +41 -0
  74. package/src/hooks/useForceUpdate.ts +8 -0
  75. package/src/hooks/useId.ts +6 -0
  76. package/src/hooks/useIsomorphicLayoutEffect.ts +4 -0
  77. package/src/hooks/useMergedRefs.ts +18 -0
  78. package/src/hooks/useStableArray.ts +15 -0
  79. package/src/hooks/useStableCallback.ts +18 -0
  80. package/src/hooks/useStableObject.ts +15 -0
  81. package/src/index.tsrx +12 -0
  82. package/src/internal.ts +23 -0
  83. package/src/types.ts +18 -0
  84. package/src/utils/EventEmitter.ts +72 -0
  85. package/src/utils/assert.ts +8 -0
  86. package/src/utils/isArrayEqual.ts +12 -0
  87. package/src/utils/isHTMLElement.ts +9 -0
  88. package/src/utils/isShadowRoot.ts +9 -0
  89. package/src/utils/parseNumericStyleValue.ts +15 -0
  90. package/src/vendor/stacking-order.ts +133 -0
  91. package/status.json +11 -0
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2018 Brian Vaughn
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,22 @@
1
+ # @octanejs/resizable-panels
2
+
3
+ Octane binding for `react-resizable-panels@4.12.2`.
4
+
5
+ Install `@octanejs/resizable-panels` and replace the React package import;
6
+ the public `Group`, `Panel`, `Separator`, persistence hooks, refs, and imperative
7
+ handles retain the pinned upstream contract.
8
+
9
+ ```tsrx
10
+ import { Group, Panel, Separator } from '@octanejs/resizable-panels';
11
+
12
+ export function Workspace() @{
13
+ <Group defaultLayout={{ navigation: 25, content: 75 }}>
14
+ <Panel id="navigation" minSize="15%">Navigation</Panel>
15
+ <Separator aria-label="Resize navigation" />
16
+ <Panel id="content">Content</Panel>
17
+ </Group>
18
+ }
19
+ ```
20
+
21
+ See [`UPSTREAM.md`](./UPSTREAM.md) for pinned provenance and executable parity
22
+ evidence.
package/UPSTREAM.md ADDED
@@ -0,0 +1,64 @@
1
+ # react-resizable-panels upstream contract
2
+
3
+ ## Pin and source boundary
4
+
5
+ | Field | Value |
6
+ |---|---|
7
+ | Package | `react-resizable-panels` |
8
+ | Version | `4.12.2` |
9
+ | Canonical repository | `https://github.com/bvaughn/react-resizable-panels.git` |
10
+ | Canonical tag commit | `a1eeb7aefdb024bb5879a323218e0ac05f77f28e` |
11
+ | Supported upstream range | exactly `4.12.2` |
12
+ | npm tarball SHA-256 | `099742808fafbe3a0288d758271aaf1c35dc9b66ec85077e60f0861e58e89e61` |
13
+ | License | MIT, copyright Brian Vaughn |
14
+
15
+ `upstream/` contains the byte-exact canonical `lib/` source and tests plus
16
+ the package, TypeScript, and Vitest metadata needed to execute them.
17
+ `upstream-artifact/` contains the complete unpacked npm publication artifact. Both
18
+ boundaries are development evidence and excluded from the published `files`.
19
+ `audit/upstream.lock.json` pins every committed Git-sourced byte to the canonical
20
+ commit's Git blob identity; registry-only artifact bytes remain separately
21
+ hash-checked by the package verifier.
22
+
23
+ The adapted suite under `tests/upstream/` is regenerated from the lock's
24
+ mechanical rewrites plus minimal patches in `audit/upstream-patches/`; it is not
25
+ committed.
26
+
27
+ Run `pnpm --dir packages/resizable-panels upstream:verify` to reject a
28
+ modified, missing, or extra vendored file and drift in the export, type, or test
29
+ inventories. The verifier's negative-control mode proves each fail-closed path.
30
+
31
+ ## Public runtime export crosswalk
32
+
33
+ The binding exports the exact pinned runtime set: `Group`, `Panel`, `Separator`,
34
+ `isCoarsePointer`, `useDefaultLayout`, `useGroupCallbackRef`, `useGroupRef`,
35
+ `usePanelCallbackRef`, and `usePanelRef`. Their authoritative source modules and
36
+ exact names are recorded in `audit/public-api.json`.
37
+
38
+ ## Public type crosswalk
39
+
40
+ All 13 public types are implemented and checked against the pinned declaration:
41
+ `GroupImperativeHandle`, `GroupProps`, `Layout`, `LayoutChangedMeta`,
42
+ `LayoutStorage`, `OnGroupLayoutChange`, `Orientation`, `OnPanelResize`,
43
+ `PanelImperativeHandle`, `PanelProps`, `PanelSize`, `SizeUnit`, and
44
+ `SeparatorProps`.
45
+
46
+ The npm declaration imports React's node, intrinsic attribute, CSS, ref,
47
+ dispatch/set-state, and JSX result types. `audit/type-expressibility.json` and
48
+ `audit/type-probes/` preserve equal strictness by deriving host attributes, CSS,
49
+ events, and refs from Octane's existing strict JSX intrinsic surface. Octane's
50
+ type declarations intentionally derive that surface from React types, and
51
+ `@types/react` is a normal dependency of `octane`; this type-only lineage is
52
+ acceptable. The binding itself has no React runtime dependency and no required
53
+ React or ReactDOM peer. It must not replace the intrinsic surface with a
54
+ hand-written or `any`-based map.
55
+
56
+ ## Upstream test disposition
57
+
58
+ The canonical `lib/` tree contains the exact 29 test artifacts and 329 literal
59
+ registrations enumerated in `audit/test-inventory.json`. Every artifact is
60
+ adapted and executable; parameter matrices expand the adapted lane beyond the
61
+ literal registration count. Port-authored differential, persistence, SSR,
62
+ hydration, type, and real-browser evidence is inventoried separately. Negative
63
+ controls reject missing or renamed tests, stale classifications, and API or
64
+ provenance drift.
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@octanejs/resizable-panels",
3
+ "version": "0.0.1",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=22.22.2"
8
+ },
9
+ "description": "Octane port of react-resizable-panels 4.12.2.",
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/octanejs/octane.git",
16
+ "directory": "packages/resizable-panels"
17
+ },
18
+ "main": "src/index.tsrx",
19
+ "module": "src/index.tsrx",
20
+ "types": "src/index.tsrx",
21
+ "files": [
22
+ "src",
23
+ "README.md",
24
+ "UPSTREAM.md",
25
+ "status.json",
26
+ "LICENSE.upstream"
27
+ ],
28
+ "exports": {
29
+ ".": "./src/index.tsrx"
30
+ },
31
+ "peerDependencies": {
32
+ "octane": "0.1.43"
33
+ },
34
+ "devDependencies": {
35
+ "@testing-library/jest-dom": "^6.9.1",
36
+ "@testing-library/react": "^16.3.2",
37
+ "@testing-library/user-event": "^14.6.1",
38
+ "@types/react": "^19.2.17",
39
+ "@types/react-dom": "^19.2.3",
40
+ "@tsrx/react": "^0.2.58",
41
+ "jsdom": "26.1.0",
42
+ "playwright": "^1.61.1",
43
+ "react": "19.2.3",
44
+ "react-dom": "19.2.3",
45
+ "react-resizable-panels": "4.12.2",
46
+ "vite": "^8.1.5",
47
+ "vitest": "3.2.4",
48
+ "vitest-fail-on-console": "^0.10.1",
49
+ "@octanejs/testing-library": "0.1.40",
50
+ "octane": "0.1.43"
51
+ },
52
+ "scripts": {
53
+ "test": "pnpm upstream:materialize && vitest run --root ../.. --project resizable-panels && vitest run --root ../.. --project resizable-panels-server && pnpm test:upstream && vitest run --root ../.. --project resizable-panels-browser",
54
+ "test:upstream": "vitest run --config vitest.pristine.config.ts",
55
+ "upstream:materialize": "node ../../scripts/react-port/materialize.mjs run --package-dir .",
56
+ "upstream:verify": "node ../../scripts/react-parity/verify-provenance.mjs && node audit/verify-provenance.mjs",
57
+ "upstream:verify:negative-controls": "node audit/verify-provenance.mjs --negative-controls",
58
+ "typecheck:probes": "tsc --noEmit -p audit/type-probes/tsconfig.pristine.json && tsrx-tsc --noEmit -p typetests/tsconfig.json",
59
+ "typecheck": "tsrx-tsc --noEmit -p tsconfig.json && tsrx-tsc --noEmit -p typetests/tsconfig.json"
60
+ }
61
+ }
@@ -0,0 +1,244 @@
1
+ import { useEffect, useMemo, useRef } from 'octane';
2
+ import { calculatePanelConstraints } from '../../global/dom/calculatePanelConstraints';
3
+ import { mountGroup } from '../../global/mountGroup';
4
+ import {
5
+ getMountedGroupState,
6
+ getRegisteredGroup,
7
+ subscribeToMountedGroup,
8
+ updateMountedGroup,
9
+ } from '../../global/mutable-state/groups';
10
+ import { getInteractionState } from '../../global/mutable-state/interactions';
11
+ import { layoutNumbersEqual } from '../../global/utils/layoutNumbersEqual';
12
+ import { layoutsEqual } from '../../global/utils/layoutsEqual';
13
+ import { useForceUpdate } from '../../hooks/useForceUpdate';
14
+ import { useId } from '../../hooks/useId';
15
+ import { useIsomorphicLayoutEffect } from '../../hooks/useIsomorphicLayoutEffect';
16
+ import { useMergedRefs } from '../../hooks/useMergedRefs';
17
+ import { useStableCallback } from '../../hooks/useStableCallback';
18
+ import { useStableObject } from '../../hooks/useStableObject';
19
+ import type { RegisteredPanel } from '../panel/types';
20
+ import type { RegisteredSeparator } from '../separator/types';
21
+ import { GroupContext, type GroupContextValue } from './GroupContext';
22
+ import { sortByElementOffset } from './sortByElementOffset';
23
+ import type { GroupProps, Layout, RegisteredGroup, ResizeTargetMinimumSize } from './types';
24
+ import { useGroupImperativeHandle } from './useGroupImperativeHandle';
25
+
26
+ export function Group(props: GroupProps) @{
27
+ const {
28
+ children,
29
+ className,
30
+ defaultLayout,
31
+ disableCursor,
32
+ disabled,
33
+ elementRef: elementRefProp,
34
+ groupRef,
35
+ id: idProp,
36
+ onLayoutChange: onLayoutChangeUnstable,
37
+ onLayoutChanged: onLayoutChangedUnstable,
38
+ orientation = 'horizontal',
39
+ resizeTargetMinimumSize = { coarse: 20, fine: 10 },
40
+ style,
41
+ ...rest
42
+ } = props;
43
+ const previousLayouts = useRef({ onLayoutChange: {} as Layout, onLayoutChanged: {} as Layout });
44
+ const onLayoutChange = useStableCallback((layout: Layout) => {
45
+ if (layoutsEqual(previousLayouts.current.onLayoutChange, layout)) return;
46
+ previousLayouts.current.onLayoutChange = layout;
47
+ onLayoutChangeUnstable?.(layout);
48
+ });
49
+ const onLayoutChanged = useStableCallback((layout: Layout, isUserInteraction: boolean) => {
50
+ if (layoutsEqual(previousLayouts.current.onLayoutChanged, layout)) return;
51
+ previousLayouts.current.onLayoutChanged = layout;
52
+ onLayoutChangedUnstable?.(layout, { isUserInteraction });
53
+ });
54
+
55
+ const id = useId(idProp);
56
+ const elementRef = useRef<HTMLDivElement | null>(null);
57
+ const [panelOrSeparatorChangeSigil, forceUpdate] = useForceUpdate();
58
+ const valuesRef = useRef<{
59
+ lastExpandedPanelSizes: Record<string, number>;
60
+ layouts: Record<string, Layout>;
61
+ panels: RegisteredPanel[];
62
+ resizeTargetMinimumSize: ResizeTargetMinimumSize;
63
+ separators: RegisteredSeparator[];
64
+ }>({
65
+ lastExpandedPanelSizes: {},
66
+ layouts: {},
67
+ panels: [],
68
+ resizeTargetMinimumSize,
69
+ separators: [],
70
+ });
71
+ valuesRef.current.resizeTargetMinimumSize = resizeTargetMinimumSize;
72
+ const mergedRef = useMergedRefs(elementRef, elementRefProp);
73
+ useGroupImperativeHandle(id, groupRef);
74
+
75
+ const getPanelStyles = useStableCallback((groupId: string, panelId: string) => {
76
+ const interaction = getInteractionState();
77
+ const group = getRegisteredGroup(groupId);
78
+ const state = getMountedGroupState(groupId);
79
+ if (state) {
80
+ const dragActive =
81
+ interaction.state === 'active' &&
82
+ interaction.hitRegions.some((region) => region.group === group);
83
+ return {
84
+ flexGrow: state.layout[panelId] ?? 1,
85
+ pointerEvents: dragActive ? 'none' as const : undefined,
86
+ };
87
+ }
88
+ const initialSize = defaultLayout?.[panelId];
89
+ return initialSize === undefined ? undefined : { flexGrow: initialSize };
90
+ });
91
+ const stableProps = useStableObject({ defaultLayout, disableCursor });
92
+ const context = useMemo<GroupContextValue>(
93
+ () => ({
94
+ get disableCursor() {
95
+ return !!stableProps.disableCursor;
96
+ },
97
+ getPanelStyles,
98
+ id,
99
+ orientation,
100
+ registerPanel(panel) {
101
+ valuesRef.current.panels = sortByElementOffset(orientation, [
102
+ ...valuesRef.current.panels,
103
+ panel,
104
+ ]);
105
+ forceUpdate();
106
+ return () => {
107
+ valuesRef.current.panels = valuesRef.current.panels.filter(
108
+ (current) => current !== panel,
109
+ );
110
+ forceUpdate();
111
+ };
112
+ },
113
+ registerSeparator(separator) {
114
+ valuesRef.current.separators = sortByElementOffset(orientation, [
115
+ ...valuesRef.current.separators,
116
+ separator,
117
+ ]);
118
+ forceUpdate();
119
+ return () => {
120
+ valuesRef.current.separators = valuesRef.current.separators.filter(
121
+ (current) => current !== separator,
122
+ );
123
+ forceUpdate();
124
+ };
125
+ },
126
+ updatePanelProps(panelId, next) {
127
+ const panel = valuesRef.current.panels.find((current) => current.id === panelId);
128
+ if (panel) panel.panelConstraints.disabled = next.disabled;
129
+ const group = getRegisteredGroup(id);
130
+ const state = getMountedGroupState(id);
131
+ if (group && state) updateMountedGroup(group, {
132
+ ...state,
133
+ derivedPanelConstraints: calculatePanelConstraints(group),
134
+ });
135
+ },
136
+ updateSeparatorProps(separatorId, next) {
137
+ const separator = valuesRef.current.separators.find(
138
+ (current) => current.id === separatorId,
139
+ );
140
+ if (separator) Object.assign(separator, next);
141
+ },
142
+ }),
143
+ [forceUpdate, getPanelStyles, id, orientation, stableProps],
144
+ );
145
+
146
+ const registeredGroupRef = useRef<RegisteredGroup | null>(null);
147
+ useIsomorphicLayoutEffect(() => {
148
+ const element = elementRef.current;
149
+ if (element === null) return;
150
+ const values = valuesRef.current;
151
+ let sortedDefaultLayout: Layout | undefined;
152
+ if (
153
+ stableProps.defaultLayout &&
154
+ Object.keys(stableProps.defaultLayout).length === values.panels.length
155
+ ) {
156
+ sortedDefaultLayout = {};
157
+ for (const panel of values.panels) {
158
+ const size = stableProps.defaultLayout[panel.id];
159
+ if (size !== undefined) sortedDefaultLayout[panel.id] = size;
160
+ }
161
+ }
162
+ const group: RegisteredGroup = {
163
+ disabled: !!disabled,
164
+ element,
165
+ id,
166
+ mutableState: {
167
+ defaultLayout: sortedDefaultLayout,
168
+ disableCursor: !!stableProps.disableCursor,
169
+ expandedPanelSizes: values.lastExpandedPanelSizes,
170
+ layouts: values.layouts,
171
+ },
172
+ orientation,
173
+ panels: values.panels,
174
+ resizeTargetMinimumSize: values.resizeTargetMinimumSize,
175
+ separators: values.separators,
176
+ };
177
+ registeredGroupRef.current = group;
178
+ const unmount = mountGroup(group);
179
+ const initial = getMountedGroupState(id, true);
180
+ if (!initial.defaultLayoutDeferred && initial.derivedPanelConstraints.length > 0) {
181
+ onLayoutChange(initial.layout);
182
+ onLayoutChanged(initial.layout, false);
183
+ }
184
+ const unsubscribe = subscribeToMountedGroup(id, (event) => {
185
+ const { defaultLayoutDeferred, derivedPanelConstraints, layout } = event.next;
186
+ if (defaultLayoutDeferred || derivedPanelConstraints.length === 0) return;
187
+ const panelIdsKey = group.panels.map((panel) => panel.id).join(',');
188
+ group.mutableState.layouts[panelIdsKey] = layout;
189
+ for (const constraints of derivedPanelConstraints) {
190
+ const previousLayout = event.prev?.layout;
191
+ if (
192
+ constraints.collapsible && previousLayout &&
193
+ layoutNumbersEqual(constraints.collapsedSize, layout[constraints.panelId]) &&
194
+ !layoutNumbersEqual(constraints.collapsedSize, previousLayout[constraints.panelId])
195
+ ) {
196
+ group.mutableState.expandedPanelSizes[constraints.panelId] =
197
+ previousLayout[constraints.panelId];
198
+ }
199
+ }
200
+ onLayoutChange(layout);
201
+ if (getInteractionState().state !== 'active') onLayoutChanged(
202
+ layout,
203
+ event.isUserInteraction,
204
+ );
205
+ });
206
+ return () => {
207
+ registeredGroupRef.current = null;
208
+ unmount();
209
+ unsubscribe();
210
+ };
211
+ }, [disabled, id, onLayoutChange, onLayoutChanged, orientation, panelOrSeparatorChangeSigil, stableProps]);
212
+
213
+ // Match upstream: prop updates only refresh mutable state; they do not reapply
214
+ // defaultLayout via setLayout (React leaves the live layout alone).
215
+ useEffect(() => {
216
+ const group = registeredGroupRef.current;
217
+ if (!group) return;
218
+ group.mutableState.defaultLayout = defaultLayout;
219
+ group.mutableState.disableCursor = !!disableCursor;
220
+ }, [defaultLayout, disableCursor]);
221
+
222
+ return <GroupContext.Provider value={context}>
223
+ <div
224
+ {...rest}
225
+ className={className}
226
+ data-group
227
+ data-testid={id}
228
+ id={id}
229
+ ref={mergedRef}
230
+ style={{
231
+ height: '100%',
232
+ width: '100%',
233
+ overflow: 'hidden',
234
+ ...style,
235
+ display: 'flex',
236
+ flexDirection: orientation === 'horizontal' ? 'row' : 'column',
237
+ flexWrap: 'nowrap',
238
+ touchAction: orientation === 'horizontal' ? 'pan-y' : 'pan-x',
239
+ }}
240
+ >{children}</div>
241
+ </GroupContext.Provider>;
242
+ }
243
+
244
+ (Group as { displayName?: string }).displayName = 'Group';
@@ -0,0 +1,25 @@
1
+ import { createContext } from 'octane';
2
+ import type { RegisteredPanel } from '../panel/types';
3
+ import type { RegisteredSeparator } from '../separator/types';
4
+ import type { Orientation } from './types';
5
+
6
+ export type PanelStyles = {
7
+ flexGrow?: number;
8
+ pointerEvents?: 'none';
9
+ };
10
+
11
+ export type GroupContextValue = {
12
+ disableCursor: boolean;
13
+ getPanelStyles(groupId: string, panelId: string): PanelStyles | undefined;
14
+ id: string;
15
+ orientation: Orientation;
16
+ registerPanel(panel: RegisteredPanel): () => void;
17
+ registerSeparator(separator: RegisteredSeparator): () => void;
18
+ updatePanelProps(id: string, props: { disabled: boolean | undefined }): void;
19
+ updateSeparatorProps(
20
+ id: string,
21
+ props: { disabled: boolean | undefined; disableDoubleClick: boolean | undefined },
22
+ ): void;
23
+ };
24
+
25
+ export const GroupContext = createContext<GroupContextValue | null>(null);
@@ -0,0 +1,3 @@
1
+ export function getStorageKey(id: string, panelIds: readonly string[]): string {
2
+ return `react-resizable-panels:${[id, ...panelIds].join(':')}`;
3
+ }
@@ -0,0 +1,37 @@
1
+ import type { Layout, LayoutStorage } from '../types';
2
+ import { getStorageKey } from './getStorageKey';
3
+
4
+ type LegacyLayout = Record<string, { expandToSizes: unknown; layout: number[] }>;
5
+
6
+ export function readLegacyLayout({
7
+ id,
8
+ panelIds,
9
+ storage,
10
+ }: {
11
+ id: string;
12
+ panelIds?: readonly string[];
13
+ storage: LayoutStorage;
14
+ }): Layout | undefined {
15
+ const value = storage.getItem(getStorageKey(id, []));
16
+ if (!value) return undefined;
17
+
18
+ try {
19
+ const legacy = JSON.parse(value) as LegacyLayout;
20
+ const ids = panelIds ?? getOnlyLegacyPanelIds(legacy);
21
+ if (!ids) return undefined;
22
+
23
+ const entry = legacy[ids.join(',')];
24
+ if (!entry || !Array.isArray(entry.layout) || ids.length !== entry.layout.length) {
25
+ return undefined;
26
+ }
27
+
28
+ return Object.fromEntries(ids.map((panelId, index) => [panelId, entry.layout[index]]));
29
+ } catch {
30
+ return undefined;
31
+ }
32
+ }
33
+
34
+ function getOnlyLegacyPanelIds(legacy: LegacyLayout): string[] | undefined {
35
+ const keys = Object.keys(legacy);
36
+ return keys.length === 1 ? keys[0].split(',') : undefined;
37
+ }
@@ -0,0 +1,20 @@
1
+ import type { Orientation } from './types';
2
+
3
+ export function sortByElementOffset<
4
+ Type extends { element: HTMLElement },
5
+ ReturnType extends Type[],
6
+ >(orientation: Orientation, items: Type[]): ReturnType {
7
+ return Array.from(items).sort(
8
+ orientation === 'horizontal' ? horizontalSort : verticalSort,
9
+ ) as ReturnType;
10
+ }
11
+
12
+ function horizontalSort<Type extends { element: HTMLElement }>(a: Type, b: Type) {
13
+ const delta = a.element.offsetLeft - b.element.offsetLeft;
14
+ return delta !== 0 ? delta : a.element.offsetWidth - b.element.offsetWidth;
15
+ }
16
+
17
+ function verticalSort<Type extends { element: HTMLElement }>(a: Type, b: Type) {
18
+ const delta = a.element.offsetTop - b.element.offsetTop;
19
+ return delta !== 0 ? delta : a.element.offsetHeight - b.element.offsetHeight;
20
+ }
@@ -0,0 +1,50 @@
1
+ import type { OctaneNode } from 'octane';
2
+ import type { Octane } from 'octane/jsx-runtime';
3
+ import type { RegisteredPanel } from '../panel/types';
4
+ import type { RegisteredSeparator } from '../separator/types';
5
+
6
+ export type Orientation = 'horizontal' | 'vertical';
7
+ export type Layout = Record<string, number>;
8
+ export type LayoutStorage = Pick<Storage, 'getItem' | 'setItem'>;
9
+ export type LayoutChangedMeta = { isUserInteraction: boolean };
10
+ export type ResizeTargetMinimumSize = { coarse: number; fine: number };
11
+ export type RegisteredGroup = Readonly<{
12
+ disabled: boolean;
13
+ element: HTMLElement;
14
+ id: string;
15
+ mutableState: {
16
+ defaultLayout: Readonly<Layout> | undefined;
17
+ disableCursor: boolean;
18
+ expandedPanelSizes: Record<string, number>;
19
+ layouts: Record<string, Layout>;
20
+ };
21
+ orientation: Orientation;
22
+ panels: RegisteredPanel[];
23
+ resizeTargetMinimumSize: ResizeTargetMinimumSize;
24
+ separators: RegisteredSeparator[];
25
+ }>;
26
+
27
+ export interface GroupImperativeHandle {
28
+ getLayout(): Layout;
29
+ setLayout(layout: Layout): Layout;
30
+ }
31
+
32
+ type DivStyle = Exclude<Octane.JSX.IntrinsicElements['div']['style'], string | undefined>;
33
+ type DivProps = Omit<Octane.JSX.IntrinsicElements['div'], 'children' | 'id' | 'ref' | 'style'>;
34
+ export type GroupProps = DivProps & {
35
+ children?: OctaneNode;
36
+ defaultLayout?: Layout;
37
+ disableCursor?: boolean;
38
+ disabled?: boolean;
39
+ elementRef?: Octane.Ref<HTMLDivElement | null>;
40
+ groupRef?: Octane.Ref<GroupImperativeHandle | null>;
41
+ id?: string | number;
42
+ onLayoutChange?: (layout: Layout) => void;
43
+ onLayoutChanged?: (layout: Layout, meta: LayoutChangedMeta) => void;
44
+ orientation?: Orientation;
45
+ resizeTargetMinimumSize?: ResizeTargetMinimumSize;
46
+ style?: DivStyle;
47
+ };
48
+
49
+ export type OnGroupLayoutChange = GroupProps['onLayoutChange'];
50
+ export type OnGroupLayoutChanged = GroupProps['onLayoutChanged'];