@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.
- package/LICENSE.upstream +21 -0
- package/README.md +22 -0
- package/UPSTREAM.md +64 -0
- package/package.json +61 -0
- package/src/components/group/Group.tsrx +244 -0
- package/src/components/group/GroupContext.ts +25 -0
- package/src/components/group/auto-save/getStorageKey.ts +3 -0
- package/src/components/group/auto-save/readLegacyLayout.ts +37 -0
- package/src/components/group/sortByElementOffset.ts +20 -0
- package/src/components/group/types.ts +50 -0
- package/src/components/group/useDefaultLayout.ts +152 -0
- package/src/components/group/useGroupCallbackRef.ts +6 -0
- package/src/components/group/useGroupContext.ts +12 -0
- package/src/components/group/useGroupImperativeHandle.ts +46 -0
- package/src/components/group/useGroupRef.ts +6 -0
- package/src/components/panel/Panel.tsrx +130 -0
- package/src/components/panel/types.ts +66 -0
- package/src/components/panel/usePanelCallbackRef.ts +6 -0
- package/src/components/panel/usePanelImperativeHandle.ts +42 -0
- package/src/components/panel/usePanelRef.ts +6 -0
- package/src/components/separator/Separator.tsrx +126 -0
- package/src/components/separator/types.ts +22 -0
- package/src/constants.ts +25 -0
- package/src/global/cursor/getCursorStyle.ts +102 -0
- package/src/global/cursor/supportsAdvancedCursorStyles.ts +25 -0
- package/src/global/cursor/updateCursorStyle.ts +77 -0
- package/src/global/dom/calculateAvailableGroupSize.ts +11 -0
- package/src/global/dom/calculateHitRegions.ts +200 -0
- package/src/global/dom/calculatePanelConstraints.ts +84 -0
- package/src/global/event-handlers/onDocumentContextMenu.ts +9 -0
- package/src/global/event-handlers/onDocumentDoubleClick.ts +31 -0
- package/src/global/event-handlers/onDocumentKeyDown.ts +133 -0
- package/src/global/event-handlers/onDocumentPointerDown.ts +56 -0
- package/src/global/event-handlers/onDocumentPointerLeave.ts +21 -0
- package/src/global/event-handlers/onDocumentPointerMove.ts +89 -0
- package/src/global/event-handlers/onDocumentPointerOut.ts +19 -0
- package/src/global/event-handlers/onDocumentPointerUp.ts +14 -0
- package/src/global/mountGroup.ts +213 -0
- package/src/global/mutable-state/groups.ts +104 -0
- package/src/global/mutable-state/interactions.ts +49 -0
- package/src/global/mutable-state/types.ts +31 -0
- package/src/global/styles/convertEmToPixels.ts +6 -0
- package/src/global/styles/convertRemToPixels.ts +6 -0
- package/src/global/styles/convertVhToPixels.ts +3 -0
- package/src/global/styles/convertVwToPixels.ts +3 -0
- package/src/global/styles/parseSizeAndUnit.ts +28 -0
- package/src/global/styles/sizeStyleToPixels.ts +48 -0
- package/src/global/utils/adjustLayoutByDelta.ts +339 -0
- package/src/global/utils/adjustLayoutForSeparator.ts +54 -0
- package/src/global/utils/calculateDefaultLayout.ts +37 -0
- package/src/global/utils/calculateSeparatorAriaValues.ts +67 -0
- package/src/global/utils/compareLayoutNumbers.ts +9 -0
- package/src/global/utils/completeActivePointerResize.ts +37 -0
- package/src/global/utils/doRectsIntersect.ts +5 -0
- package/src/global/utils/findClosestHitRegion.ts +43 -0
- package/src/global/utils/findClosestRect.ts +36 -0
- package/src/global/utils/findMatchingHitRegions.ts +41 -0
- package/src/global/utils/findSeparatorGroup.ts +13 -0
- package/src/global/utils/formatLayoutNumber.ts +3 -0
- package/src/global/utils/getDistanceBetweenPointAndRect.ts +14 -0
- package/src/global/utils/getImperativeGroupMethods.ts +68 -0
- package/src/global/utils/getImperativePanelMethods.ts +228 -0
- package/src/global/utils/isCoarsePointer.ts +8 -0
- package/src/global/utils/isViableHitTarget.ts +53 -0
- package/src/global/utils/layoutNumbersEqual.ts +5 -0
- package/src/global/utils/layoutsEqual.ts +17 -0
- package/src/global/utils/notifyPanelOnResize.ts +33 -0
- package/src/global/utils/objectsEqual.ts +14 -0
- package/src/global/utils/preserveFixedPanelSizes.ts +74 -0
- package/src/global/utils/updateActiveHitRegion.ts +132 -0
- package/src/global/utils/validateLayoutKeys.ts +19 -0
- package/src/global/utils/validatePanelGroupLayout.ts +92 -0
- package/src/global/utils/validatePanelSize.ts +41 -0
- package/src/hooks/useForceUpdate.ts +8 -0
- package/src/hooks/useId.ts +6 -0
- package/src/hooks/useIsomorphicLayoutEffect.ts +4 -0
- package/src/hooks/useMergedRefs.ts +18 -0
- package/src/hooks/useStableArray.ts +15 -0
- package/src/hooks/useStableCallback.ts +18 -0
- package/src/hooks/useStableObject.ts +15 -0
- package/src/index.tsrx +12 -0
- package/src/internal.ts +23 -0
- package/src/types.ts +18 -0
- package/src/utils/EventEmitter.ts +72 -0
- package/src/utils/assert.ts +8 -0
- package/src/utils/isArrayEqual.ts +12 -0
- package/src/utils/isHTMLElement.ts +9 -0
- package/src/utils/isShadowRoot.ts +9 -0
- package/src/utils/parseNumericStyleValue.ts +15 -0
- package/src/vendor/stacking-order.ts +133 -0
- package/status.json +11 -0
package/LICENSE.upstream
ADDED
|
@@ -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,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'];
|