@weasel-js/labkit 1.4.3 → 1.5.0
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/README.md +13 -2
- package/dist/_dts/{CanvasStackContext-Dde8_1my.d.ts → CanvasStackContext-CqkxkFJW.d.ts} +1 -1
- package/dist/_dts/ToggleBar.d-DbHoWKIA.d.ts +116 -0
- package/dist/_dts/{frac-B5mB8LsJ.d.ts → frac-8R6UmlvN.d.ts} +256 -33
- package/dist/_dts/index-B8uu2ba7.d.ts +237 -0
- package/dist/_dts/{index-DelFfeV0.d.ts → index-retnAut7.d.ts} +45 -12
- package/dist/_dts/{types-B9_zrHmb.d.ts → types-Ca2LCnPq.d.ts} +65 -2
- package/dist/_dts/usePanZoom-nS798rOF.d.ts +160 -0
- package/dist/_dts/useTrialState-jcx_TdU6.d.ts +212 -0
- package/dist/canvas/index.d.ts +17 -128
- package/dist/canvas/index.js +4 -2
- package/dist/chrome/index.d.ts +4 -92
- package/dist/chrome/index.js +9 -5
- package/dist/chunk-67SJLMC7.js +704 -0
- package/dist/chunk-67SJLMC7.js.map +1 -0
- package/dist/chunk-6ZDGOZQV.js +310 -0
- package/dist/chunk-6ZDGOZQV.js.map +1 -0
- package/dist/{chunk-QAGAHJKZ.js → chunk-EKIICY6X.js} +111 -25
- package/dist/chunk-EKIICY6X.js.map +1 -0
- package/dist/{chunk-CTRKTLYZ.js → chunk-H6ZAOWNE.js} +11 -11
- package/dist/chunk-H6ZAOWNE.js.map +1 -0
- package/dist/{chunk-2RQPGOQF.js → chunk-KJALCDWE.js} +9 -7
- package/dist/chunk-KJALCDWE.js.map +1 -0
- package/dist/{chunk-ISCOFAUO.js → chunk-MUKOW3TC.js} +3 -3
- package/dist/{chunk-ISCOFAUO.js.map → chunk-MUKOW3TC.js.map} +1 -1
- package/dist/chunk-NDRYLVOW.js +15 -0
- package/dist/chunk-NDRYLVOW.js.map +1 -0
- package/dist/{chunk-C42T6DJT.js → chunk-NS64DXMN.js} +226 -159
- package/dist/chunk-NS64DXMN.js.map +1 -0
- package/dist/{chunk-I6JCVE24.js → chunk-PGETSEDK.js} +135 -13
- package/dist/chunk-PGETSEDK.js.map +1 -0
- package/dist/{chunk-FQMJUVHJ.js → chunk-QLSV2N3G.js} +32 -37
- package/dist/chunk-QLSV2N3G.js.map +1 -0
- package/dist/chunk-RWNDDKOH.js +682 -0
- package/dist/chunk-RWNDDKOH.js.map +1 -0
- package/dist/chunk-S2SHZRD7.js +7681 -0
- package/dist/chunk-S2SHZRD7.js.map +1 -0
- package/dist/{chunk-ASWKLRKJ.js → chunk-TQBBYTAF.js} +19 -82
- package/dist/chunk-TQBBYTAF.js.map +1 -0
- package/dist/chunk-UU3NJO6N.js +72 -0
- package/dist/chunk-UU3NJO6N.js.map +1 -0
- package/dist/chunk-WIG6XHJ7.js +35 -0
- package/dist/chunk-WIG6XHJ7.js.map +1 -0
- package/dist/{chunk-TJ7QY3OC.js → chunk-ZPH5WTWF.js} +21 -15
- package/dist/chunk-ZPH5WTWF.js.map +1 -0
- package/dist/config/index.d.ts +232 -0
- package/dist/config/index.js +6 -0
- package/dist/config/index.js.map +1 -0
- package/dist/controls/index.d.ts +2 -3
- package/dist/controls/index.js +3 -2
- package/dist/dragdrop/index.d.ts +2 -4
- package/dist/index.d.ts +123 -348
- package/dist/index.js +794 -837
- package/dist/index.js.map +1 -1
- package/dist/layers/index.d.ts +3 -5
- package/dist/layers/index.js +2 -2
- package/dist/loupe/index.d.ts +6 -8
- package/dist/loupe/index.js +2 -2
- package/dist/passthrough/weasel-ui.d.ts +169 -117
- package/dist/passthrough/weasel-ui.js +1 -1
- package/dist/primitives/index.d.ts +53 -6
- package/dist/primitives/index.js +6 -4
- package/dist/state/index.d.ts +12 -7
- package/dist/state/index.js +16 -13
- package/dist/state/index.js.map +1 -1
- package/dist/styles.css +163 -62
- package/dist/surface/index.d.ts +44 -7
- package/dist/surface/index.js +2 -1
- package/dist/ui/layers/index.js +1 -1
- package/dist/undo/index.d.ts +2 -4
- package/package.json +15 -8
- package/src/annotations/AnnotationOverlay.tsx +53 -5
- package/src/annotations/Annotations.overlay.test.tsx +85 -6
- package/src/annotations/drawOne.test.ts +1 -1
- package/src/annotations/drawOne.ts +2 -2
- package/src/annotations/frac.test.ts +4 -4
- package/src/annotations/frac.ts +3 -2
- package/src/annotations/index.ts +1 -1
- package/src/annotations/paint.test.ts +2 -2
- package/src/annotations/paint.ts +1 -1
- package/src/annotations/preload.ts +6 -0
- package/src/annotations/store.test.ts +18 -0
- package/src/annotations/store.ts +7 -6
- package/src/annotations/svgNodes.test.ts +2 -2
- package/src/annotations/svgNodes.ts +1 -2
- package/src/annotations/toolMap.ts +3 -3
- package/src/annotations/types.ts +6 -3
- package/src/canvas/CameraWheelContext.ts +12 -0
- package/src/canvas/CanvasStack.test.tsx +25 -0
- package/src/canvas/CanvasStack.tsx +31 -4
- package/src/canvas/Stage.less +18 -0
- package/src/canvas/Stage.test.tsx +186 -0
- package/src/canvas/Stage.tsx +134 -0
- package/src/canvas/camera.test.ts +17 -2
- package/src/canvas/camera.ts +9 -9
- package/src/canvas/index.ts +4 -0
- package/src/canvas/usePanZoom.ts +10 -5
- package/src/chrome/ChromeRegions.stories.tsx +0 -3
- package/src/chrome/LabChrome.tsx +114 -0
- package/src/chrome/builtins.tsx +20 -31
- package/src/chrome/index.ts +15 -0
- package/src/chrome/labRegions.test.tsx +208 -0
- package/src/chrome/labTypes.ts +50 -0
- package/src/chrome/merge.ts +7 -9
- package/src/chrome/regions/PaletteRegion.test.tsx +6 -4
- package/src/chrome/regions/PaletteRegion.tsx +21 -9
- package/src/chrome/regions/SidebarRegion.tsx +25 -13
- package/src/chrome/regions/StatusRegion.tsx +15 -8
- package/src/chrome/regions/ToolbarRegion.tsx +30 -16
- package/src/chrome/types.ts +47 -20
- package/src/config/builder.test.ts +4 -0
- package/src/config/builder.ts +16 -10
- package/src/config/declarationEmit.fixture.ts +11 -0
- package/src/config/declarationEmit.test.ts +51 -0
- package/src/config/entry.test.ts +46 -0
- package/src/config/index.ts +13 -1
- package/src/config/nodeClasses.test.ts +18 -0
- package/src/config/types.ts +3 -1
- package/src/controls/ControlPanel.test.tsx +16 -0
- package/src/controls/ControlPanel.tsx +5 -2
- package/src/fake-indexeddb-auto.d.ts +5 -0
- package/src/index.test.ts +7 -0
- package/src/index.ts +39 -6
- package/src/instrument/serializers.test.ts +22 -0
- package/src/instrument/serializers.ts +11 -0
- package/src/instrument/types.ts +25 -1
- package/src/lab/Lab.less +4 -13
- package/src/lab/Lab.persist.test.tsx +147 -0
- package/src/lab/Lab.stories.tsx +0 -2
- package/src/lab/Lab.surface.test.tsx +66 -3
- package/src/lab/Lab.test.tsx +91 -17
- package/src/lab/Lab.tsx +337 -124
- package/src/lab/LabFit.stories.less +18 -0
- package/src/lab/LabFit.stories.tsx +219 -0
- package/src/lab/LabFullChrome.stories.tsx +0 -3
- package/src/lab/LabHeader.test.tsx +40 -9
- package/src/lab/LabHeader.tsx +7 -13
- package/src/lab/LabPalette.tsx +13 -26
- package/src/lab/LabShell.less +49 -12
- package/src/lab/LabShell.tsx +14 -2
- package/src/lab/LabSwitcher.less +92 -0
- package/src/lab/LabSwitcher.test.tsx +96 -0
- package/src/lab/LabSwitcher.tsx +112 -0
- package/src/lab/Workspace.surface.test.tsx +2 -1
- package/src/lab/fitCheck.test.ts +57 -0
- package/src/lab/fitCheck.ts +125 -0
- package/src/lab/index.ts +2 -0
- package/src/loupe/AGENTS.md +1 -1
- package/src/loupe/Loupe.less +1 -1
- package/src/loupe/canvasLens.ts +1 -1
- package/src/loupe/types.ts +1 -1
- package/src/loupe/useLoupe.test.tsx +1 -1
- package/src/loupe/useLoupe.ts +1 -1
- package/src/passthrough/weasel-ui.ts +9 -0
- package/src/primitives/FloatingPanel.stories.tsx +13 -1
- package/src/primitives/FloatingPanel.test.tsx +47 -16
- package/src/primitives/FloatingPanel.tsx +11 -25
- package/src/primitives/Split.less +14 -0
- package/src/primitives/Split.test.tsx +108 -0
- package/src/primitives/Split.tsx +159 -0
- package/src/primitives/ZoomControl.tsx +5 -2
- package/src/primitives/index.ts +2 -0
- package/src/state/Persistence.stories.tsx +75 -0
- package/src/state/Persistence.tsx +33 -0
- package/src/state/SingletonExperiment.test.tsx +47 -53
- package/src/state/SingletonExperiment.tsx +24 -13
- package/src/state/adapterContract.ts +116 -0
- package/src/state/adapters.test.ts +113 -61
- package/src/state/adapters.ts +328 -69
- package/src/state/document.test.ts +71 -96
- package/src/state/document.ts +66 -52
- package/src/state/helpers.test.ts +20 -0
- package/src/state/helpers.ts +16 -0
- package/src/state/index.ts +8 -0
- package/src/state/labRecords.test.ts +105 -0
- package/src/state/labRecords.ts +160 -0
- package/src/state/openLabStore.test.ts +658 -0
- package/src/state/openLabStore.ts +403 -0
- package/src/state/records.test.ts +234 -0
- package/src/state/records.ts +219 -0
- package/src/state/store.test.ts +104 -635
- package/src/state/store.ts +117 -158
- package/src/state/toolSlot.test.ts +1 -1
- package/src/state/types.ts +39 -15
- package/src/state/useOpenOnce.ts +68 -0
- package/src/state/usePersistedState.test.tsx +125 -0
- package/src/state/usePersistedState.ts +66 -0
- package/src/state/useTrialState.test.tsx +2 -3
- package/src/state/view.test.ts +49 -15
- package/src/state/view.ts +18 -1
- package/src/styles.less +3 -0
- package/src/surface/AGENTS.md +39 -7
- package/src/surface/SurfaceContext.ts +26 -3
- package/src/surface/index.ts +2 -0
- package/src/surface/useSurfaceTile.test.tsx +2 -1
- package/src/surface/useSurfaceTile.ts +6 -5
- package/src/surface/useTiledSurface.test.tsx +175 -0
- package/src/surface/useTiledSurface.ts +54 -5
- package/src/theme/Interstellar.stories.tsx +7 -7
- package/src/theme/base.less +4 -4
- package/src/theme/interstellar.test.ts +1 -1
- package/src/theme/interstellar.tokens.json +2 -2
- package/src/tools/labTool.ts +11 -0
- package/src/trial/Trial.annotations.persist.test.tsx +100 -58
- package/src/trial/Trial.annotations.test.tsx +47 -4
- package/src/trial/Trial.job.test.tsx +3 -3
- package/src/trial/Trial.less +1 -26
- package/src/trial/Trial.stories.tsx +1 -2
- package/src/trial/Trial.test.tsx +28 -4
- package/src/trial/Trial.trialId.test.tsx +23 -1
- package/src/trial/Trial.tsx +118 -38
- package/src/trial/TrialBody.tsx +17 -141
- package/src/trial/TrialChrome.tsx +7 -4
- package/src/trial/trialOps.ts +7 -4
- package/dist/_dts/types-BP2OCcpg.d.ts +0 -155
- package/dist/_dts/types-x92Kfeme.d.ts +0 -62
- package/dist/_dts/useTrialState-D6Vb3T-g.d.ts +0 -99
- package/dist/chunk-2RQPGOQF.js.map +0 -1
- package/dist/chunk-5TROQVQ2.js +0 -547
- package/dist/chunk-5TROQVQ2.js.map +0 -1
- package/dist/chunk-ASWKLRKJ.js.map +0 -1
- package/dist/chunk-C42T6DJT.js.map +0 -1
- package/dist/chunk-CTRKTLYZ.js.map +0 -1
- package/dist/chunk-FQMJUVHJ.js.map +0 -1
- package/dist/chunk-I6JCVE24.js.map +0 -1
- package/dist/chunk-MHAGC6VD.js +0 -7320
- package/dist/chunk-MHAGC6VD.js.map +0 -1
- package/dist/chunk-QAGAHJKZ.js.map +0 -1
- package/dist/chunk-TJ7QY3OC.js.map +0 -1
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { useContext, useMemo } from 'react';
|
|
2
|
+
import { useStore } from 'zustand/react';
|
|
3
|
+
import { useLabContext } from '../lab/LabContext';
|
|
4
|
+
import { LabStoreContext } from '../state/context';
|
|
5
|
+
import { usePersistedState } from '../state/usePersistedState';
|
|
6
|
+
import { resolveLabTool } from '../tools/labTool';
|
|
7
|
+
import type { TrialTool } from '../tools/types';
|
|
8
|
+
import type { LabChromeContext, LabContribution, LabRegion } from './labTypes';
|
|
9
|
+
import { mergeContributions } from './merge';
|
|
10
|
+
import { SidebarRegion } from './regions/SidebarRegion';
|
|
11
|
+
import { StatusRegion } from './regions/StatusRegion';
|
|
12
|
+
import { ToolbarRegion } from './regions/ToolbarRegion';
|
|
13
|
+
|
|
14
|
+
/** The lab's chrome context: the lab itself, plus the lab's tool slot. Throws
|
|
15
|
+
* outside a `<Lab>`. */
|
|
16
|
+
export function useLabChromeContext(): LabChromeContext {
|
|
17
|
+
const lab = useLabContext();
|
|
18
|
+
const storeCtx = useContext(LabStoreContext);
|
|
19
|
+
if (!storeCtx) throw new Error('[labkit] lab chrome requires <LabStoreProvider>');
|
|
20
|
+
const slot = useStore(storeCtx.store, (s) => s.activeToolId);
|
|
21
|
+
const storeInstruments = useStore(storeCtx.store, (s) => s.instruments);
|
|
22
|
+
const activeToolId = resolveLabTool(slot, storeInstruments ?? lab.instruments);
|
|
23
|
+
const setLabTool = useStore(storeCtx.store, (s) => s.setLabTool);
|
|
24
|
+
return useMemo(
|
|
25
|
+
() => ({ ...lab, activeToolId, setActiveTool: setLabTool }),
|
|
26
|
+
[lab, activeToolId, setLabTool],
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The lab's contributions for one region, in declaration order. */
|
|
31
|
+
export function contributionsIn<TCtx = LabChromeContext>(
|
|
32
|
+
contributions: readonly LabContribution<TCtx>[],
|
|
33
|
+
region: LabRegion,
|
|
34
|
+
): LabContribution<TCtx>[] {
|
|
35
|
+
return contributions.filter((c) => c.region === region);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The palette contributions a lab's `tools` stand for. Declaring a tool is
|
|
39
|
+
* shorthand for contributing a palette item that writes the lab's tool
|
|
40
|
+
* slot. */
|
|
41
|
+
export function toolContributions(tools: readonly TrialTool[]): LabContribution[] {
|
|
42
|
+
return tools.map((t) => ({
|
|
43
|
+
id: t.id,
|
|
44
|
+
region: 'palette' as const,
|
|
45
|
+
...(t.group === undefined ? {} : { group: t.group }),
|
|
46
|
+
item: {
|
|
47
|
+
icon: t.icon,
|
|
48
|
+
label: t.label,
|
|
49
|
+
...(t.shortcut === undefined ? {} : { shortcut: t.shortcut }),
|
|
50
|
+
},
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Everything the lab's chrome renders, from both of the ways a lab declares
|
|
55
|
+
* it. Tools and contributions share one id namespace, so a tool colliding
|
|
56
|
+
* with a contribution throws here rather than one of them silently losing. */
|
|
57
|
+
export function labContributions(
|
|
58
|
+
tools: readonly TrialTool[] | undefined,
|
|
59
|
+
chrome: readonly LabContribution[] | undefined,
|
|
60
|
+
): LabContribution[] {
|
|
61
|
+
return mergeContributions<LabContribution>(toolContributions(tools ?? []), [...(chrome ?? [])]);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Props shared by the lab's region mounts. Each takes the lab's whole
|
|
65
|
+
* contribution list and lays out the part addressed to it. */
|
|
66
|
+
export interface LabRegionProps {
|
|
67
|
+
contributions: readonly LabContribution[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The lab's action bar, in the shell header beside the title. */
|
|
71
|
+
export function LabHeaderRegion({ contributions }: LabRegionProps) {
|
|
72
|
+
const ctx = useLabChromeContext();
|
|
73
|
+
return (
|
|
74
|
+
<ToolbarRegion
|
|
75
|
+
region="header"
|
|
76
|
+
label="Lab actions"
|
|
77
|
+
contributions={contributionsIn(contributions, 'header')}
|
|
78
|
+
ctx={ctx}
|
|
79
|
+
/>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The lab's sidebar sections. Fold state is a lab-scoped persisted value, so it
|
|
84
|
+
* survives a reload in a stored lab and lasts the session in an unstored one. */
|
|
85
|
+
export function LabSidebarRegion({ contributions }: LabRegionProps) {
|
|
86
|
+
const lab = useLabChromeContext();
|
|
87
|
+
const [collapsedSections, setFolds] = usePersistedState<Record<string, boolean>>(
|
|
88
|
+
'lk-lab-sidebar-folds',
|
|
89
|
+
{},
|
|
90
|
+
{ scope: 'lab' },
|
|
91
|
+
);
|
|
92
|
+
const ctx = useMemo(
|
|
93
|
+
() => ({
|
|
94
|
+
...lab,
|
|
95
|
+
collapsedSections,
|
|
96
|
+
setSectionCollapsed: (key: string, collapsed: boolean) =>
|
|
97
|
+
setFolds((f) => ({ ...f, [key]: collapsed })),
|
|
98
|
+
}),
|
|
99
|
+
[lab, collapsedSections, setFolds],
|
|
100
|
+
);
|
|
101
|
+
return <SidebarRegion contributions={contributionsIn(contributions, 'sidebar')} ctx={ctx} />;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** The lab's readouts, in the shell footer below the workspace. */
|
|
105
|
+
export function LabFooterRegion({ contributions }: LabRegionProps) {
|
|
106
|
+
const ctx = useLabChromeContext();
|
|
107
|
+
return (
|
|
108
|
+
<StatusRegion
|
|
109
|
+
region="footer"
|
|
110
|
+
contributions={contributionsIn(contributions, 'footer')}
|
|
111
|
+
ctx={ctx}
|
|
112
|
+
/>
|
|
113
|
+
);
|
|
114
|
+
}
|
package/src/chrome/builtins.tsx
CHANGED
|
@@ -12,11 +12,10 @@ import {
|
|
|
12
12
|
} from '@weasel-js/ui';
|
|
13
13
|
import { ExportMenu } from '../annotations/ExportMenu';
|
|
14
14
|
import { MarkList } from '../annotations/MarkList';
|
|
15
|
-
import { ANNOTATION_TOOLS } from '../annotations/toolMap';
|
|
16
15
|
import type { ControlRenderer } from '../config/types';
|
|
17
16
|
import { ControlPanel } from '../controls/ControlPanel';
|
|
18
17
|
import type { Instrument } from '../instrument/types';
|
|
19
|
-
import {
|
|
18
|
+
import { MenuButton } from '../passthrough/weasel-ui';
|
|
20
19
|
import { FpsMeter } from '../primitives/FpsMeter';
|
|
21
20
|
import { ScaleIndicator } from '../primitives/ScaleIndicator';
|
|
22
21
|
import { ZoomControl } from '../primitives/ZoomControl';
|
|
@@ -82,16 +81,10 @@ export function builtinContributions(
|
|
|
82
81
|
});
|
|
83
82
|
}
|
|
84
83
|
|
|
85
|
-
// Declaring `annotations` provides the drawing palette, the way declaring
|
|
86
|
-
// `tools` provides the instrument's own; both write the same tool slot.
|
|
87
84
|
// A tool id shares the contribution namespace, so a tool called `close`
|
|
88
|
-
// collides with the built-in close button and throws
|
|
89
|
-
//
|
|
90
|
-
const tools
|
|
91
|
-
...(instrument.annotations ? ANNOTATION_TOOLS : []),
|
|
92
|
-
...(instrument.tools?.tools ?? []),
|
|
93
|
-
];
|
|
94
|
-
for (const t of tools) {
|
|
85
|
+
// collides with the built-in close button and throws. Annotation tools are
|
|
86
|
+
// not here: the lab's rail carries them.
|
|
87
|
+
for (const t of instrument.tools?.tools ?? []) {
|
|
95
88
|
out.push({
|
|
96
89
|
id: t.id,
|
|
97
90
|
region: 'palette',
|
|
@@ -116,7 +109,7 @@ export function builtinContributions(
|
|
|
116
109
|
|
|
117
110
|
// Zoom acts on the view of the trial, so it is a viewport control. A trial
|
|
118
111
|
// holding a non-2D view reports zoom as null and gets none of this.
|
|
119
|
-
if (instrument.canvas != null && zoom !== null) {
|
|
112
|
+
if ((instrument.canvas != null || instrument.stage != null) && zoom !== null) {
|
|
120
113
|
out.push({
|
|
121
114
|
id: 'zoom-out',
|
|
122
115
|
region: 'viewport',
|
|
@@ -153,15 +146,17 @@ export function builtinContributions(
|
|
|
153
146
|
id: 'scale',
|
|
154
147
|
region: 'status',
|
|
155
148
|
group: 'view',
|
|
156
|
-
render: () => <ScaleIndicator />,
|
|
157
|
-
});
|
|
158
|
-
out.push({
|
|
159
|
-
id: 'fps',
|
|
160
|
-
region: 'status',
|
|
161
|
-
group: 'view',
|
|
162
|
-
end: true,
|
|
163
|
-
render: () => <FpsMeter />,
|
|
149
|
+
render: (c) => <ScaleIndicator zoom={c.zoom ?? 1} />,
|
|
164
150
|
});
|
|
151
|
+
if (instrument.canvas != null) {
|
|
152
|
+
out.push({
|
|
153
|
+
id: 'fps',
|
|
154
|
+
region: 'status',
|
|
155
|
+
group: 'view',
|
|
156
|
+
end: true,
|
|
157
|
+
render: () => <FpsMeter />,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
165
160
|
}
|
|
166
161
|
|
|
167
162
|
if (instrument.annotations != null) {
|
|
@@ -205,24 +200,18 @@ export function builtinContributions(
|
|
|
205
200
|
}
|
|
206
201
|
|
|
207
202
|
if (ctx.savedSnapshots.length > 0) {
|
|
208
|
-
// A
|
|
209
|
-
// `selectedKey={null}` so it stays a "load one" action rather than
|
|
210
|
-
// drifting into a display of what was loaded last.
|
|
203
|
+
// A menu is not an icon button, so it takes the render escape.
|
|
211
204
|
out.push({
|
|
212
205
|
id: 'snapshot-load',
|
|
213
206
|
region: 'toolbar',
|
|
214
207
|
group: 'trial',
|
|
215
208
|
end: true,
|
|
216
209
|
render: (c) => (
|
|
217
|
-
<
|
|
218
|
-
className="lk-toolbar__load-select"
|
|
210
|
+
<MenuButton
|
|
219
211
|
aria-label="Load snapshot"
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
onSelectionChange={(id) => {
|
|
224
|
-
if (id != null) c.loadSnapshot(String(id));
|
|
225
|
-
}}
|
|
212
|
+
label="Load…"
|
|
213
|
+
items={c.savedSnapshots.map((sn) => ({ value: sn.id, label: sn.name }))}
|
|
214
|
+
onAction={(id) => c.loadSnapshot(id)}
|
|
226
215
|
/>
|
|
227
216
|
),
|
|
228
217
|
});
|
package/src/chrome/index.ts
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
export { builtinContributions } from './builtins';
|
|
2
|
+
export type { LabRegionProps } from './LabChrome';
|
|
3
|
+
export {
|
|
4
|
+
contributionsIn,
|
|
5
|
+
LabFooterRegion,
|
|
6
|
+
LabHeaderRegion,
|
|
7
|
+
LabSidebarRegion,
|
|
8
|
+
labContributions,
|
|
9
|
+
toolContributions,
|
|
10
|
+
useLabChromeContext,
|
|
11
|
+
} from './LabChrome';
|
|
12
|
+
export type { LabChromeContext, LabContribution, LabRegion } from './labTypes';
|
|
2
13
|
export { mergeContributions, suppressContributions } from './merge';
|
|
3
14
|
export type { PaletteRegionProps } from './regions/PaletteRegion';
|
|
4
15
|
export { PaletteRegion } from './regions/PaletteRegion';
|
|
@@ -13,11 +24,15 @@ export { ToolbarRegion } from './regions/ToolbarRegion';
|
|
|
13
24
|
export type { ViewportRegionProps } from './regions/ViewportRegion';
|
|
14
25
|
export { ViewportRegion } from './regions/ViewportRegion';
|
|
15
26
|
export type {
|
|
27
|
+
ContributionBase,
|
|
16
28
|
IconComponent,
|
|
29
|
+
RegionContribution,
|
|
17
30
|
SidebarSection,
|
|
31
|
+
SidebarSlotContext,
|
|
18
32
|
StatusReadout,
|
|
19
33
|
ToolbarItem,
|
|
20
34
|
ToolItem,
|
|
35
|
+
ToolSlotContext,
|
|
21
36
|
TrialChromeContext,
|
|
22
37
|
TrialContribution,
|
|
23
38
|
TrialRegion,
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import type { Instrument } from '../instrument/types';
|
|
4
|
+
import { Lab } from '../lab/Lab';
|
|
5
|
+
import { LabShell } from '../lab/LabShell';
|
|
6
|
+
import type { LabChromeContext, LabContribution } from './labTypes';
|
|
7
|
+
import { ToolbarRegion } from './regions/ToolbarRegion';
|
|
8
|
+
|
|
9
|
+
const Glyph = () => <svg />;
|
|
10
|
+
const bare: Instrument = {
|
|
11
|
+
name: 'Bare',
|
|
12
|
+
defaultConfig: () => ({}),
|
|
13
|
+
initialState: () => ({}),
|
|
14
|
+
render: () => null,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function silenceRenderError(): void {
|
|
18
|
+
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('lab-level chrome', () => {
|
|
22
|
+
it('renders a header contribution beside the lab header', () => {
|
|
23
|
+
const contribution: LabContribution = {
|
|
24
|
+
id: 'publish',
|
|
25
|
+
region: 'header',
|
|
26
|
+
item: { icon: Glyph, label: 'Publish', onActivate: () => {} },
|
|
27
|
+
};
|
|
28
|
+
render(
|
|
29
|
+
<Lab title="T" instruments={[bare]} defaultInstrument="Bare" labChrome={[contribution]} />,
|
|
30
|
+
);
|
|
31
|
+
expect(screen.getByRole('button', { name: 'Publish' })).toBeInTheDocument();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('hands a header contribution the lab, not a trial', () => {
|
|
35
|
+
const contribution: LabContribution = {
|
|
36
|
+
id: 'another',
|
|
37
|
+
region: 'header',
|
|
38
|
+
item: {
|
|
39
|
+
icon: Glyph,
|
|
40
|
+
label: 'Another',
|
|
41
|
+
onActivate: (ctx) => ctx.addTrial('Bare'),
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
render(
|
|
45
|
+
<Lab title="T" instruments={[bare]} defaultInstrument="Bare" labChrome={[contribution]} />,
|
|
46
|
+
);
|
|
47
|
+
expect(screen.getAllByLabelText(/^Trial /)).toHaveLength(1);
|
|
48
|
+
fireEvent.click(screen.getByRole('button', { name: 'Another' }));
|
|
49
|
+
expect(screen.getAllByLabelText(/^Trial /)).toHaveLength(2);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('renders a palette contribution in the lab rail and selects it', () => {
|
|
53
|
+
const contribution: LabContribution = {
|
|
54
|
+
id: 'measure',
|
|
55
|
+
region: 'palette',
|
|
56
|
+
item: { icon: Glyph, label: 'Measure' },
|
|
57
|
+
};
|
|
58
|
+
render(
|
|
59
|
+
<Lab title="T" instruments={[bare]} defaultInstrument="Bare" labChrome={[contribution]} />,
|
|
60
|
+
);
|
|
61
|
+
const button = screen.getByRole('button', { name: 'Measure' });
|
|
62
|
+
fireEvent.click(button);
|
|
63
|
+
expect(screen.getByRole('button', { name: 'Measure' })).toHaveAttribute('aria-current', 'true');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('gives a palette contribution the whole lab context, not a tool slot alone', () => {
|
|
67
|
+
// The shape the `as unknown as TrialChromeContext` cast used to fake: a
|
|
68
|
+
// contribution reading anything past the tool slot got `undefined`.
|
|
69
|
+
const seen: LabChromeContext[] = [];
|
|
70
|
+
const contribution: LabContribution = {
|
|
71
|
+
id: 'readout',
|
|
72
|
+
region: 'palette',
|
|
73
|
+
render: (ctx) => {
|
|
74
|
+
seen.push(ctx);
|
|
75
|
+
return <span>{`${ctx.trials.length} trial(s), mode ${ctx.mode}`}</span>;
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
render(
|
|
79
|
+
<Lab title="T" instruments={[bare]} defaultInstrument="Bare" labChrome={[contribution]} />,
|
|
80
|
+
);
|
|
81
|
+
expect(screen.getByText('1 trial(s), mode auto')).toBeInTheDocument();
|
|
82
|
+
expect(seen[0]?.activeToolId).toBeNull();
|
|
83
|
+
expect(typeof seen[0]?.saveSnapshot).toBe('function');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('renders a footer contribution', () => {
|
|
87
|
+
const contribution: LabContribution = {
|
|
88
|
+
id: 'count',
|
|
89
|
+
region: 'footer',
|
|
90
|
+
item: { text: 'idle' },
|
|
91
|
+
};
|
|
92
|
+
render(
|
|
93
|
+
<Lab title="T" instruments={[bare]} defaultInstrument="Bare" labChrome={[contribution]} />,
|
|
94
|
+
);
|
|
95
|
+
expect(screen.getByText('idle')).toBeInTheDocument();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('throws when two lab contributions share an id', () => {
|
|
99
|
+
silenceRenderError();
|
|
100
|
+
const clash: LabContribution[] = [
|
|
101
|
+
{ id: 'same', region: 'header', item: { icon: Glyph, label: 'One', onActivate: () => {} } },
|
|
102
|
+
{ id: 'same', region: 'footer', item: { text: 'two' } },
|
|
103
|
+
];
|
|
104
|
+
expect(() =>
|
|
105
|
+
render(<Lab title="T" instruments={[bare]} defaultInstrument="Bare" labChrome={clash} />),
|
|
106
|
+
).toThrow(/duplicate contribution id "same"/);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('keeps the lab tool slot and lab palette contributions in one namespace', () => {
|
|
110
|
+
silenceRenderError();
|
|
111
|
+
const clash: LabContribution = {
|
|
112
|
+
id: 'pick',
|
|
113
|
+
region: 'palette',
|
|
114
|
+
item: { icon: Glyph, label: 'Mine' },
|
|
115
|
+
};
|
|
116
|
+
expect(() =>
|
|
117
|
+
render(
|
|
118
|
+
<Lab
|
|
119
|
+
title="T"
|
|
120
|
+
instruments={[bare]}
|
|
121
|
+
defaultInstrument="Bare"
|
|
122
|
+
tools={[{ id: 'pick', label: 'Pick', icon: Glyph }]}
|
|
123
|
+
labChrome={[clash]}
|
|
124
|
+
/>,
|
|
125
|
+
),
|
|
126
|
+
).toThrow(/duplicate contribution id "pick"/);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('lab sidebar region', () => {
|
|
131
|
+
const tree: LabContribution = {
|
|
132
|
+
id: 'tree',
|
|
133
|
+
region: 'sidebar',
|
|
134
|
+
item: { title: 'Stories', body: <p>story list</p> },
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
it('renders a section beside the workspace', () => {
|
|
138
|
+
const { container } = render(
|
|
139
|
+
<Lab title="T" instruments={[bare]} defaultInstrument="Bare" labChrome={[tree]} />,
|
|
140
|
+
);
|
|
141
|
+
expect(screen.getByText('story list')).toBeInTheDocument();
|
|
142
|
+
expect(container.querySelector('.lk-lab__sidebar')).not.toBeNull();
|
|
143
|
+
expect(screen.getByRole('separator', { name: /lab sidebar/i })).toBeInTheDocument();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('folds a section', () => {
|
|
147
|
+
render(<Lab title="T" instruments={[bare]} defaultInstrument="Bare" labChrome={[tree]} />);
|
|
148
|
+
fireEvent.click(screen.getByRole('button', { name: 'Stories' }));
|
|
149
|
+
expect(screen.queryByText('story list')).toBeNull();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('offers no tear-out at lab level', () => {
|
|
153
|
+
render(<Lab title="T" instruments={[bare]} defaultInstrument="Bare" labChrome={[tree]} />);
|
|
154
|
+
expect(screen.queryByRole('button', { name: 'Undock Stories' })).toBeNull();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('hands a render contribution the lab context', () => {
|
|
158
|
+
const seen: LabChromeContext[] = [];
|
|
159
|
+
const probe: LabContribution = {
|
|
160
|
+
id: 'probe',
|
|
161
|
+
region: 'sidebar',
|
|
162
|
+
render: (ctx) => {
|
|
163
|
+
seen.push(ctx);
|
|
164
|
+
return <p>probe</p>;
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
render(<Lab title="T" instruments={[bare]} defaultInstrument="Bare" labChrome={[probe]} />);
|
|
168
|
+
expect(screen.getByText('probe')).toBeInTheDocument();
|
|
169
|
+
expect(seen[0]?.trials).toHaveLength(1);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('adds no strip to a lab without sidebar chrome', () => {
|
|
173
|
+
const { container } = render(<Lab title="T" instruments={[bare]} defaultInstrument="Bare" />);
|
|
174
|
+
expect(container.querySelector('.lk-lab__panes')).toBeNull();
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe('lab chrome without <Lab>', () => {
|
|
179
|
+
// The rail `apps/theme-editor` mounts: `<LabShell>` with no trial runtime
|
|
180
|
+
// above it, so the context handed to `onActivate` is the consumer's own.
|
|
181
|
+
interface RailCtx {
|
|
182
|
+
undo: () => void;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
it('renders a contribution typed against a consumer context and hands it back', () => {
|
|
186
|
+
const undo = vi.fn();
|
|
187
|
+
const ctx: RailCtx = { undo };
|
|
188
|
+
const rail: readonly LabContribution<RailCtx>[] = [
|
|
189
|
+
{
|
|
190
|
+
id: 'undo',
|
|
191
|
+
region: 'header',
|
|
192
|
+
item: { icon: Glyph, label: 'Undo', showLabel: true, onActivate: (c) => c.undo() },
|
|
193
|
+
},
|
|
194
|
+
];
|
|
195
|
+
render(
|
|
196
|
+
<LabShell
|
|
197
|
+
title="Palette lab"
|
|
198
|
+
header={
|
|
199
|
+
<ToolbarRegion region="header" label="Lab actions" contributions={rail} ctx={ctx} />
|
|
200
|
+
}
|
|
201
|
+
>
|
|
202
|
+
<div />
|
|
203
|
+
</LabShell>,
|
|
204
|
+
);
|
|
205
|
+
fireEvent.click(screen.getByRole('button', { name: 'Undo' }));
|
|
206
|
+
expect(undo).toHaveBeenCalledTimes(1);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { LabContextValue } from '../lab/LabContext';
|
|
3
|
+
import type {
|
|
4
|
+
ContributionBase,
|
|
5
|
+
SidebarSection,
|
|
6
|
+
StatusReadout,
|
|
7
|
+
ToolbarItem,
|
|
8
|
+
ToolItem,
|
|
9
|
+
ToolSlotContext,
|
|
10
|
+
} from './types';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A named position in the lab's own chrome — a box the shell owns, outside any
|
|
14
|
+
* one trial. `header` and `footer` are the shell's two bars, named as
|
|
15
|
+
* `<LabShell>` names them; `palette` is the tool rail down the side of the
|
|
16
|
+
* workspace; `sidebar` is a column of sections left of the tool rail.
|
|
17
|
+
*/
|
|
18
|
+
export type LabRegion = 'header' | 'palette' | 'sidebar' | 'footer';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Everything a lab-level contribution can read and command: the lab's own
|
|
22
|
+
* state and operations, plus the lab's tool slot — which every trial whose
|
|
23
|
+
* instrument declares no tools of its own reflects and writes.
|
|
24
|
+
*/
|
|
25
|
+
export interface LabChromeContext extends LabContextValue, ToolSlotContext {}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A contribution to the lab's chrome. Shaped as `TrialContribution` is, and
|
|
29
|
+
* merged and suppressed by the same rules, but keyed to the lab's regions and
|
|
30
|
+
* rendered with the lab's context. Supplying `render` instead of `item` opts
|
|
31
|
+
* out of the region's layout.
|
|
32
|
+
*
|
|
33
|
+
* `TCtx` is what the chrome hands a handler. It defaults to `LabChromeContext`,
|
|
34
|
+
* which is what `<Lab>` renders its regions with; a consumer mounting the
|
|
35
|
+
* regions under a bare `<LabShell>` names whatever it passes as `ctx` instead.
|
|
36
|
+
*/
|
|
37
|
+
export type LabContribution<TCtx = LabChromeContext> =
|
|
38
|
+
| (ContributionBase & {
|
|
39
|
+
region: 'header';
|
|
40
|
+
item: ToolbarItem<TCtx>;
|
|
41
|
+
render?: never;
|
|
42
|
+
})
|
|
43
|
+
| (ContributionBase & { region: 'palette'; item: ToolItem; render?: never })
|
|
44
|
+
| (ContributionBase & { region: 'sidebar'; item: SidebarSection; render?: never })
|
|
45
|
+
| (ContributionBase & { region: 'footer'; item: StatusReadout; render?: never })
|
|
46
|
+
| (ContributionBase & {
|
|
47
|
+
region: LabRegion;
|
|
48
|
+
item?: never;
|
|
49
|
+
render: (ctx: TCtx) => ReactNode;
|
|
50
|
+
});
|
package/src/chrome/merge.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import type { TrialContribution } from './types';
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* Concatenate contribution bundles into one list, preserving order — order
|
|
5
3
|
* decides how a region lays its contributions out, so it is part of the
|
|
@@ -8,10 +6,10 @@ import type { TrialContribution } from './types';
|
|
|
8
6
|
* Throws on a duplicate id rather than dropping one: a contribution silently
|
|
9
7
|
* losing to a later bundle is the failure a registry exists to prevent.
|
|
10
8
|
*/
|
|
11
|
-
export function mergeContributions(
|
|
12
|
-
...bundles: readonly
|
|
13
|
-
):
|
|
14
|
-
const out:
|
|
9
|
+
export function mergeContributions<T extends { id: string }>(
|
|
10
|
+
...bundles: readonly (readonly T[])[]
|
|
11
|
+
): T[] {
|
|
12
|
+
const out: T[] = [];
|
|
15
13
|
const seen = new Set<string>();
|
|
16
14
|
for (const bundle of bundles) {
|
|
17
15
|
for (const entry of bundle) {
|
|
@@ -34,10 +32,10 @@ export function mergeContributions(
|
|
|
34
32
|
* silently suppresses nothing is the same class of bug as a duplicate id
|
|
35
33
|
* silently winning.
|
|
36
34
|
*/
|
|
37
|
-
export function suppressContributions(
|
|
38
|
-
bundle: readonly
|
|
35
|
+
export function suppressContributions<T extends { id: string }>(
|
|
36
|
+
bundle: readonly T[],
|
|
39
37
|
ids: readonly string[],
|
|
40
|
-
):
|
|
38
|
+
): T[] {
|
|
41
39
|
const present = new Set(bundle.map((c) => c.id));
|
|
42
40
|
for (const id of ids) {
|
|
43
41
|
if (!present.has(id)) {
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { fireEvent, render, screen } from '@testing-library/react';
|
|
2
2
|
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
-
import type {
|
|
3
|
+
import type { RegionContribution, ToolSlotContext } from '../types';
|
|
4
4
|
import { PaletteRegion } from './PaletteRegion';
|
|
5
5
|
|
|
6
6
|
const Glyph = () => <svg />;
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
// The region reads a tool slot and nothing else, so the fixture is a real
|
|
9
|
+
// `ToolSlotContext` rather than a faked trial.
|
|
10
|
+
function ctxWith(activeToolId: string | null, setActiveTool = vi.fn()): ToolSlotContext {
|
|
11
|
+
return { activeToolId, setActiveTool };
|
|
10
12
|
}
|
|
11
13
|
|
|
12
|
-
function tool(id: string):
|
|
14
|
+
function tool(id: string): RegionContribution<ToolSlotContext> {
|
|
13
15
|
return { id, region: 'palette', item: { icon: Glyph, label: id } };
|
|
14
16
|
}
|
|
15
17
|
|
|
@@ -1,16 +1,28 @@
|
|
|
1
1
|
import { ToolButton, ToolGroup } from '../../passthrough/weasel-ui';
|
|
2
2
|
import { useRovingTabIndex } from '../../primitives/useRovingTabIndex';
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
RegionContribution,
|
|
5
|
+
ToolItem,
|
|
6
|
+
ToolSlotContext,
|
|
7
|
+
TrialChromeContext,
|
|
8
|
+
} from '../types';
|
|
4
9
|
|
|
5
10
|
/** Props for `<PaletteRegion>`. */
|
|
6
|
-
export interface PaletteRegionProps {
|
|
7
|
-
contributions: readonly
|
|
8
|
-
ctx:
|
|
11
|
+
export interface PaletteRegionProps<TCtx extends ToolSlotContext = TrialChromeContext> {
|
|
12
|
+
contributions: readonly RegionContribution<NoInfer<TCtx>>[];
|
|
13
|
+
ctx: TCtx;
|
|
14
|
+
/** Which region's items to lay out. The lab's rail names the same box
|
|
15
|
+
* `palette` too, so this only moves for a chrome that does not. */
|
|
16
|
+
region?: string;
|
|
9
17
|
}
|
|
10
18
|
|
|
11
|
-
/** A
|
|
12
|
-
* slot; this region only reflects it. */
|
|
13
|
-
export function PaletteRegion
|
|
19
|
+
/** A tool strip: a trial's, or the lab's. Selection lives in whichever tool
|
|
20
|
+
* slot the context carries; this region only reflects it. */
|
|
21
|
+
export function PaletteRegion<TCtx extends ToolSlotContext = TrialChromeContext>({
|
|
22
|
+
contributions,
|
|
23
|
+
ctx,
|
|
24
|
+
region = 'palette',
|
|
25
|
+
}: PaletteRegionProps<TCtx>) {
|
|
14
26
|
const { ref, onKeyDown } = useRovingTabIndex<HTMLDivElement>('vertical');
|
|
15
27
|
if (contributions.length === 0) return null;
|
|
16
28
|
return (
|
|
@@ -25,8 +37,8 @@ export function PaletteRegion({ contributions, ctx }: PaletteRegionProps) {
|
|
|
25
37
|
<ToolGroup orientation="vertical">
|
|
26
38
|
{contributions.map((c) => {
|
|
27
39
|
if (c.render) return <span key={c.id}>{c.render(ctx)}</span>;
|
|
28
|
-
if (c.region !==
|
|
29
|
-
const { icon: Icon, label, shortcut, disabled } = c.item;
|
|
40
|
+
if (c.region !== region || !c.item) return null;
|
|
41
|
+
const { icon: Icon, label, shortcut, disabled } = c.item as ToolItem;
|
|
30
42
|
return (
|
|
31
43
|
<ToolButton
|
|
32
44
|
key={c.id}
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
RegionContribution,
|
|
4
|
+
SidebarSection,
|
|
5
|
+
SidebarSlotContext,
|
|
6
|
+
TrialChromeContext,
|
|
7
|
+
} from '../types';
|
|
3
8
|
|
|
4
9
|
/** Props for `<SidebarRegion>`. */
|
|
5
|
-
export interface SidebarRegionProps {
|
|
6
|
-
contributions: readonly
|
|
7
|
-
ctx:
|
|
10
|
+
export interface SidebarRegionProps<TCtx extends SidebarSlotContext = TrialChromeContext> {
|
|
11
|
+
contributions: readonly RegionContribution<NoInfer<TCtx>>[];
|
|
12
|
+
ctx: TCtx;
|
|
13
|
+
/** Which region's items to lay out. */
|
|
14
|
+
region?: string;
|
|
8
15
|
}
|
|
9
16
|
|
|
10
17
|
function Section({
|
|
@@ -48,17 +55,22 @@ function Section({
|
|
|
48
55
|
);
|
|
49
56
|
}
|
|
50
57
|
|
|
51
|
-
/** Lays
|
|
52
|
-
* A section the trial has torn out is not rendered
|
|
53
|
-
* body into the workspace instead.
|
|
54
|
-
|
|
58
|
+
/** Lays `sidebar` contributions out as titled, collapsible sections — a
|
|
59
|
+
* trial's, or the lab's. A section the trial has torn out is not rendered
|
|
60
|
+
* here — `Trial` portals its body into the workspace instead. A context with
|
|
61
|
+
* no `undockPanel` offers no tear-out. */
|
|
62
|
+
export function SidebarRegion<TCtx extends SidebarSlotContext = TrialChromeContext>({
|
|
63
|
+
contributions,
|
|
64
|
+
ctx,
|
|
65
|
+
region = 'sidebar',
|
|
66
|
+
}: SidebarRegionProps<TCtx>) {
|
|
55
67
|
if (contributions.length === 0) return null;
|
|
56
68
|
return (
|
|
57
69
|
<>
|
|
58
70
|
{contributions.map((c) => {
|
|
59
71
|
if (c.render) return <div key={c.id}>{c.render(ctx)}</div>;
|
|
60
|
-
if (c.region !==
|
|
61
|
-
if (ctx.undockedPanels
|
|
72
|
+
if (c.region !== region || !c.item) return null;
|
|
73
|
+
if (ctx.undockedPanels?.includes(c.id)) return null;
|
|
62
74
|
const item = c.item as SidebarSection;
|
|
63
75
|
return (
|
|
64
76
|
<Section
|
|
@@ -67,9 +79,9 @@ export function SidebarRegion({ contributions, ctx }: SidebarRegionProps) {
|
|
|
67
79
|
collapsed={ctx.collapsedSections[c.id] ?? item.defaultCollapsed ?? false}
|
|
68
80
|
onCollapsedChange={(next) => ctx.setSectionCollapsed(c.id, next)}
|
|
69
81
|
onUndock={
|
|
70
|
-
item.undockable
|
|
71
|
-
?
|
|
72
|
-
:
|
|
82
|
+
ctx.undockPanel && item.undockable !== false
|
|
83
|
+
? () => ctx.undockPanel?.(c.id, item.undockAs ?? 'tile')
|
|
84
|
+
: undefined
|
|
73
85
|
}
|
|
74
86
|
>
|
|
75
87
|
{item.body}
|