@weasel-js/labkit 1.4.4 → 1.5.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/README.md +54 -6
- package/dist/_dts/{CanvasStackContext-kTEZvEgE.d.ts → CanvasStackContext-CJNMS6mz.d.ts} +1 -1
- package/dist/_dts/{PrefsForm.d-DDHFANkc.d.ts → PrefsForm.d-DPy9I8It.d.ts} +4 -0
- package/dist/_dts/Select.d-Gwr8fLpK.d.ts +453 -0
- package/dist/_dts/{frac-z7ker2Vx.d.ts → frac-B4okC2lX.d.ts} +291 -31
- package/dist/_dts/{index-Blv9uQzu.d.ts → index-BNdz-tNd.d.ts} +105 -17
- package/dist/_dts/index-CSJoj4LC.d.ts +248 -0
- package/dist/_dts/{types-B9_zrHmb.d.ts → types-DzoNY1pO.d.ts} +91 -4
- package/dist/_dts/usePanZoom-Pzf9f2jh.d.ts +160 -0
- package/dist/_dts/useTrialState-uLlmaccn.d.ts +212 -0
- package/dist/canvas/index.d.ts +18 -129
- package/dist/canvas/index.js +4 -2
- package/dist/chrome/index.d.ts +5 -93
- package/dist/chrome/index.js +10 -6
- package/dist/chunk-7PFH3CNM.js +150 -0
- package/dist/chunk-7PFH3CNM.js.map +1 -0
- package/dist/{chunk-I6JCVE24.js → chunk-CEIUBPPW.js} +143 -17
- package/dist/chunk-CEIUBPPW.js.map +1 -0
- package/dist/chunk-EH7UEQV7.js +590 -0
- package/dist/chunk-EH7UEQV7.js.map +1 -0
- package/dist/{chunk-MOM3GOVY.js → chunk-EHGVROVV.js} +125 -27
- package/dist/chunk-EHGVROVV.js.map +1 -0
- package/dist/{chunk-TJ7QY3OC.js → chunk-ERFMC6WU.js} +160 -78
- package/dist/chunk-ERFMC6WU.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-64ZCN3DA.js → chunk-IHUVQ3TX.js} +250 -160
- package/dist/chunk-IHUVQ3TX.js.map +1 -0
- package/dist/{chunk-FQMJUVHJ.js → chunk-IJJUK4FU.js} +53 -46
- package/dist/chunk-IJJUK4FU.js.map +1 -0
- package/dist/chunk-IXTQI7JX.js +737 -0
- package/dist/chunk-IXTQI7JX.js.map +1 -0
- package/dist/{chunk-BKFVHKJH.js → chunk-MDVTK5U3.js} +4 -4
- package/dist/{chunk-BKFVHKJH.js.map → chunk-MDVTK5U3.js.map} +1 -1
- package/dist/chunk-PHK4W562.js +72 -0
- package/dist/chunk-PHK4W562.js.map +1 -0
- package/dist/chunk-PJ7WEQBJ.js +18 -0
- package/dist/chunk-PJ7WEQBJ.js.map +1 -0
- package/dist/chunk-RWNDDKOH.js +682 -0
- package/dist/chunk-RWNDDKOH.js.map +1 -0
- package/dist/chunk-T3EC2DXD.js +12628 -0
- package/dist/chunk-T3EC2DXD.js.map +1 -0
- package/dist/{chunk-SMHP6XZ4.js → chunk-UN4U4P63.js} +10 -8
- package/dist/chunk-UN4U4P63.js.map +1 -0
- package/dist/chunk-WIG6XHJ7.js +35 -0
- package/dist/chunk-WIG6XHJ7.js.map +1 -0
- package/dist/chunk-Z76KEZAX.js +15 -0
- package/dist/chunk-Z76KEZAX.js.map +1 -0
- package/dist/chunk-ZDHY47EV.js +363 -0
- package/dist/chunk-ZDHY47EV.js.map +1 -0
- package/dist/config/index.d.ts +276 -0
- package/dist/config/index.js +8 -0
- package/dist/config/index.js.map +1 -0
- package/dist/controls/index.d.ts +3 -4
- package/dist/controls/index.js +4 -3
- package/dist/dragdrop/index.d.ts +3 -5
- package/dist/index.d.ts +266 -413
- package/dist/index.js +1775 -2251
- package/dist/index.js.map +1 -1
- package/dist/layers/index.d.ts +4 -6
- package/dist/layers/index.js +2 -2
- package/dist/loupe/index.d.ts +69 -20
- package/dist/loupe/index.js +2 -2
- package/dist/passthrough/weasel-ui.d.ts +175 -309
- package/dist/passthrough/weasel-ui.js +1 -1
- package/dist/primitives/index.d.ts +55 -6
- package/dist/primitives/index.js +6 -4
- package/dist/state/index.d.ts +13 -8
- package/dist/state/index.js +18 -13
- package/dist/state/index.js.map +1 -1
- package/dist/styles.css +223 -76
- 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 +3 -5
- package/package.json +15 -8
- package/src/annotations/AnnotationOverlay.tsx +56 -5
- package/src/annotations/Annotations.less +4 -0
- package/src/annotations/Annotations.overlay.test.tsx +105 -6
- package/src/annotations/drawOne.test.ts +1 -1
- package/src/annotations/drawOne.ts +2 -2
- 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 +5 -4
- package/src/annotations/svgNodes.test.ts +2 -2
- package/src/annotations/svgNodes.ts +1 -0
- package/src/annotations/toolMap.test.ts +9 -5
- package/src/annotations/toolMap.ts +6 -3
- package/src/annotations/types.ts +6 -3
- package/src/canvas/CameraWheelContext.ts +12 -0
- package/src/canvas/CanvasStack.stories.tsx +1 -1
- 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.test.ts +28 -0
- package/src/canvas/usePanZoom.ts +21 -9
- package/src/chrome/ChromeRegions.stories.tsx +1 -4
- package/src/chrome/LabChrome.tsx +130 -0
- package/src/chrome/builtins.test.ts +1 -0
- package/src/chrome/builtins.tsx +21 -31
- package/src/chrome/index.ts +16 -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 +16 -9
- package/src/chrome/regions/SidebarRegion.stories.tsx +44 -0
- package/src/chrome/regions/SidebarRegion.tsx +25 -13
- package/src/chrome/regions/StatusRegion.tsx +15 -8
- package/src/chrome/regions/ToolbarRegion.tsx +34 -16
- package/src/chrome/types.ts +50 -20
- package/src/config/auto.test.ts +12 -0
- package/src/config/auto.ts +15 -0
- package/src/config/autoConfig.test.ts +107 -0
- package/src/config/autoConfig.ts +153 -0
- package/src/config/builder.test.ts +42 -0
- package/src/config/builder.ts +68 -17
- 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 +16 -1
- package/src/config/nodeClasses.test.ts +18 -0
- package/src/config/path.ts +7 -2
- package/src/config/resolve.test.ts +25 -0
- package/src/config/resolve.ts +17 -1
- package/src/config/types.ts +34 -2
- package/src/config/useResolvedConfig.ts +26 -0
- package/src/controls/AutoControls.stories.tsx +88 -0
- package/src/controls/ControlPanel.stories.tsx +42 -1
- package/src/controls/ControlPanel.test.tsx +530 -4
- package/src/controls/ControlPanel.tsx +562 -82
- package/src/fake-indexeddb-auto.d.ts +5 -0
- package/src/index.test.ts +7 -0
- package/src/index.ts +66 -5
- package/src/instrument/serializers.test.ts +22 -0
- package/src/instrument/serializers.ts +40 -1
- package/src/instrument/types.ts +25 -1
- package/src/lab/Lab.chrome.test.tsx +16 -0
- package/src/lab/Lab.less +4 -13
- package/src/lab/Lab.persist.test.tsx +147 -0
- package/src/lab/Lab.stories.tsx +1 -3
- package/src/lab/Lab.surface.test.tsx +71 -3
- package/src/lab/Lab.test.tsx +168 -16
- package/src/lab/Lab.tsx +434 -129
- package/src/lab/LabContext.ts +8 -0
- package/src/lab/LabFit.stories.less +18 -0
- package/src/lab/LabFit.stories.tsx +219 -0
- package/src/lab/LabFullChrome.stories.tsx +1 -4
- package/src/lab/LabHeader.test.tsx +40 -9
- package/src/lab/LabHeader.tsx +7 -14
- package/src/lab/LabPalette.tsx +13 -26
- package/src/lab/LabRoot.test.tsx +56 -0
- package/src/lab/LabRoot.tsx +47 -0
- package/src/lab/LabShell.less +73 -15
- package/src/lab/LabShell.stories.tsx +50 -1
- package/src/lab/LabShell.tsx +18 -27
- package/src/lab/LabSwitcher.less +18 -11
- package/src/lab/LabSwitcher.test.tsx +14 -9
- package/src/lab/LabSwitcher.tsx +8 -4
- package/src/lab/Workspace.less +1 -1
- package/src/lab/Workspace.stories.tsx +1 -1
- package/src/lab/Workspace.surface.test.tsx +1 -0
- package/src/lab/fitCheck.test.ts +57 -0
- package/src/lab/fitCheck.ts +125 -0
- package/src/lab/index.ts +2 -0
- package/src/layers/LayerList.stories.tsx +1 -1
- package/src/loupe/AGENTS.md +17 -5
- package/src/loupe/Loupe.less +1 -1
- package/src/loupe/LoupeGestures.tsx +67 -0
- package/src/loupe/TrialLoupe.tsx +43 -28
- package/src/loupe/canvasLens.ts +1 -1
- package/src/loupe/index.ts +4 -0
- package/src/loupe/loupeActions.ts +98 -0
- package/src/loupe/types.ts +1 -1
- package/src/loupe/useLoupe.test.tsx +60 -3
- package/src/loupe/useLoupe.ts +29 -49
- package/src/passthrough/weasel-ui.ts +9 -0
- package/src/primitives/FloatingPanel.stories.tsx +14 -2
- package/src/primitives/FloatingPanel.test.tsx +47 -16
- package/src/primitives/FloatingPanel.tsx +11 -25
- package/src/primitives/FpsMeter.stories.tsx +1 -1
- package/src/primitives/JobProgress.stories.tsx +1 -1
- package/src/primitives/Legend.stories.tsx +1 -1
- package/src/primitives/ScaleIndicator.stories.tsx +1 -1
- package/src/primitives/Sidebar.less +7 -0
- package/src/primitives/Sidebar.stories.tsx +1 -1
- package/src/primitives/Split.less +14 -0
- package/src/primitives/Split.test.tsx +133 -0
- package/src/primitives/Split.tsx +175 -0
- package/src/primitives/StatusBar.stories.tsx +1 -1
- package/src/primitives/Toolbar.stories.tsx +1 -1
- package/src/primitives/ZoomControl.tsx +5 -2
- package/src/primitives/index.ts +2 -0
- package/src/specimen/Specimen.less +95 -0
- package/src/specimen/Specimen.stories.tsx +13 -0
- package/src/specimen/Specimen.test.tsx +13 -0
- package/src/specimen/Specimen.tsx +888 -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 +1 -1
- 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 +169 -635
- package/src/state/store.ts +146 -163
- package/src/state/toolSlot.test.ts +1 -1
- package/src/state/types.ts +65 -17
- 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 +87 -3
- package/src/state/useTrialState.ts +9 -1
- 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 +1 -0
- package/src/surface/useSurfaceTile.ts +6 -5
- package/src/surface/useTiledSurface.test.tsx +231 -1
- package/src/surface/useTiledSurface.ts +76 -15
- package/src/theme/Interstellar.stories.tsx +9 -9
- package/src/theme/interstellar.test.ts +17 -6
- package/src/theme/interstellar.theme.json +180 -0
- package/src/theme/interstellar.ts +8 -5
- package/src/tools/labTool.ts +11 -0
- package/src/trial/Trial.annotations.persist.test.tsx +100 -58
- package/src/trial/Trial.annotations.test.tsx +59 -7
- package/src/trial/Trial.canvas.test.tsx +32 -0
- package/src/trial/Trial.config.test.tsx +75 -2
- package/src/trial/Trial.job.test.tsx +3 -3
- package/src/trial/Trial.less +5 -26
- package/src/trial/Trial.stories.tsx +5 -3
- package/src/trial/Trial.test.tsx +31 -4
- package/src/trial/Trial.trialId.test.tsx +23 -1
- package/src/trial/Trial.tsx +139 -52
- package/src/trial/TrialBody.tsx +17 -141
- package/src/trial/TrialChrome.tsx +24 -7
- package/src/trial/trialOps.test.ts +101 -1
- package/src/trial/trialOps.ts +67 -7
- 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-2P6PP5N4.js +0 -553
- package/dist/chunk-2P6PP5N4.js.map +0 -1
- package/dist/chunk-64ZCN3DA.js.map +0 -1
- package/dist/chunk-CTRKTLYZ.js.map +0 -1
- package/dist/chunk-E44SU6XS.js +0 -50
- package/dist/chunk-E44SU6XS.js.map +0 -1
- package/dist/chunk-FQMJUVHJ.js.map +0 -1
- package/dist/chunk-I6JCVE24.js.map +0 -1
- package/dist/chunk-MOM3GOVY.js.map +0 -1
- package/dist/chunk-SMHP6XZ4.js.map +0 -1
- package/dist/chunk-TJ7QY3OC.js.map +0 -1
- package/dist/chunk-U3IYHIAE.js +0 -343
- package/dist/chunk-U3IYHIAE.js.map +0 -1
- package/dist/chunk-W3ECWC2K.js +0 -7335
- package/dist/chunk-W3ECWC2K.js.map +0 -1
- package/src/theme/interstellar.tokens.json +0 -61
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// fake-indexeddb's `./auto` export declares no `types` condition, so under
|
|
2
|
+
// `moduleResolution: Bundler` tsc finds no declaration for it — the ambient one
|
|
3
|
+
// the package ships at its root is never part of the compilation. The subpath
|
|
4
|
+
// is side-effect-only, so a bare declaration is the whole of it.
|
|
5
|
+
declare module 'fake-indexeddb/auto';
|
package/src/index.test.ts
CHANGED
|
@@ -51,6 +51,13 @@ describe('public export surface', () => {
|
|
|
51
51
|
});
|
|
52
52
|
});
|
|
53
53
|
|
|
54
|
+
describe('ui re-exports', () => {
|
|
55
|
+
it('carries ToggleBar and Button from the package root', () => {
|
|
56
|
+
expect(labkit).toHaveProperty('ToggleBar');
|
|
57
|
+
expect(labkit).toHaveProperty('Button');
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
54
61
|
describe('surface, job and orbit entry points', () => {
|
|
55
62
|
it('are reachable from the package root', async () => {
|
|
56
63
|
const kit = await import('./index');
|
package/src/index.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
// The property-panel family and
|
|
2
|
-
// re-exports them so
|
|
3
|
-
// not `export *` — a star re-export of an external
|
|
4
|
-
// the bundle.
|
|
1
|
+
// The property-panel family, LayerStack, Input, Select, TokenPanel, ToggleBar and Button live in
|
|
2
|
+
// `@weasel-js/ui`; labkit re-exports them so chrome built on labkit needs no
|
|
3
|
+
// direct ui dependency. Named, not `export *` — a star re-export of an external
|
|
4
|
+
// package emits no binding in the bundle.
|
|
5
|
+
// The playback glyphs ride along for the same reason: chrome that labels a control with
|
|
6
|
+
// one should not need `@weasel-js/ui` in its manifest to draw a play button.
|
|
5
7
|
export type {
|
|
8
|
+
ButtonProps,
|
|
9
|
+
ButtonSize,
|
|
10
|
+
ButtonVariant,
|
|
6
11
|
CheckboxRowProps,
|
|
7
12
|
ColorRowProps,
|
|
8
13
|
CurveFieldProps,
|
|
@@ -10,6 +15,9 @@ export type {
|
|
|
10
15
|
EffectCardListItem,
|
|
11
16
|
EffectCardListProps,
|
|
12
17
|
EffectCardProps,
|
|
18
|
+
IconProps,
|
|
19
|
+
InputProps,
|
|
20
|
+
JogProps,
|
|
13
21
|
LayerStackItem,
|
|
14
22
|
LayerStackProps,
|
|
15
23
|
NumberRowProps,
|
|
@@ -22,30 +30,55 @@ export type {
|
|
|
22
30
|
PropertyRowProps,
|
|
23
31
|
PropertyRowVariant,
|
|
24
32
|
PropertySpanProps,
|
|
33
|
+
SelectProps,
|
|
25
34
|
SelectRowProps,
|
|
26
35
|
SliderRowProps,
|
|
27
36
|
SubpanelProps,
|
|
28
37
|
TextRowProps,
|
|
38
|
+
ToggleBarItem,
|
|
39
|
+
ToggleBarProps,
|
|
40
|
+
ToggleBarSize,
|
|
41
|
+
ToggleBarVariant,
|
|
29
42
|
ToggleRowProps,
|
|
43
|
+
TokenCategory,
|
|
44
|
+
TokenEntry,
|
|
45
|
+
TokenPanelProps,
|
|
30
46
|
} from '@weasel-js/ui';
|
|
31
47
|
export {
|
|
48
|
+
Button,
|
|
32
49
|
CheckboxRow,
|
|
50
|
+
CloseIcon,
|
|
51
|
+
CollapseIcon,
|
|
33
52
|
ColorRow,
|
|
34
53
|
CurveField,
|
|
35
54
|
EffectCard,
|
|
36
55
|
EffectCardList,
|
|
56
|
+
ExpandIcon,
|
|
57
|
+
Icon,
|
|
58
|
+
Input,
|
|
59
|
+
inferTokenType,
|
|
60
|
+
Jog,
|
|
37
61
|
LayerStack,
|
|
38
62
|
NumberRow,
|
|
63
|
+
PauseIcon,
|
|
64
|
+
PlayIcon,
|
|
39
65
|
PropertyGroup,
|
|
40
66
|
PropertyList,
|
|
41
67
|
PropertyPanel,
|
|
42
68
|
PropertyRow,
|
|
43
69
|
PropertySpan,
|
|
70
|
+
ResetIcon,
|
|
71
|
+
Select,
|
|
44
72
|
SelectRow,
|
|
45
73
|
SliderRow,
|
|
74
|
+
StepBackIcon,
|
|
75
|
+
StepForwardIcon,
|
|
76
|
+
StopIcon,
|
|
46
77
|
Subpanel,
|
|
47
78
|
TextRow,
|
|
79
|
+
ToggleBar,
|
|
48
80
|
ToggleRow,
|
|
81
|
+
TokenPanel,
|
|
49
82
|
} from '@weasel-js/ui';
|
|
50
83
|
export type {
|
|
51
84
|
Annotation,
|
|
@@ -118,6 +151,7 @@ export type {
|
|
|
118
151
|
OrbitHandlers,
|
|
119
152
|
OrbitView,
|
|
120
153
|
PanZoomHandlers,
|
|
154
|
+
StageProps,
|
|
121
155
|
UseOrbitOptions,
|
|
122
156
|
UsePanZoomOptions,
|
|
123
157
|
Vec3,
|
|
@@ -133,10 +167,12 @@ export {
|
|
|
133
167
|
centerOn,
|
|
134
168
|
clampPitch,
|
|
135
169
|
DEFAULT_FRAME,
|
|
170
|
+
fitStage,
|
|
136
171
|
orbitAfterDrag,
|
|
137
172
|
orbitAfterWheel,
|
|
138
173
|
PITCH_LIMIT,
|
|
139
174
|
resolveFrame,
|
|
175
|
+
Stage,
|
|
140
176
|
screenToWorld,
|
|
141
177
|
useOrbit,
|
|
142
178
|
usePanZoom,
|
|
@@ -145,7 +181,21 @@ export {
|
|
|
145
181
|
zoomAt,
|
|
146
182
|
} from './canvas';
|
|
147
183
|
export * from './chrome';
|
|
148
|
-
export {
|
|
184
|
+
export { type Auto, auto, isAuto } from './config/auto';
|
|
185
|
+
export { autoPathsOf, resolveAutoConfig } from './config/autoConfig';
|
|
186
|
+
export {
|
|
187
|
+
BaseNode,
|
|
188
|
+
BooleanNode,
|
|
189
|
+
ColorNode,
|
|
190
|
+
CustomNode,
|
|
191
|
+
EnumNode,
|
|
192
|
+
f,
|
|
193
|
+
GroupNode,
|
|
194
|
+
isConfigBranch,
|
|
195
|
+
NumberNode,
|
|
196
|
+
StringNode,
|
|
197
|
+
ValueNode,
|
|
198
|
+
} from './config/builder';
|
|
149
199
|
export { fromConfigFields } from './config/fromConfigField';
|
|
150
200
|
export {
|
|
151
201
|
fillConfigDefaults,
|
|
@@ -181,6 +231,7 @@ export type {
|
|
|
181
231
|
ValueAtPath,
|
|
182
232
|
} from './config/types';
|
|
183
233
|
export { useConfigSchema } from './config/useConfigSchema';
|
|
234
|
+
export { useResolvedConfig } from './config/useResolvedConfig';
|
|
184
235
|
export { isLeafVisible } from './config/visible';
|
|
185
236
|
export { ControlPanel } from './controls/ControlPanel';
|
|
186
237
|
export type {
|
|
@@ -209,6 +260,7 @@ export type {
|
|
|
209
260
|
PaletteItem,
|
|
210
261
|
Point,
|
|
211
262
|
RenderContext,
|
|
263
|
+
StageCapability,
|
|
212
264
|
SystemEvent,
|
|
213
265
|
UndoCapability,
|
|
214
266
|
ViewTransform,
|
|
@@ -221,8 +273,12 @@ export type { LayerListProps, LayerTreeNode } from './layers';
|
|
|
221
273
|
export { LayerList } from './layers';
|
|
222
274
|
export * from './loupe';
|
|
223
275
|
export * from './primitives';
|
|
276
|
+
export { SPECIMEN_SECTIONS, Specimen } from './specimen/Specimen';
|
|
224
277
|
export {
|
|
278
|
+
createIndexedDbAdapter,
|
|
225
279
|
createMemoryAdapter,
|
|
280
|
+
type IndexedDbAdapterOptions,
|
|
281
|
+
indexedDbAdapter,
|
|
226
282
|
localStorageAdapter,
|
|
227
283
|
noneAdapter,
|
|
228
284
|
sessionStorageAdapter,
|
|
@@ -237,6 +293,9 @@ export {
|
|
|
237
293
|
useTrialId,
|
|
238
294
|
} from './state/context';
|
|
239
295
|
export { CURRENT_DOCUMENT_VERSION, labDocumentKey, quarantineKey } from './state/document';
|
|
296
|
+
export { type OpenedLabStore, type OpenLabStoreOptions, openLabStore } from './state/openLabStore';
|
|
297
|
+
export { Persistence, type PersistenceProps } from './state/Persistence';
|
|
298
|
+
export type { RecordCache, RecordChange } from './state/records';
|
|
240
299
|
export type {
|
|
241
300
|
CreateLabStoreOptions,
|
|
242
301
|
LabDocument,
|
|
@@ -245,6 +304,7 @@ export type {
|
|
|
245
304
|
SavedSnapshot,
|
|
246
305
|
SerializedTrial,
|
|
247
306
|
StorageAdapter,
|
|
307
|
+
StorageChange,
|
|
248
308
|
TrialInfo,
|
|
249
309
|
TrialRecord,
|
|
250
310
|
TrialStateHandle,
|
|
@@ -252,6 +312,7 @@ export type {
|
|
|
252
312
|
} from './state/types';
|
|
253
313
|
export type { UndockedPanel, UndockedPanels } from './state/undock';
|
|
254
314
|
export { dockPanel, panelKey, undockPanel } from './state/undock';
|
|
315
|
+
export { type PersistedStateOptions, usePersistedState } from './state/usePersistedState';
|
|
255
316
|
export { useTrialState } from './state/useTrialState';
|
|
256
317
|
export type { ViewTransform2D } from './state/view';
|
|
257
318
|
export { as2DView, DEFAULT_VIEW } from './state/view';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { configMigrationsOf } from './serializers';
|
|
3
|
+
import type { InstrumentList } from './types';
|
|
4
|
+
|
|
5
|
+
const instrument = (name: string, extra: Record<string, unknown> = {}) => ({
|
|
6
|
+
name,
|
|
7
|
+
defaultConfig: () => ({}),
|
|
8
|
+
initialState: () => ({}),
|
|
9
|
+
render: () => null,
|
|
10
|
+
...extra,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe('configMigrationsOf', () => {
|
|
14
|
+
it('keys each declared migration by instrument name, and skips the rest', () => {
|
|
15
|
+
const migrations = configMigrationsOf([
|
|
16
|
+
instrument('moves', { migrateConfig: (c: unknown) => ({ moved: c }) }),
|
|
17
|
+
instrument('stays'),
|
|
18
|
+
] as InstrumentList);
|
|
19
|
+
expect(Object.keys(migrations)).toEqual(['moves']);
|
|
20
|
+
expect(migrations.moves?.({ a: 1 })).toEqual({ moved: { a: 1 } });
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -1,5 +1,33 @@
|
|
|
1
|
+
import { resolveConfigSchema } from '../config/resolve';
|
|
2
|
+
import type { ResolvedConfig } from '../config/types';
|
|
1
3
|
import type { InstrumentSerializers } from '../state/types';
|
|
2
|
-
import type { InstrumentList } from './types';
|
|
4
|
+
import type { Instrument, InstrumentList } from './types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* One instrument's config schema, resolved without the lab's rules — which
|
|
8
|
+
* neither the store nor a trial composed outside a lab has access to. No rule
|
|
9
|
+
* can reach `autoResolve`, `unpinned` or `manual`, so for reading those the
|
|
10
|
+
* two resolutions agree; anything reading the rendered leaves wants
|
|
11
|
+
* `useConfigSchema` instead. `undefined` for a legacy `configSchema()`
|
|
12
|
+
* instrument, which declares no auto annotations at all.
|
|
13
|
+
*/
|
|
14
|
+
export function unruledConfigSchema(
|
|
15
|
+
// biome-ignore lint/suspicious/noExplicitAny: instruments are stored contravariantly; see InstrumentList
|
|
16
|
+
instrument: Instrument<any, any, any>, // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
17
|
+
): ResolvedConfig | undefined {
|
|
18
|
+
return instrument.config ? resolveConfigSchema(instrument.config, []) : undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Each instrument's unruled config schema, keyed by name — what the store
|
|
22
|
+
* reads a leaf's auto annotations off. */
|
|
23
|
+
export function configSchemasOf(instruments: InstrumentList): Record<string, ResolvedConfig> {
|
|
24
|
+
const out: Record<string, ResolvedConfig> = {};
|
|
25
|
+
for (const instrument of instruments) {
|
|
26
|
+
const schema = unruledConfigSchema(instrument);
|
|
27
|
+
if (schema) out[instrument.name] = schema;
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
3
31
|
|
|
4
32
|
/**
|
|
5
33
|
* Each instrument's default config, keyed by name — what `createLabStore`
|
|
@@ -12,6 +40,17 @@ export function configDefaultsOf(instruments: InstrumentList): Record<string, ()
|
|
|
12
40
|
return out;
|
|
13
41
|
}
|
|
14
42
|
|
|
43
|
+
/** Each instrument's `migrateConfig`, keyed by name, for those that declare one. */
|
|
44
|
+
export function configMigrationsOf(
|
|
45
|
+
instruments: InstrumentList,
|
|
46
|
+
): Record<string, (stored: unknown) => unknown> {
|
|
47
|
+
const out: Record<string, (stored: unknown) => unknown> = {};
|
|
48
|
+
for (const { name, migrateConfig } of instruments) {
|
|
49
|
+
if (migrateConfig) out[name] = (stored) => migrateConfig(stored);
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
|
|
15
54
|
/**
|
|
16
55
|
* The `serialize` / `deserialize` an instrument list declares, keyed by
|
|
17
56
|
* instrument name — what `createLabStore` needs to rebuild a trial's state
|
package/src/instrument/types.ts
CHANGED
|
@@ -67,6 +67,19 @@ export interface CanvasCapability<TS = unknown, TC = unknown> {
|
|
|
67
67
|
maxZoom?: number;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
/** Declares that an instrument's DOM is content of a fixed size, which the
|
|
71
|
+
* trial pans and zooms the way it does a canvas's layers. Not combined with
|
|
72
|
+
* `canvas`: there the DOM is an unzoomed overlay, and `canvas` wins. */
|
|
73
|
+
export interface StageCapability {
|
|
74
|
+
/** The content's own size in CSS pixels, at zoom 1. */
|
|
75
|
+
size: ViewportSize;
|
|
76
|
+
/** Where the view starts. Omitted, the content opens centered and shrunk to
|
|
77
|
+
* fit (`fitStage`). A function is called once the stage knows its size. */
|
|
78
|
+
initialView?: ViewTransform | ((viewport: ViewportSize) => ViewTransform);
|
|
79
|
+
minZoom?: number;
|
|
80
|
+
maxZoom?: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
70
83
|
/** Declares which of an instrument's layers the trial should offer
|
|
71
84
|
* show/hide controls for. */
|
|
72
85
|
export interface LayerCapability {
|
|
@@ -99,7 +112,8 @@ export type SystemEvent = string;
|
|
|
99
112
|
export type Point = { x: number; y: number };
|
|
100
113
|
/** What a hit-test found, and where. */
|
|
101
114
|
export type HitResult = { hit: boolean; layerId?: string; pointId?: string };
|
|
102
|
-
/** A trial's camera.
|
|
115
|
+
/** A trial's camera. `zoom` is always positive and finite once labkit holds
|
|
116
|
+
* it — see `normalize2DView`. */
|
|
103
117
|
export type ViewTransform = { zoom: number; pan: Point };
|
|
104
118
|
/** A layer as the layer list shows it. `alwaysOn` layers cannot be hidden. */
|
|
105
119
|
export type LayerDescriptor = { id: string; label: string; alwaysOn?: boolean };
|
|
@@ -119,7 +133,10 @@ export type DragFeedback = { ok: boolean; reason?: string };
|
|
|
119
133
|
* is what makes the trial provide the corresponding chrome.
|
|
120
134
|
*/
|
|
121
135
|
export interface Instrument<TS = unknown, TC = unknown, TItem = unknown> {
|
|
136
|
+
/** The id a trial record names its instrument by. */
|
|
122
137
|
name: string;
|
|
138
|
+
/** What a trial of this instrument and the add-trial menu read. Default: `name`. */
|
|
139
|
+
title?: string;
|
|
123
140
|
defaultConfig: () => TC;
|
|
124
141
|
initialState: (config: TC) => TS;
|
|
125
142
|
/** The instrument's config, declared once: values, types and controls.
|
|
@@ -133,9 +150,16 @@ export interface Instrument<TS = unknown, TC = unknown, TItem = unknown> {
|
|
|
133
150
|
* layers rather than instead of them; return `null` for canvas only. */
|
|
134
151
|
render: (ctx: RenderContext<TS, TC>) => ReactNode;
|
|
135
152
|
onConfigChange?: (config: TC, prev: TC, state: TS) => TS;
|
|
153
|
+
/** Moves a stored config's values to where the current schema keeps them —
|
|
154
|
+
* `gridSize` to `grid.size` — before the defaults fill its gaps, so it
|
|
155
|
+
* returns only what it moved and leaves the rest to them. It runs on every
|
|
156
|
+
* read of a stored config, including one already moved, so it must hand
|
|
157
|
+
* back a current config unchanged. */
|
|
158
|
+
migrateConfig?: (stored: unknown) => unknown;
|
|
136
159
|
serialize?: (state: TS) => unknown;
|
|
137
160
|
deserialize?: (data: unknown, config: TC) => TS;
|
|
138
161
|
canvas?: CanvasCapability<TS, TC>;
|
|
162
|
+
stage?: StageCapability;
|
|
139
163
|
layers?: LayerCapability;
|
|
140
164
|
dragDrop?: DragDropCapability<TS, TC>;
|
|
141
165
|
undo?: UndoCapability;
|
|
@@ -62,4 +62,20 @@ describe('<Lab> chrome', () => {
|
|
|
62
62
|
render(<Lab title="T" instruments={[withChrome]} defaultInstrument="Bare" />);
|
|
63
63
|
expect(screen.getByText('ready')).toBeInTheDocument();
|
|
64
64
|
});
|
|
65
|
+
|
|
66
|
+
it('renders an aside contribution in its own pane, after the workspace', () => {
|
|
67
|
+
render(
|
|
68
|
+
<Lab
|
|
69
|
+
title="T"
|
|
70
|
+
instruments={[bare]}
|
|
71
|
+
defaultInstrument="Bare"
|
|
72
|
+
labChrome={[{ id: 'notes', region: 'aside', item: { title: 'Notes', body: <p>jot</p> } }]}
|
|
73
|
+
/>,
|
|
74
|
+
);
|
|
75
|
+
const body = screen.getByText('jot');
|
|
76
|
+
const trial = screen.getByRole('region', { name: /trial/i });
|
|
77
|
+
expect(body.closest('.lk-lab__aside')).not.toBeNull();
|
|
78
|
+
expect(trial.compareDocumentPosition(body) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
|
79
|
+
expect(screen.getByRole('separator', { name: /Lab aside/ })).toBeInTheDocument();
|
|
80
|
+
});
|
|
65
81
|
});
|
package/src/lab/Lab.less
CHANGED
|
@@ -1,17 +1,8 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
// mounts no lab is untouched, and stopping at the lab's own parent so an
|
|
6
|
-
// embedded lab cannot resize its host's layout.
|
|
7
|
-
html:has(.lk-lab),
|
|
8
|
-
body:has(.lk-lab) {
|
|
1
|
+
// A fullscreen lab's page is the viewport, and body's default margin would
|
|
2
|
+
// scroll it. `<LabShell>` alone fills the viewport too. `:has` leaves a page
|
|
3
|
+
// that mounts neither untouched.
|
|
4
|
+
body:has(.lk-lab, .lk-shell) {
|
|
9
5
|
margin: 0;
|
|
10
|
-
height: 100%;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
body:has(.lk-lab) > :has(> .lk-lab) {
|
|
14
|
-
height: 100%;
|
|
15
6
|
}
|
|
16
7
|
|
|
17
8
|
.lk-lab {
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import 'fake-indexeddb/auto';
|
|
2
|
+
import { act, render, screen, waitFor } from '@testing-library/react';
|
|
3
|
+
import { StrictMode } from 'react';
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import type { Instrument } from '../instrument/types';
|
|
6
|
+
import { createMemoryAdapter, indexedDbAdapter, resetDefaultStorage } from '../state/adapters';
|
|
7
|
+
import { labPrefix } from '../state/labRecords';
|
|
8
|
+
import type { StorageAdapter } from '../state/types';
|
|
9
|
+
import { Lab } from './Lab';
|
|
10
|
+
import { LabContext, type LabContextValue } from './LabContext';
|
|
11
|
+
|
|
12
|
+
const stub: Instrument = {
|
|
13
|
+
name: 'Stub',
|
|
14
|
+
defaultConfig: () => ({}),
|
|
15
|
+
initialState: () => ({}),
|
|
16
|
+
render: () => <div data-testid="stub-content">stub</div>,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
let labRef: LabContextValue | null = null;
|
|
20
|
+
function CaptureLab() {
|
|
21
|
+
return (
|
|
22
|
+
<LabContext.Consumer>
|
|
23
|
+
{(value) => {
|
|
24
|
+
labRef = value;
|
|
25
|
+
return null;
|
|
26
|
+
}}
|
|
27
|
+
</LabContext.Consumer>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** A memory adapter whose first `list` waits until released. */
|
|
32
|
+
function gated() {
|
|
33
|
+
const memory = createMemoryAdapter();
|
|
34
|
+
let release = (): void => {};
|
|
35
|
+
const opened = new Promise<void>((resolve) => {
|
|
36
|
+
release = resolve;
|
|
37
|
+
});
|
|
38
|
+
const storage: StorageAdapter = {
|
|
39
|
+
...memory,
|
|
40
|
+
list: async (prefix) => {
|
|
41
|
+
await opened;
|
|
42
|
+
return memory.list(prefix);
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
return { storage, release };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
afterEach(() => {
|
|
49
|
+
resetDefaultStorage();
|
|
50
|
+
vi.restoreAllMocks();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('<Lab> with a storageKey', () => {
|
|
54
|
+
it('renders its fallback until the lab has loaded', async () => {
|
|
55
|
+
const { storage, release } = gated();
|
|
56
|
+
render(
|
|
57
|
+
<Lab
|
|
58
|
+
instruments={[stub]}
|
|
59
|
+
defaultInstrument="Stub"
|
|
60
|
+
storageKey="gate"
|
|
61
|
+
storage={storage}
|
|
62
|
+
fallback={<p>loading</p>}
|
|
63
|
+
/>,
|
|
64
|
+
);
|
|
65
|
+
expect(screen.getByText('loading')).toBeInTheDocument();
|
|
66
|
+
expect(screen.queryByTestId('stub-content')).toBeNull();
|
|
67
|
+
await act(async () => release());
|
|
68
|
+
expect(await screen.findByTestId('stub-content')).toBeInTheDocument();
|
|
69
|
+
expect(screen.queryByText('loading')).toBeNull();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("falls back to the lab's empty shell by default", () => {
|
|
73
|
+
const { storage } = gated();
|
|
74
|
+
const { container } = render(
|
|
75
|
+
<Lab
|
|
76
|
+
instruments={[stub]}
|
|
77
|
+
defaultInstrument="Stub"
|
|
78
|
+
storageKey="gate"
|
|
79
|
+
storage={storage}
|
|
80
|
+
title="Waiting"
|
|
81
|
+
/>,
|
|
82
|
+
);
|
|
83
|
+
expect(container.querySelector('.lk-lab .lk-shell')).not.toBeNull();
|
|
84
|
+
expect(screen.getByText('Waiting')).toBeInTheDocument();
|
|
85
|
+
expect(screen.queryByRole('region', { name: /trial/i })).toBeNull();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('opens the lab once under StrictMode', async () => {
|
|
89
|
+
const storage = createMemoryAdapter();
|
|
90
|
+
const list = vi.spyOn(storage, 'list');
|
|
91
|
+
render(
|
|
92
|
+
<StrictMode>
|
|
93
|
+
<Lab instruments={[stub]} defaultInstrument="Stub" storageKey="strict" storage={storage} />
|
|
94
|
+
</StrictMode>,
|
|
95
|
+
);
|
|
96
|
+
await screen.findByTestId('stub-content');
|
|
97
|
+
expect(list).toHaveBeenCalledTimes(1);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('persists to IndexedDB when given no storage', async () => {
|
|
101
|
+
const view = render(
|
|
102
|
+
<Lab instruments={[stub]} defaultInstrument="Stub" storageKey="idb-lab">
|
|
103
|
+
<CaptureLab />
|
|
104
|
+
</Lab>,
|
|
105
|
+
);
|
|
106
|
+
await screen.findByTestId('stub-content');
|
|
107
|
+
act(() => labRef?.setMode('light'));
|
|
108
|
+
view.unmount();
|
|
109
|
+
await waitFor(async () => {
|
|
110
|
+
const records = new Map(await indexedDbAdapter.list(labPrefix('idb-lab')));
|
|
111
|
+
expect(records.get(`${labPrefix('idb-lab')}meta`)).toMatchObject({ mode: 'light' });
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('warns when its storageKey changes after mount, and keeps the first', async () => {
|
|
116
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
117
|
+
const storage = createMemoryAdapter();
|
|
118
|
+
const view = render(
|
|
119
|
+
<Lab instruments={[stub]} defaultInstrument="Stub" storageKey="one" storage={storage} />,
|
|
120
|
+
);
|
|
121
|
+
await screen.findByTestId('stub-content');
|
|
122
|
+
view.rerender(
|
|
123
|
+
<Lab instruments={[stub]} defaultInstrument="Stub" storageKey="two" storage={storage} />,
|
|
124
|
+
);
|
|
125
|
+
expect(warn).toHaveBeenCalledWith(expect.stringContaining('`storageKey`'));
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('closes the lab on unmount, sending what was queued', async () => {
|
|
129
|
+
const backing = new Map<string, unknown>();
|
|
130
|
+
const view = render(
|
|
131
|
+
<Lab
|
|
132
|
+
instruments={[stub]}
|
|
133
|
+
defaultInstrument="Stub"
|
|
134
|
+
storageKey="close"
|
|
135
|
+
storage={createMemoryAdapter(backing)}
|
|
136
|
+
>
|
|
137
|
+
<CaptureLab />
|
|
138
|
+
</Lab>,
|
|
139
|
+
);
|
|
140
|
+
await screen.findByTestId('stub-content');
|
|
141
|
+
act(() => labRef?.setMode('dark'));
|
|
142
|
+
view.unmount();
|
|
143
|
+
await waitFor(() =>
|
|
144
|
+
expect(backing.get(`${labPrefix('close')}meta`)).toMatchObject({ mode: 'dark' }),
|
|
145
|
+
);
|
|
146
|
+
});
|
|
147
|
+
});
|
package/src/lab/Lab.stories.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from '@
|
|
1
|
+
import type { Meta, StoryObj } from '@weasel-js/forge';
|
|
2
2
|
import { useEffect } from 'react';
|
|
3
3
|
import type { Instrument } from '../instrument/types';
|
|
4
4
|
import { Lab } from './Lab';
|
|
@@ -27,7 +27,6 @@ export const Default: Story = {
|
|
|
27
27
|
instruments: [StubInstrument],
|
|
28
28
|
defaultInstrument: 'Stub',
|
|
29
29
|
title: 'Default Lab',
|
|
30
|
-
storage: null,
|
|
31
30
|
},
|
|
32
31
|
};
|
|
33
32
|
|
|
@@ -44,7 +43,6 @@ export const TwoTrials: Story = {
|
|
|
44
43
|
instruments: [StubInstrument],
|
|
45
44
|
defaultInstrument: 'Stub',
|
|
46
45
|
title: 'Two Trials',
|
|
47
|
-
storage: null,
|
|
48
46
|
children: <AddSecondTrial />,
|
|
49
47
|
},
|
|
50
48
|
};
|
|
@@ -12,7 +12,12 @@ import type { ReactNode } from 'react';
|
|
|
12
12
|
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
13
13
|
import { defineInstrument } from '../instrument/defineInstrument';
|
|
14
14
|
import { SurfaceContext } from '../surface/SurfaceContext';
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
useSurfaceCanvas,
|
|
17
|
+
useSurfaceOptional,
|
|
18
|
+
useSurfaceTile,
|
|
19
|
+
useTileId,
|
|
20
|
+
} from '../surface/useSurfaceTile';
|
|
16
21
|
import type { SurfaceFrame, SurfaceHandle } from '../surface/useTiledSurface';
|
|
17
22
|
import { useTiledSurface } from '../surface/useTiledSurface';
|
|
18
23
|
import { Lab } from './Lab';
|
|
@@ -43,9 +48,21 @@ let seen: SurfaceHandle | null | 'not-rendered' = 'not-rendered';
|
|
|
43
48
|
* trial is scoped by it, and the trial's id is minted by the store. */
|
|
44
49
|
let paneTileId = 'pane';
|
|
45
50
|
|
|
51
|
+
interface SeenCanvases {
|
|
52
|
+
over: HTMLCanvasElement | null;
|
|
53
|
+
under: HTMLCanvasElement | null;
|
|
54
|
+
byDefault: HTMLCanvasElement | null;
|
|
55
|
+
}
|
|
56
|
+
let seenCanvases: SeenCanvases = { over: null, under: null, byDefault: null };
|
|
57
|
+
|
|
46
58
|
function Probe() {
|
|
47
59
|
seen = useSurfaceOptional();
|
|
48
60
|
paneTileId = useTileId('pane');
|
|
61
|
+
seenCanvases = {
|
|
62
|
+
over: useSurfaceCanvas('over'),
|
|
63
|
+
under: useSurfaceCanvas('under'),
|
|
64
|
+
byDefault: useSurfaceCanvas(),
|
|
65
|
+
};
|
|
49
66
|
const tile = useSurfaceTile('pane');
|
|
50
67
|
return <div data-testid="pane" ref={tile} />;
|
|
51
68
|
}
|
|
@@ -66,10 +83,34 @@ describe('<Lab> surface provider', () => {
|
|
|
66
83
|
expect(typeof (seen as unknown as SurfaceHandle).registerTile).toBe('function');
|
|
67
84
|
});
|
|
68
85
|
|
|
69
|
-
it('
|
|
86
|
+
it('stacks an under and an over buffer inside the lab body', () => {
|
|
70
87
|
const { container } = render(<Lab instruments={[probeInstrument]} defaultInstrument="Probe" />);
|
|
71
88
|
const canvases = container.querySelectorAll('.lk-lab__body > canvas.lk-lab__surface');
|
|
72
|
-
expect(canvases).toHaveLength(
|
|
89
|
+
expect(canvases).toHaveLength(2);
|
|
90
|
+
// Source order, because the z-index that separates them is in a stylesheet
|
|
91
|
+
// jsdom does not resolve — this is a proxy for the stacking, not proof of
|
|
92
|
+
// it. The real check is a browser.
|
|
93
|
+
expect([...canvases].map((c) => c.className)).toEqual([
|
|
94
|
+
'lk-lab__surface lk-lab__surface--under',
|
|
95
|
+
'lk-lab__surface lk-lab__surface--over',
|
|
96
|
+
]);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('hands a tenant whichever of the two it asks for', () => {
|
|
100
|
+
const { container } = render(<Lab instruments={[probeInstrument]} defaultInstrument="Probe" />);
|
|
101
|
+
const [underEl, overEl] = [...container.querySelectorAll('canvas.lk-lab__surface')];
|
|
102
|
+
expect(seenCanvases.over).toBe(overEl);
|
|
103
|
+
expect(seenCanvases.under).toBe(underEl);
|
|
104
|
+
expect(overEl).not.toBe(underEl);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('defaults a tenant to the buffer over the trials', () => {
|
|
108
|
+
// Every tenant predating the split wants the over one: the surface was
|
|
109
|
+
// built for marks that annotate an instrument.
|
|
110
|
+
const { container } = render(<Lab instruments={[probeInstrument]} defaultInstrument="Probe" />);
|
|
111
|
+
const [underEl, overEl] = [...container.querySelectorAll('canvas.lk-lab__surface')];
|
|
112
|
+
expect(seenCanvases.byDefault).toBe(overEl);
|
|
113
|
+
expect(seenCanvases.byDefault).not.toBe(underEl);
|
|
73
114
|
});
|
|
74
115
|
|
|
75
116
|
it('mounts no buffer of its own when a host already owns the surface', () => {
|
|
@@ -165,3 +206,30 @@ describe('<Lab> tile round trip', () => {
|
|
|
165
206
|
expect(rect).toEqual({ x: 240, y: 100, w: 320, h: 200 });
|
|
166
207
|
});
|
|
167
208
|
});
|
|
209
|
+
|
|
210
|
+
describe('<Lab> pages', () => {
|
|
211
|
+
it("gives the title a way back to the project's other labs", () => {
|
|
212
|
+
const { getByRole } = render(
|
|
213
|
+
<Lab
|
|
214
|
+
instruments={[probeInstrument]}
|
|
215
|
+
defaultInstrument="Probe"
|
|
216
|
+
title="kliegsminister"
|
|
217
|
+
path="/kliegsminister"
|
|
218
|
+
pages={[
|
|
219
|
+
{ href: '/kliegsminister', label: 'kliegsminister' },
|
|
220
|
+
{ href: '/stats', label: 'corpus stats' },
|
|
221
|
+
]}
|
|
222
|
+
/>,
|
|
223
|
+
);
|
|
224
|
+
// A switcher, not a bare heading: the lab it came from has to be reachable.
|
|
225
|
+
expect(getByRole('button', { name: /kliegsminister/ })).toBeTruthy();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('leaves the title a plain heading when there are no other labs', () => {
|
|
229
|
+
const { getByRole, queryByRole } = render(
|
|
230
|
+
<Lab instruments={[probeInstrument]} defaultInstrument="Probe" title="solo" />,
|
|
231
|
+
);
|
|
232
|
+
expect(getByRole('heading', { name: 'solo' })).toBeTruthy();
|
|
233
|
+
expect(queryByRole('button', { name: /solo/ })).toBeNull();
|
|
234
|
+
});
|
|
235
|
+
});
|