cursedbelt 5.99.5 → 5.99.6
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/dist/react/components/PanelGrid.d.ts +4 -1
- package/dist/react/components/PanelGrid.d.ts.map +1 -1
- package/dist/react/components/PanelGrid.js +18 -10
- package/dist/react/components/PanelGrid.js.map +1 -1
- package/package.json +13 -3
- package/src/demoFixture.spec.ts +6 -3
- package/src/react/components/PanelGrid.tsx +27 -4
- package/src/react/components/SplitPane.spec.tsx +17 -0
- package/src/verifyGraph.spec.ts +25 -3
|
@@ -24,7 +24,10 @@ export interface PanelGridLeaf {
|
|
|
24
24
|
/** A split — lays its children out along `direction` with dividers between them. */
|
|
25
25
|
export interface PanelGridSplit {
|
|
26
26
|
type: 'split';
|
|
27
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* Unique within the whole grid — namespaces the persisted layout of this group. It need not be
|
|
29
|
+
* unique on the PAGE: the library's group is registered as `<this grid's useId()>:<id>`.
|
|
30
|
+
*/
|
|
28
31
|
id: string;
|
|
29
32
|
/** 'horizontal' = children side-by-side; 'vertical' = children stacked. */
|
|
30
33
|
direction: 'horizontal' | 'vertical';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PanelGrid.d.ts","sourceRoot":"","sources":["../../../src/react/components/PanelGrid.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,
|
|
1
|
+
{"version":3,"file":"PanelGrid.d.ts","sourceRoot":"","sources":["../../../src/react/components/PanelGrid.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAkB,MAAM,OAAO,CAAC;AAIvD,OAAO,EAAkB,KAAK,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAEjF,0EAA0E;AAC1E,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,6EAA6E;IAC7E,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,SAAS,CAAC;IACnB;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC9B;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,oFAAoF;AACpF,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,OAAO,CAAC;IACd;;;OAGG;IACH,EAAE,EAAE,MAAM,CAAC;IACX,2EAA2E;IAC3E,SAAS,EAAE,YAAY,GAAG,UAAU,CAAC;IACrC,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,sFAAsF;IACtF,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,iFAAiF;IACjF,gBAAgB,CAAC,EAAE,qBAAqB,CAAC;IACzC,2GAA2G;IAC3G,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC9B,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,aAAa,GAAG,aAAa,GAAG,cAAc,CAAC;AAE3D,MAAM,WAAW,cAAc;IAC7B,+EAA+E;IAC/E,IAAI,EAAE,cAAc,CAAC;IACrB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,kCAAkC;IAClC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,iDAAiD;IACjD,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,qBAAqB,CAAC;IACzC;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAUD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,wBAAgB,SAAS,CAAC,EACxB,IAAI,EACJ,UAAU,EACV,QAAQ,EACR,WAAkB,EAClB,gBAAyB,EACzB,WAAkB,EAClB,SAAS,EACT,GAAG,IAAI,EACR,EAAE,cAAc,+BAoChB"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useMemo } from 'react';
|
|
2
|
+
import { useId, useMemo } from 'react';
|
|
3
3
|
import { Group, Panel, useDefaultLayout } from 'react-resizable-panels';
|
|
4
4
|
import { useMediaQuery } from '../hooks/useMediaQuery.js';
|
|
5
5
|
import { cn } from '../lib/cn.js';
|
|
@@ -52,33 +52,41 @@ function leavesOf(node) {
|
|
|
52
52
|
export function PanelGrid({ root, storageKey, disabled, withHandles = true, separatorVariant = 'line', mobileStack = true, className, ...rest }) {
|
|
53
53
|
const isMobile = useMediaQuery('(max-width: 639px)');
|
|
54
54
|
const flatLeaves = useMemo(() => leavesOf(root), [root]);
|
|
55
|
+
// 🔴 react-resizable-panels keys its group registry by `id` ACROSS THE DOCUMENT, and each Group's
|
|
56
|
+
// listener takes every change made under its id. `SplitPane` gave every instance the same root
|
|
57
|
+
// id, `'root'`, so two on one page heard each other's layouts: a collapsible panel looked itself
|
|
58
|
+
// up in the other group's layout, found nothing, and `toFixed` threw out of a ResizeObserver —
|
|
59
|
+
// before the listener applied the new layout (forge task 3981, caught by the kitchen sink's
|
|
60
|
+
// `test:browser:smoke`). It also put a second `id="root"` in the DOM beside the app's mount node.
|
|
61
|
+
// Each grid instance scopes its groups; the persisted key stays `${storageKey}:${split.id}`.
|
|
62
|
+
const scope = useId();
|
|
55
63
|
if (mobileStack && isMobile) {
|
|
56
64
|
return (_jsx("div", { ...rest, className: cn('flex flex-col', className), children: flatLeaves.map((leaf) => (_jsx("div", { className: cn('min-h-0', leaf.className), children: leaf.content }, leaf.id))) }));
|
|
57
65
|
}
|
|
58
|
-
return (_jsx(GridSplit, { ...rest, node: root, storageKey: storageKey, disabled: disabled, withHandles: withHandles, separatorVariant: separatorVariant, className: cn('h-full', className) }));
|
|
66
|
+
return (_jsx(GridSplit, { ...rest, node: root, scope: scope, storageKey: storageKey, disabled: disabled, withHandles: withHandles, separatorVariant: separatorVariant, className: cn('h-full', className) }));
|
|
59
67
|
}
|
|
60
68
|
function GridSplit(props) {
|
|
61
69
|
// Persistence needs the useDefaultLayout hook; hooks can't be conditional,
|
|
62
70
|
// so the persistent and plain variants are separate components.
|
|
63
71
|
return props.storageKey ? _jsx(PersistentGridSplit, { ...props }) : _jsx(PlainGridSplit, { ...props });
|
|
64
72
|
}
|
|
65
|
-
function PlainGridSplit({ node, storageKey, disabled, withHandles, separatorVariant, className, }) {
|
|
66
|
-
return (_jsx(Group, { id: node.id
|
|
73
|
+
function PlainGridSplit({ node, scope, storageKey, disabled, withHandles, separatorVariant, className, }) {
|
|
74
|
+
return (_jsx(Group, { id: `${scope}:${node.id}`, orientation: node.direction, disabled: disabled, className: cn('flex', node.direction === 'horizontal' ? 'flex-row' : 'flex-col', className ?? node.className), children: _jsx(GridChildren, { node: node, scope: scope, storageKey: storageKey, disabled: disabled, withHandles: withHandles, separatorVariant: separatorVariant }) }));
|
|
67
75
|
}
|
|
68
|
-
function PersistentGridSplit({ node, storageKey, disabled, withHandles, separatorVariant, className, }) {
|
|
76
|
+
function PersistentGridSplit({ node, scope, storageKey, disabled, withHandles, separatorVariant, className, }) {
|
|
69
77
|
const { defaultLayout, onLayoutChanged } = useDefaultLayout({
|
|
70
78
|
id: `${storageKey}:${node.id}`,
|
|
71
79
|
onlySaveAfterUserInteractions: true,
|
|
72
80
|
storage: localStorage,
|
|
73
81
|
});
|
|
74
|
-
return (_jsx(Group, { id: node.id
|
|
82
|
+
return (_jsx(Group, { id: `${scope}:${node.id}`, orientation: node.direction, defaultLayout: defaultLayout, onLayoutChanged: onLayoutChanged, disabled: disabled, className: cn('flex', node.direction === 'horizontal' ? 'flex-row' : 'flex-col', className ?? node.className), children: _jsx(GridChildren, { node: node, scope: scope, storageKey: storageKey, disabled: disabled, withHandles: withHandles, separatorVariant: separatorVariant }) }));
|
|
75
83
|
}
|
|
76
|
-
function GridChildren({ node, storageKey, disabled, withHandles, separatorVariant, }) {
|
|
84
|
+
function GridChildren({ node, scope, storageKey, disabled, withHandles, separatorVariant, }) {
|
|
77
85
|
const showSeparators = node.resizable !== false;
|
|
78
86
|
const variant = node.separatorVariant ?? separatorVariant;
|
|
79
|
-
return (_jsx(_Fragment, { children: node.children.map((child, i) => (_jsx(GridCell, { child: child, direction: node.direction, storageKey: storageKey, disabled: disabled, withHandles: withHandles, separatorVariant: variant, separatorBefore: i > 0 && showSeparators }, child.id))) }));
|
|
87
|
+
return (_jsx(_Fragment, { children: node.children.map((child, i) => (_jsx(GridCell, { child: child, scope: scope, direction: node.direction, storageKey: storageKey, disabled: disabled, withHandles: withHandles, separatorVariant: variant, separatorBefore: i > 0 && showSeparators }, child.id))) }));
|
|
80
88
|
}
|
|
81
|
-
function GridCell({ child, direction, storageKey, disabled, withHandles, separatorVariant, separatorBefore, }) {
|
|
82
|
-
return (_jsxs(_Fragment, { children: [separatorBefore && (_jsx(PanelSeparator, { direction: direction, disabled: disabled, variant: separatorVariant, withHandle: withHandles })), _jsx(Panel, { id: child.id, defaultSize: child.defaultSize, minSize: child.minSize, collapsible: true, collapsedSize: 0, className: cn('min-h-0 min-w-0', child.className), children: isSplit(child) ? (_jsx(GridSplit, { node: child, storageKey: storageKey, disabled: disabled, withHandles: withHandles, separatorVariant: separatorVariant, className: 'h-full' })) : (child.content) })] }));
|
|
89
|
+
function GridCell({ child, scope, direction, storageKey, disabled, withHandles, separatorVariant, separatorBefore, }) {
|
|
90
|
+
return (_jsxs(_Fragment, { children: [separatorBefore && (_jsx(PanelSeparator, { direction: direction, disabled: disabled, variant: separatorVariant, withHandle: withHandles })), _jsx(Panel, { id: child.id, defaultSize: child.defaultSize, minSize: child.minSize, collapsible: true, collapsedSize: 0, className: cn('min-h-0 min-w-0', child.className), children: isSplit(child) ? (_jsx(GridSplit, { node: child, scope: scope, storageKey: storageKey, disabled: disabled, withHandles: withHandles, separatorVariant: separatorVariant, className: 'h-full' })) : (child.content) })] }));
|
|
83
91
|
}
|
|
84
92
|
//# sourceMappingURL=PanelGrid.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PanelGrid.js","sourceRoot":"","sources":["../../../src/react/components/PanelGrid.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAkB,OAAO,EAAE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"PanelGrid.js","sourceRoot":"","sources":["../../../src/react/components/PanelGrid.tsx"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursedbelt",
|
|
3
|
-
"version": "5.99.
|
|
3
|
+
"version": "5.99.6",
|
|
4
4
|
"license": "ISC",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "The React design system of the cursedbelt split — components, styles, theme. cursedbelt-core below it; server tier in cursedbelt-server.",
|
|
@@ -90,6 +90,7 @@
|
|
|
90
90
|
"demo:serve": "bun run demo:build && bun run scripts/demoServer.ts",
|
|
91
91
|
"build": "rm -rf dist.next dist.prev && bun run scripts/generateStaticStyles.ts && bun run scripts/generateUtilityStyles.ts && bun run scripts/generateAreaStyles.ts && tsc --project tsconfig.build.json --outDir dist.next && tsc --project tsconfig.scripts.build.json --outDir dist.next/scripts && CURSEDBELT_DIST_DIR=dist.next bun run scripts/copyDistAssets.ts && CURSEDBELT_DIST_DIR=dist.next bun run scripts/checkDistExports.ts &&{ [ -d dist ] && mv dist dist.prev || true; } && mv dist.next dist && rm -rf dist.prev",
|
|
92
92
|
"e2e": "playwright test --config e2e/playwright.config.ts",
|
|
93
|
+
"test:browser:smoke": "playwright test --config e2e/playwright.config.ts smoke.spec.ts --project=chromium",
|
|
93
94
|
"e2e:filter-rail": "playwright test --config e2e/playwright.config.ts filter-rail.spec.ts",
|
|
94
95
|
"e2e:table-layout": "playwright test --config e2e/playwright.config.ts table-layout.spec.ts",
|
|
95
96
|
"e2e:stage-layout": "playwright test --config e2e/playwright.config.ts stage-layout.spec.ts",
|
|
@@ -965,7 +966,7 @@
|
|
|
965
966
|
"@vidstack/react": "1.15.6",
|
|
966
967
|
"@vitejs/plugin-react": "^5.2.0",
|
|
967
968
|
"cursedbelt-server": "^4.43.1",
|
|
968
|
-
"cursedops": "^0.
|
|
969
|
+
"cursedops": "^0.23.0",
|
|
969
970
|
"diff": "^9.0.0",
|
|
970
971
|
"emoji-mart": "^5.6.0",
|
|
971
972
|
"hls.js": "^1.5.0",
|
|
@@ -1076,12 +1077,21 @@
|
|
|
1076
1077
|
"why": "stylesStaticMatches / stylesUtilitiesMatches read those two files and assert they are current",
|
|
1077
1078
|
"timeoutSec": 300
|
|
1078
1079
|
},
|
|
1080
|
+
{
|
|
1081
|
+
"script": "test:browser:smoke",
|
|
1082
|
+
"needs": [
|
|
1083
|
+
"styles"
|
|
1084
|
+
],
|
|
1085
|
+
"why": "the one browser proof every verify keeps (forge task 3981): the built kitchen sink boots, renders its first sections and logs no console error. Behind styles because the demo bundle is compiled from those stylesheets"
|
|
1086
|
+
}
|
|
1087
|
+
],
|
|
1088
|
+
"deferred": [
|
|
1079
1089
|
{
|
|
1080
1090
|
"script": "e2e",
|
|
1081
1091
|
"needs": [
|
|
1082
1092
|
"styles"
|
|
1083
1093
|
],
|
|
1084
|
-
"why": "the demo bundle it drives is compiled from the same stylesheets"
|
|
1094
|
+
"why": "the whole kitchen-sink suite — 60 specs, ~127 s, the slowest stage in the generation. The queue's suite job runs it after landing (verify --full here), and prepublishOnly's --force runs it before any publish. Behind styles because the demo bundle it drives is compiled from the same stylesheets"
|
|
1085
1095
|
}
|
|
1086
1096
|
]
|
|
1087
1097
|
}
|
package/src/demoFixture.spec.ts
CHANGED
|
@@ -14,8 +14,11 @@ import {
|
|
|
14
14
|
} from '../demo/devServer.js';
|
|
15
15
|
import { DEMO_TSCONFIG, derivePaths, readDemoTsconfig, renderPathsBlock } from '../demo/generatePaths.js';
|
|
16
16
|
import pkg from '../package.json';
|
|
17
|
-
// The declared graph, read as data — there is no scheduler in this repo to import from.
|
|
18
|
-
|
|
17
|
+
// The declared graph, read as data — there is no scheduler in this repo to import from. Both halves:
|
|
18
|
+
// `e2e` is the deferred half since forge task 3981, which `verify --full`, the queue's suite job and a
|
|
19
|
+
// publish's `--force` run — `verifyGraph.spec.ts` pins which half it sits in.
|
|
20
|
+
const GATE = (pkg as { gate?: { stages?: { script: string }[]; deferred?: { script: string }[] } }).gate;
|
|
21
|
+
const STAGES: readonly { script: string }[] = [...(GATE?.stages ?? []), ...(GATE?.deferred ?? [])];
|
|
19
22
|
|
|
20
23
|
/**
|
|
21
24
|
* 🔴 The e2e suite must never be able to sit green while it drives nothing.
|
|
@@ -159,7 +162,7 @@ describe('the e2e fixture exists', () => {
|
|
|
159
162
|
// is still in it, which the `toInclude` never checked for any of them.
|
|
160
163
|
expect(
|
|
161
164
|
STAGES.map((s) => s.script),
|
|
162
|
-
'`verify` must still run the e2e suite',
|
|
165
|
+
'the gate (`verify --full`) must still run the e2e suite',
|
|
163
166
|
).toContain('e2e');
|
|
164
167
|
expect(pkg.scripts.e2e).toInclude('e2e/playwright.config.ts');
|
|
165
168
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ReactNode, useMemo } from 'react';
|
|
1
|
+
import { type ReactNode, useId, useMemo } from 'react';
|
|
2
2
|
import { Group, Panel, useDefaultLayout } from 'react-resizable-panels';
|
|
3
3
|
import { useMediaQuery } from '../hooks/useMediaQuery.js';
|
|
4
4
|
import { cn } from '../lib/cn.js';
|
|
@@ -29,7 +29,10 @@ export interface PanelGridLeaf {
|
|
|
29
29
|
/** A split — lays its children out along `direction` with dividers between them. */
|
|
30
30
|
export interface PanelGridSplit {
|
|
31
31
|
type: 'split';
|
|
32
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* Unique within the whole grid — namespaces the persisted layout of this group. It need not be
|
|
34
|
+
* unique on the PAGE: the library's group is registered as `<this grid's useId()>:<id>`.
|
|
35
|
+
*/
|
|
33
36
|
id: string;
|
|
34
37
|
/** 'horizontal' = children side-by-side; 'vertical' = children stacked. */
|
|
35
38
|
direction: 'horizontal' | 'vertical';
|
|
@@ -133,6 +136,14 @@ export function PanelGrid({
|
|
|
133
136
|
}: PanelGridProps) {
|
|
134
137
|
const isMobile = useMediaQuery('(max-width: 639px)');
|
|
135
138
|
const flatLeaves = useMemo(() => leavesOf(root), [root]);
|
|
139
|
+
// 🔴 react-resizable-panels keys its group registry by `id` ACROSS THE DOCUMENT, and each Group's
|
|
140
|
+
// listener takes every change made under its id. `SplitPane` gave every instance the same root
|
|
141
|
+
// id, `'root'`, so two on one page heard each other's layouts: a collapsible panel looked itself
|
|
142
|
+
// up in the other group's layout, found nothing, and `toFixed` threw out of a ResizeObserver —
|
|
143
|
+
// before the listener applied the new layout (forge task 3981, caught by the kitchen sink's
|
|
144
|
+
// `test:browser:smoke`). It also put a second `id="root"` in the DOM beside the app's mount node.
|
|
145
|
+
// Each grid instance scopes its groups; the persisted key stays `${storageKey}:${split.id}`.
|
|
146
|
+
const scope = useId();
|
|
136
147
|
|
|
137
148
|
if (mobileStack && isMobile) {
|
|
138
149
|
return (
|
|
@@ -150,6 +161,7 @@ export function PanelGrid({
|
|
|
150
161
|
<GridSplit
|
|
151
162
|
{...rest}
|
|
152
163
|
node={root}
|
|
164
|
+
scope={scope}
|
|
153
165
|
storageKey={storageKey}
|
|
154
166
|
disabled={disabled}
|
|
155
167
|
withHandles={withHandles}
|
|
@@ -161,6 +173,8 @@ export function PanelGrid({
|
|
|
161
173
|
|
|
162
174
|
interface GridSplitProps {
|
|
163
175
|
node: PanelGridSplit;
|
|
176
|
+
/** This grid instance's `useId()` — prefixes every Group id so two grids never share one. */
|
|
177
|
+
scope: string;
|
|
164
178
|
storageKey: string | undefined;
|
|
165
179
|
disabled: boolean | undefined;
|
|
166
180
|
withHandles: boolean;
|
|
@@ -176,6 +190,7 @@ function GridSplit(props: GridSplitProps) {
|
|
|
176
190
|
|
|
177
191
|
function PlainGridSplit({
|
|
178
192
|
node,
|
|
193
|
+
scope,
|
|
179
194
|
storageKey,
|
|
180
195
|
disabled,
|
|
181
196
|
withHandles,
|
|
@@ -184,7 +199,7 @@ function PlainGridSplit({
|
|
|
184
199
|
}: GridSplitProps) {
|
|
185
200
|
return (
|
|
186
201
|
<Group
|
|
187
|
-
id={node.id}
|
|
202
|
+
id={`${scope}:${node.id}`}
|
|
188
203
|
orientation={node.direction}
|
|
189
204
|
disabled={disabled}
|
|
190
205
|
className={cn(
|
|
@@ -195,6 +210,7 @@ function PlainGridSplit({
|
|
|
195
210
|
>
|
|
196
211
|
<GridChildren
|
|
197
212
|
node={node}
|
|
213
|
+
scope={scope}
|
|
198
214
|
storageKey={storageKey}
|
|
199
215
|
disabled={disabled}
|
|
200
216
|
withHandles={withHandles}
|
|
@@ -206,6 +222,7 @@ function PlainGridSplit({
|
|
|
206
222
|
|
|
207
223
|
function PersistentGridSplit({
|
|
208
224
|
node,
|
|
225
|
+
scope,
|
|
209
226
|
storageKey,
|
|
210
227
|
disabled,
|
|
211
228
|
withHandles,
|
|
@@ -219,7 +236,7 @@ function PersistentGridSplit({
|
|
|
219
236
|
});
|
|
220
237
|
return (
|
|
221
238
|
<Group
|
|
222
|
-
id={node.id}
|
|
239
|
+
id={`${scope}:${node.id}`}
|
|
223
240
|
orientation={node.direction}
|
|
224
241
|
defaultLayout={defaultLayout}
|
|
225
242
|
onLayoutChanged={onLayoutChanged}
|
|
@@ -232,6 +249,7 @@ function PersistentGridSplit({
|
|
|
232
249
|
>
|
|
233
250
|
<GridChildren
|
|
234
251
|
node={node}
|
|
252
|
+
scope={scope}
|
|
235
253
|
storageKey={storageKey}
|
|
236
254
|
disabled={disabled}
|
|
237
255
|
withHandles={withHandles}
|
|
@@ -243,6 +261,7 @@ function PersistentGridSplit({
|
|
|
243
261
|
|
|
244
262
|
function GridChildren({
|
|
245
263
|
node,
|
|
264
|
+
scope,
|
|
246
265
|
storageKey,
|
|
247
266
|
disabled,
|
|
248
267
|
withHandles,
|
|
@@ -256,6 +275,7 @@ function GridChildren({
|
|
|
256
275
|
<GridCell
|
|
257
276
|
key={child.id}
|
|
258
277
|
child={child}
|
|
278
|
+
scope={scope}
|
|
259
279
|
direction={node.direction}
|
|
260
280
|
storageKey={storageKey}
|
|
261
281
|
disabled={disabled}
|
|
@@ -270,6 +290,7 @@ function GridChildren({
|
|
|
270
290
|
|
|
271
291
|
interface GridCellProps {
|
|
272
292
|
child: PanelGridNode;
|
|
293
|
+
scope: string;
|
|
273
294
|
direction: 'horizontal' | 'vertical';
|
|
274
295
|
storageKey: string | undefined;
|
|
275
296
|
disabled: boolean | undefined;
|
|
@@ -280,6 +301,7 @@ interface GridCellProps {
|
|
|
280
301
|
|
|
281
302
|
function GridCell({
|
|
282
303
|
child,
|
|
304
|
+
scope,
|
|
283
305
|
direction,
|
|
284
306
|
storageKey,
|
|
285
307
|
disabled,
|
|
@@ -308,6 +330,7 @@ function GridCell({
|
|
|
308
330
|
{isSplit(child) ? (
|
|
309
331
|
<GridSplit
|
|
310
332
|
node={child}
|
|
333
|
+
scope={scope}
|
|
311
334
|
storageKey={storageKey}
|
|
312
335
|
disabled={disabled}
|
|
313
336
|
withHandles={withHandles}
|
|
@@ -32,6 +32,23 @@ describe('SplitPane', () => {
|
|
|
32
32
|
expect(getByText('Panel C')).toBeTruthy();
|
|
33
33
|
});
|
|
34
34
|
|
|
35
|
+
// 🔴 forge task 3981: every SplitPane registered its group as `'root'`, and react-resizable-panels
|
|
36
|
+
// keys groups by id across the document — two on one page heard each other's layouts and threw
|
|
37
|
+
// `toFixed` of undefined out of a ResizeObserver (the kitchen sink's `test:browser:smoke`), and the
|
|
38
|
+
// page carried a second `id="root"` beside the app's mount node.
|
|
39
|
+
test('two SplitPanes on one page register distinct groups, and neither is id="root"', () => {
|
|
40
|
+
const { container } = render(
|
|
41
|
+
<>
|
|
42
|
+
<SplitPane panels={TWO_PANELS} />
|
|
43
|
+
<SplitPane panels={THREE_PANELS} />
|
|
44
|
+
</>,
|
|
45
|
+
);
|
|
46
|
+
const ids = [...container.querySelectorAll('[data-group]')].map((g) => g.id);
|
|
47
|
+
expect(ids).toHaveLength(2);
|
|
48
|
+
expect(new Set(ids).size).toBe(2);
|
|
49
|
+
expect(container.querySelector('#root')).toBeNull();
|
|
50
|
+
});
|
|
51
|
+
|
|
35
52
|
test('horizontal layout: Group gets flex-row class', () => {
|
|
36
53
|
const { container } = render(<SplitPane panels={TWO_PANELS} />);
|
|
37
54
|
const group = container.querySelector('[data-group]');
|
package/src/verifyGraph.spec.ts
CHANGED
|
@@ -40,8 +40,18 @@ interface Stage {
|
|
|
40
40
|
why?: string;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
const GATE = (pkg as { gate?: { stages?: Stage[]; deferred?: Stage[] } }).gate;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The declared graph, read as data — BOTH halves. There is no import of the scheduler to drift from.
|
|
47
|
+
*
|
|
48
|
+
* 🔴 Since 2026-10-29 (forge task 3981) `e2e` sits in `gate.deferred`: plain `verify` runs `stages`
|
|
49
|
+
* and the one `test:browser:smoke` spec, while `verify --full` — the queue's `suite` job after
|
|
50
|
+
* landing — and `--force` run the deferred half too. Moving a stage there is not removing it, so the
|
|
51
|
+
* stage-set floor below reads both halves; what it must never be is in NEITHER.
|
|
52
|
+
*/
|
|
53
|
+
const STAGES: readonly Stage[] = [...(GATE?.stages ?? []), ...(GATE?.deferred ?? [])];
|
|
54
|
+
const DEFERRED: readonly string[] = (GATE?.deferred ?? []).map((s) => s.script);
|
|
45
55
|
|
|
46
56
|
/**
|
|
47
57
|
* What `verify` ran before it became a graph, expanded to the leaf scripts.
|
|
@@ -118,6 +128,18 @@ describe('the verify graph', () => {
|
|
|
118
128
|
// never let them be replaced. `--force` is the re-roll.
|
|
119
129
|
expect(pkg.scripts.prepublishOnly).toInclude('--force');
|
|
120
130
|
});
|
|
131
|
+
|
|
132
|
+
it('🔴 e2e is deferred, never dropped — and a publish still runs it', () => {
|
|
133
|
+
// `$FORGE/tools/gate.ts` → `runnable(spec, full)`: `--force` implies `--full`, which is the
|
|
134
|
+
// deferred half joining the graph. So the `--force` above is also what makes a publish run
|
|
135
|
+
// `e2e`; this pins that the suite is in the half `--force` reaches, and that `verify` keeps
|
|
136
|
+
// the one browser proof check-gate-graph rule 9 asks of a repo deferring its browser suite.
|
|
137
|
+
expect(DEFERRED).toContain('e2e');
|
|
138
|
+
expect(pkg.scripts.gate).not.toInclude('--full');
|
|
139
|
+
const smoke = (GATE?.stages ?? []).find((s) => s.script === 'test:browser:smoke');
|
|
140
|
+
expect(smoke, '`verify` must keep `test:browser:smoke` in `stages`').toBeTruthy();
|
|
141
|
+
expect((pkg.scripts as Record<string, string | undefined>)['test:browser:smoke']).toInclude('smoke.spec.ts');
|
|
142
|
+
});
|
|
121
143
|
});
|
|
122
144
|
|
|
123
145
|
describe('the two edges the graph rests on', () => {
|
|
@@ -126,7 +148,7 @@ describe('the two edges the graph rests on', () => {
|
|
|
126
148
|
// stylesStaticMatches.spec.ts and stylesUtilitiesMatches.spec.ts assert those exact files
|
|
127
149
|
// are current, and the demo bundle e2e drives is compiled from them. Any of those three
|
|
128
150
|
// running beside the generators is a writer racing a reader.
|
|
129
|
-
for (const script of ['build', 'test', 'e2e']) {
|
|
151
|
+
for (const script of ['build', 'test', 'e2e', 'test:browser:smoke']) {
|
|
130
152
|
const stage = STAGES.find((s) => s.script === script);
|
|
131
153
|
expect(stage?.needs, `\`${script}\` must wait for \`styles\``).toContain('styles');
|
|
132
154
|
// 🔴 And it must say WHY. An edge nobody can explain is an edge nobody can safely
|