@weasel-js/labkit 1.5.0 → 1.5.2
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 +41 -4
- package/dist/_dts/{CanvasStackContext-CqkxkFJW.d.ts → CanvasStackContext-CMMo180l.d.ts} +1 -1
- package/dist/_dts/{PrefsForm.d-DDHFANkc.d.ts → PrefsForm.d-DPy9I8It.d.ts} +4 -0
- package/dist/_dts/Select.d-ZTiwzYyT.d.ts +479 -0
- package/dist/_dts/{frac-8R6UmlvN.d.ts → frac-B4hFJMiN.d.ts} +54 -6
- package/dist/_dts/{index-retnAut7.d.ts → index-8LiKe3A-.d.ts} +77 -10
- package/dist/_dts/{index-B8uu2ba7.d.ts → index-DlKolEIZ.d.ts} +20 -8
- package/dist/_dts/{types-Ca2LCnPq.d.ts → types-Db-3NNk1.d.ts} +28 -4
- package/dist/_dts/{usePanZoom-nS798rOF.d.ts → usePanZoom-SSTZe410.d.ts} +4 -47
- package/dist/_dts/{useTrialState-jcx_TdU6.d.ts → useTrialState-pnAE-UBD.d.ts} +1 -1
- package/dist/canvas/index.d.ts +5 -5
- package/dist/canvas/index.js +1 -1
- package/dist/chrome/index.d.ts +4 -4
- package/dist/chrome/index.js +7 -7
- package/dist/chunk-6BUQ77ZV.js +18 -0
- package/dist/chunk-6BUQ77ZV.js.map +1 -0
- package/dist/{chunk-KJALCDWE.js → chunk-753Y6HE6.js} +9 -9
- package/dist/chunk-753Y6HE6.js.map +1 -0
- package/dist/{chunk-6ZDGOZQV.js → chunk-A2ACAGW6.js} +72 -20
- package/dist/chunk-A2ACAGW6.js.map +1 -0
- package/dist/{chunk-NS64DXMN.js → chunk-CKUKF72R.js} +44 -21
- package/dist/chunk-CKUKF72R.js.map +1 -0
- package/dist/{chunk-MUKOW3TC.js → chunk-D35PFTS7.js} +4 -4
- package/dist/{chunk-MUKOW3TC.js.map → chunk-D35PFTS7.js.map} +1 -1
- package/dist/{chunk-UU3NJO6N.js → chunk-EMXAIRUJ.js} +3 -3
- package/dist/{chunk-UU3NJO6N.js.map → chunk-EMXAIRUJ.js.map} +1 -1
- package/dist/{chunk-ZPH5WTWF.js → chunk-ERFMC6WU.js} +142 -66
- package/dist/chunk-ERFMC6WU.js.map +1 -0
- package/dist/chunk-FKLT3ITC.js +590 -0
- package/dist/chunk-FKLT3ITC.js.map +1 -0
- package/dist/{chunk-QLSV2N3G.js → chunk-IJJUK4FU.js} +25 -13
- package/dist/chunk-IJJUK4FU.js.map +1 -0
- package/dist/{chunk-EKIICY6X.js → chunk-KJCUERQF.js} +37 -25
- package/dist/chunk-KJCUERQF.js.map +1 -0
- package/dist/chunk-LASCBWW7.js +13456 -0
- package/dist/chunk-LASCBWW7.js.map +1 -0
- package/dist/{chunk-67SJLMC7.js → chunk-LRI3MBGR.js} +45 -12
- package/dist/chunk-LRI3MBGR.js.map +1 -0
- package/dist/chunk-REOBGYMC.js +150 -0
- package/dist/chunk-REOBGYMC.js.map +1 -0
- package/dist/{chunk-PGETSEDK.js → chunk-TT5C5S6R.js} +11 -90
- package/dist/chunk-TT5C5S6R.js.map +1 -0
- package/dist/{chunk-NDRYLVOW.js → chunk-Z76KEZAX.js} +2 -2
- package/dist/chunk-Z76KEZAX.js.map +1 -0
- package/dist/config/index.d.ts +62 -19
- package/dist/config/index.js +6 -4
- package/dist/controls/index.d.ts +3 -3
- package/dist/controls/index.js +4 -4
- package/dist/dragdrop/index.d.ts +3 -3
- package/dist/index.d.ts +230 -62
- package/dist/index.js +1367 -1698
- package/dist/index.js.map +1 -1
- package/dist/layers/index.d.ts +4 -4
- package/dist/layers/index.js +2 -2
- package/dist/loupe/index.d.ts +67 -16
- package/dist/loupe/index.js +1 -1
- package/dist/passthrough/weasel-ui.d.ts +57 -230
- package/dist/passthrough/weasel-ui.js +1 -1
- package/dist/primitives/index.d.ts +3 -1
- package/dist/primitives/index.js +3 -3
- package/dist/state/index.d.ts +5 -5
- package/dist/state/index.js +5 -3
- package/dist/state/index.js.map +1 -1
- package/dist/styles.css +266 -57
- package/dist/surface/index.js +1 -1
- package/dist/ui/layers/index.js +1 -1
- package/dist/undo/index.d.ts +3 -3
- package/package.json +9 -9
- package/src/annotations/AnnotationOverlay.tsx +3 -0
- package/src/annotations/Annotations.less +4 -0
- package/src/annotations/Annotations.overlay.test.tsx +20 -0
- package/src/annotations/svgNodes.ts +1 -0
- package/src/annotations/toolMap.test.ts +9 -5
- package/src/annotations/toolMap.ts +3 -0
- package/src/canvas/CanvasStack.stories.tsx +1 -1
- package/src/canvas/index.ts +0 -9
- package/src/canvas/usePanZoom.test.ts +28 -0
- package/src/canvas/usePanZoom.ts +11 -4
- package/src/chrome/ChromeRegions.stories.tsx +1 -1
- package/src/chrome/LabChrome.tsx +21 -5
- package/src/chrome/builtins.test.ts +1 -0
- package/src/chrome/builtins.tsx +1 -0
- package/src/chrome/index.ts +1 -0
- package/src/chrome/labTypes.ts +3 -3
- package/src/chrome/regions/PaletteRegion.test.tsx +18 -0
- package/src/chrome/regions/PaletteRegion.tsx +6 -10
- package/src/chrome/regions/SidebarRegion.stories.tsx +44 -0
- package/src/chrome/regions/TitleBarRegion.tsx +1 -1
- package/src/chrome/regions/ToolbarRegion.tsx +11 -7
- package/src/chrome/types.ts +11 -2
- 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 +38 -0
- package/src/config/builder.ts +55 -11
- package/src/config/index.ts +3 -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 +32 -2
- package/src/config/useResolvedConfig.ts +26 -0
- package/src/controls/AutoControls.stories.tsx +88 -0
- package/src/controls/ControlPanel.less +23 -0
- package/src/controls/ControlPanel.stories.tsx +83 -1
- package/src/controls/ControlPanel.test.tsx +457 -4
- package/src/controls/ControlPanel.tsx +534 -73
- package/src/index.test.ts +0 -2
- package/src/index.ts +33 -11
- package/src/instrument/serializers.ts +29 -1
- package/src/lab/Lab.chrome.test.tsx +16 -0
- package/src/lab/Lab.stories.tsx +1 -1
- package/src/lab/Lab.surface.test.tsx +7 -2
- package/src/lab/Lab.test.tsx +79 -1
- package/src/lab/Lab.tsx +124 -16
- package/src/lab/LabContext.ts +8 -0
- package/src/lab/LabFit.stories.tsx +4 -4
- package/src/lab/LabFullChrome.stories.tsx +1 -1
- package/src/lab/LabHeader.tsx +0 -1
- package/src/lab/LabRoot.test.tsx +56 -0
- package/src/lab/LabRoot.tsx +47 -0
- package/src/lab/LabShell.less +24 -3
- package/src/lab/LabShell.stories.tsx +50 -1
- package/src/lab/LabShell.tsx +18 -27
- package/src/lab/LabSwitcher.less +10 -10
- 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 -1
- package/src/lab/index.ts +2 -0
- package/src/layers/LayerList.less +1 -1
- package/src/layers/LayerList.stories.tsx +2 -2
- package/src/loupe/AGENTS.md +16 -4
- package/src/loupe/LoupeGestures.tsx +67 -0
- package/src/loupe/TrialLoupe.tsx +43 -28
- package/src/loupe/index.ts +4 -0
- package/src/loupe/loupeActions.ts +98 -0
- package/src/loupe/useLoupe.test.tsx +59 -2
- package/src/loupe/useLoupe.ts +28 -48
- package/src/passthrough/weasel-ui.ts +3 -3
- package/src/primitives/FloatingPanel.stories.tsx +1 -1
- package/src/primitives/FpsMeter.less +6 -1
- package/src/primitives/FpsMeter.stories.tsx +1 -1
- package/src/primitives/FpsMeter.test.tsx +12 -4
- package/src/primitives/FpsMeter.tsx +6 -1
- package/src/primitives/JobProgress.stories.tsx +2 -2
- 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.test.tsx +25 -0
- package/src/primitives/Split.tsx +36 -20
- package/src/primitives/StatusBar.less +6 -0
- package/src/primitives/StatusBar.stories.tsx +1 -1
- package/src/primitives/Toolbar.stories.tsx +1 -1
- 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 +4 -4
- package/src/state/store.test.ts +65 -0
- package/src/state/store.ts +32 -8
- package/src/state/types.ts +31 -2
- package/src/state/useTrialState.test.tsx +85 -0
- package/src/state/useTrialState.ts +9 -1
- package/src/state/view.test.ts +4 -4
- package/src/styles.less +1 -0
- package/src/surface/index.ts +1 -1
- package/src/surface/useSurfaceTile.test.tsx +1 -1
- package/src/surface/useTiledSurface.test.tsx +65 -10
- package/src/surface/useTiledSurface.ts +23 -11
- package/src/theme/Interstellar.stories.tsx +2 -2
- package/src/theme/interstellar.test.ts +16 -5
- package/src/theme/interstellar.theme.json +180 -0
- package/src/theme/interstellar.ts +8 -5
- package/src/tools/labTool.ts +3 -3
- package/src/trial/Trial.annotations.test.tsx +13 -4
- package/src/trial/Trial.canvas.test.tsx +36 -0
- package/src/trial/Trial.config.test.tsx +75 -2
- package/src/trial/Trial.less +4 -0
- package/src/trial/Trial.stories.tsx +4 -1
- package/src/trial/Trial.test.tsx +3 -0
- package/src/trial/Trial.tsx +21 -14
- package/src/trial/TrialChrome.tsx +17 -3
- package/src/trial/trialOps.test.ts +101 -1
- package/src/trial/trialOps.ts +62 -4
- package/dist/_dts/ToggleBar.d-DbHoWKIA.d.ts +0 -116
- package/dist/chunk-67SJLMC7.js.map +0 -1
- package/dist/chunk-6ZDGOZQV.js.map +0 -1
- package/dist/chunk-E44SU6XS.js +0 -50
- package/dist/chunk-E44SU6XS.js.map +0 -1
- package/dist/chunk-EKIICY6X.js.map +0 -1
- package/dist/chunk-KJALCDWE.js.map +0 -1
- package/dist/chunk-NDRYLVOW.js.map +0 -1
- package/dist/chunk-NS64DXMN.js.map +0 -1
- package/dist/chunk-PGETSEDK.js.map +0 -1
- package/dist/chunk-QLSV2N3G.js.map +0 -1
- package/dist/chunk-S2SHZRD7.js +0 -7681
- package/dist/chunk-S2SHZRD7.js.map +0 -1
- package/dist/chunk-TQBBYTAF.js +0 -280
- package/dist/chunk-TQBBYTAF.js.map +0 -1
- package/dist/chunk-ZPH5WTWF.js.map +0 -1
- package/src/canvas/useOrbit.test.ts +0 -167
- package/src/canvas/useOrbit.ts +0 -156
- package/src/theme/interstellar.tokens.json +0 -61
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "interstellar",
|
|
3
|
+
"extends": "weasel",
|
|
4
|
+
"pins": {
|
|
5
|
+
"radius-md": {
|
|
6
|
+
"value": "6px",
|
|
7
|
+
"type": "dimension"
|
|
8
|
+
},
|
|
9
|
+
"glass-blur": {
|
|
10
|
+
"value": "12px",
|
|
11
|
+
"type": "dimension"
|
|
12
|
+
},
|
|
13
|
+
"font-weight-light": {
|
|
14
|
+
"value": 300,
|
|
15
|
+
"type": "fontWeight"
|
|
16
|
+
},
|
|
17
|
+
"font-weight-medium": {
|
|
18
|
+
"value": 500,
|
|
19
|
+
"type": "fontWeight"
|
|
20
|
+
},
|
|
21
|
+
"font-weight-bold": {
|
|
22
|
+
"value": 700,
|
|
23
|
+
"type": "fontWeight"
|
|
24
|
+
},
|
|
25
|
+
"surface": {
|
|
26
|
+
"by": "mode",
|
|
27
|
+
"dark": {
|
|
28
|
+
"value": "#0a0a14",
|
|
29
|
+
"type": "color"
|
|
30
|
+
},
|
|
31
|
+
"light": {
|
|
32
|
+
"value": "#fafaf7",
|
|
33
|
+
"type": "color"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"surface-raised": {
|
|
37
|
+
"by": "mode",
|
|
38
|
+
"dark": {
|
|
39
|
+
"value": "rgba(20, 20, 36, 0.55)",
|
|
40
|
+
"type": "color"
|
|
41
|
+
},
|
|
42
|
+
"light": {
|
|
43
|
+
"value": "#ffffff",
|
|
44
|
+
"type": "color"
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"surface-sunken": {
|
|
48
|
+
"by": "mode",
|
|
49
|
+
"dark": {
|
|
50
|
+
"value": "#02020a",
|
|
51
|
+
"type": "color"
|
|
52
|
+
},
|
|
53
|
+
"light": {
|
|
54
|
+
"value": "#f0ebe2",
|
|
55
|
+
"type": "color"
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"border": {
|
|
59
|
+
"by": "mode",
|
|
60
|
+
"dark": {
|
|
61
|
+
"value": "rgba(255, 255, 255, 0.14)",
|
|
62
|
+
"type": "color"
|
|
63
|
+
},
|
|
64
|
+
"light": {
|
|
65
|
+
"value": "#c9bba5",
|
|
66
|
+
"type": "color"
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"line-subtle": {
|
|
70
|
+
"by": "mode",
|
|
71
|
+
"dark": {
|
|
72
|
+
"value": "rgba(255, 255, 255, 0.08)",
|
|
73
|
+
"type": "color"
|
|
74
|
+
},
|
|
75
|
+
"light": {
|
|
76
|
+
"value": "rgba(0, 0, 0, 0.08)",
|
|
77
|
+
"type": "color"
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
"fg": {
|
|
81
|
+
"by": "mode",
|
|
82
|
+
"dark": {
|
|
83
|
+
"value": "#e8e8f0",
|
|
84
|
+
"type": "color"
|
|
85
|
+
},
|
|
86
|
+
"light": {
|
|
87
|
+
"value": "#2a2018",
|
|
88
|
+
"type": "color"
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
"fg-muted": {
|
|
92
|
+
"by": "mode",
|
|
93
|
+
"dark": {
|
|
94
|
+
"value": "#a8a8c0",
|
|
95
|
+
"type": "color"
|
|
96
|
+
},
|
|
97
|
+
"light": {
|
|
98
|
+
"value": "#6b5c4a",
|
|
99
|
+
"type": "color"
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
"fg-subtle": {
|
|
103
|
+
"by": "mode",
|
|
104
|
+
"dark": {
|
|
105
|
+
"value": "#5a5a78",
|
|
106
|
+
"type": "color"
|
|
107
|
+
},
|
|
108
|
+
"light": {
|
|
109
|
+
"value": "#a89b85",
|
|
110
|
+
"type": "color"
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
"accent": {
|
|
114
|
+
"by": "mode",
|
|
115
|
+
"dark": {
|
|
116
|
+
"value": "#b08adb",
|
|
117
|
+
"type": "color"
|
|
118
|
+
},
|
|
119
|
+
"light": {
|
|
120
|
+
"value": "#a86f3c",
|
|
121
|
+
"type": "color"
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
"accent-hover": {
|
|
125
|
+
"by": "mode",
|
|
126
|
+
"dark": {
|
|
127
|
+
"value": "#c6a3ee",
|
|
128
|
+
"type": "color"
|
|
129
|
+
},
|
|
130
|
+
"light": {
|
|
131
|
+
"value": "#c2854f",
|
|
132
|
+
"type": "color"
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
"focus-ring": {
|
|
136
|
+
"by": "mode",
|
|
137
|
+
"dark": {
|
|
138
|
+
"value": "rgba(176, 138, 219, 0.45)",
|
|
139
|
+
"type": "color"
|
|
140
|
+
},
|
|
141
|
+
"light": {
|
|
142
|
+
"value": "rgba(168, 111, 60, 0.35)",
|
|
143
|
+
"type": "color"
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
"shadow": {
|
|
147
|
+
"by": "mode",
|
|
148
|
+
"dark": {
|
|
149
|
+
"value": "rgba(0, 0, 0, 0.7)",
|
|
150
|
+
"type": "color"
|
|
151
|
+
},
|
|
152
|
+
"light": {
|
|
153
|
+
"value": "rgba(80, 60, 40, 0.18)",
|
|
154
|
+
"type": "color"
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
"border-strong": {
|
|
158
|
+
"by": "mode",
|
|
159
|
+
"dark": {
|
|
160
|
+
"value": "rgba(255, 255, 255, 0.36)",
|
|
161
|
+
"type": "color"
|
|
162
|
+
},
|
|
163
|
+
"light": {
|
|
164
|
+
"value": "#9e8b71",
|
|
165
|
+
"type": "color"
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
"backdrop": {
|
|
169
|
+
"by": "mode",
|
|
170
|
+
"dark": {
|
|
171
|
+
"value": "radial-gradient(ellipse 60% 80% at 18% 30%, rgba(150, 80, 200, 0.22), transparent 70%), radial-gradient(ellipse 55% 90% at 78% 70%, rgba(220, 100, 140, 0.18), transparent 65%), radial-gradient(ellipse 50% 70% at 50% 50%, rgba(60, 110, 200, 0.18), transparent 75%), radial-gradient(ellipse at center, #0a0a18 0%, #02020a 100%)",
|
|
172
|
+
"type": "gradient"
|
|
173
|
+
},
|
|
174
|
+
"light": {
|
|
175
|
+
"value": "none",
|
|
176
|
+
"type": "gradient"
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import { defineTheme, type Theme, type ThemeInput, weaselTheme } from '@weasel-js/theme';
|
|
2
|
+
import definition from './interstellar.theme.json' with { type: 'json' };
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* labkit's theme: a cosmic dark and a warm parchment light, extending the
|
|
6
|
-
* built-in weasel theme.
|
|
7
|
-
*
|
|
6
|
+
* built-in weasel theme. A pins-only theme definition: nothing in it is
|
|
7
|
+
* derived, so it loads without the engine.
|
|
8
8
|
*/
|
|
9
|
-
export const interstellarTheme: Theme =
|
|
9
|
+
export const interstellarTheme: Theme = defineTheme({
|
|
10
|
+
...definition,
|
|
11
|
+
extends: weaselTheme,
|
|
12
|
+
} as ThemeInput);
|
package/src/tools/labTool.ts
CHANGED
|
@@ -2,10 +2,10 @@ import type { InstrumentList } from '../instrument/types';
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* The tool the lab's slot holds, once its default is applied: a lab with any
|
|
5
|
-
* annotating instrument starts in `
|
|
6
|
-
*
|
|
5
|
+
* annotating instrument starts in `pointer`, so a first click reaches the
|
|
6
|
+
* instrument rather than the marks over it.
|
|
7
7
|
*/
|
|
8
8
|
export function resolveLabTool(slot: string | null, instruments: InstrumentList): string | null {
|
|
9
9
|
if (slot !== null) return slot;
|
|
10
|
-
return instruments.some((i) => i.annotations != null) ? '
|
|
10
|
+
return instruments.some((i) => i.annotations != null) ? 'pointer' : null;
|
|
11
11
|
}
|
|
@@ -55,7 +55,16 @@ const plain = defineInstrument<Record<string, never>, Record<string, never>>({
|
|
|
55
55
|
describe('an instrument that declares annotations', () => {
|
|
56
56
|
it('gets a drawing palette', () => {
|
|
57
57
|
render(<Lab instruments={[annotating]} defaultInstrument="Annotating" />);
|
|
58
|
-
for (const label of [
|
|
58
|
+
for (const label of [
|
|
59
|
+
'Interact',
|
|
60
|
+
'Select',
|
|
61
|
+
'Freehand',
|
|
62
|
+
'Line',
|
|
63
|
+
'Arrow',
|
|
64
|
+
'Rectangle',
|
|
65
|
+
'Ellipse',
|
|
66
|
+
'Text',
|
|
67
|
+
]) {
|
|
59
68
|
expect(screen.getByRole('button', { name: label })).toBeTruthy();
|
|
60
69
|
}
|
|
61
70
|
});
|
|
@@ -65,9 +74,9 @@ describe('an instrument that declares annotations', () => {
|
|
|
65
74
|
expect(screen.getByTestId('pane').dataset.marks).toBe('0');
|
|
66
75
|
});
|
|
67
76
|
|
|
68
|
-
it('starts in
|
|
77
|
+
it('starts in interact, so a first click reaches the instrument', () => {
|
|
69
78
|
render(<Lab instruments={[annotating]} defaultInstrument="Annotating" />);
|
|
70
|
-
expect(screen.getByRole('button', { name: '
|
|
79
|
+
expect(screen.getByRole('button', { name: 'Interact' }).getAttribute('aria-current')).toBe(
|
|
71
80
|
'true',
|
|
72
81
|
);
|
|
73
82
|
});
|
|
@@ -106,7 +115,7 @@ describe('the annotation tool', () => {
|
|
|
106
115
|
act(() => labRef?.addTrial('Annotating'));
|
|
107
116
|
const panes = screen.getAllByTestId('pane');
|
|
108
117
|
expect(panes).toHaveLength(2);
|
|
109
|
-
for (const p of panes) expect(p.dataset.tool).toBe('
|
|
118
|
+
for (const p of panes) expect(p.dataset.tool).toBe('pointer');
|
|
110
119
|
|
|
111
120
|
act(() => {
|
|
112
121
|
fireEvent.click(screen.getByRole('button', { name: 'Rectangle' }));
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { render, screen } from '@testing-library/react';
|
|
2
2
|
import userEvent from '@testing-library/user-event';
|
|
3
3
|
import { beforeAll, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { auto } from '../config/auto';
|
|
5
|
+
import { f } from '../config/builder';
|
|
4
6
|
import { defineInstrument } from '../instrument/defineInstrument';
|
|
7
|
+
import type { Instrument } from '../instrument/types';
|
|
5
8
|
import { Lab } from '../lab/Lab';
|
|
6
9
|
|
|
7
10
|
interface St {
|
|
@@ -74,6 +77,39 @@ describe('an instrument that draws', () => {
|
|
|
74
77
|
});
|
|
75
78
|
});
|
|
76
79
|
|
|
80
|
+
describe('an auto config path', () => {
|
|
81
|
+
it('draws with the resolved value, which is what the panel shows too', async () => {
|
|
82
|
+
const draw = vi.fn();
|
|
83
|
+
const instrument = defineInstrument({
|
|
84
|
+
name: 'Auto',
|
|
85
|
+
config: f.schema({
|
|
86
|
+
gap: f
|
|
87
|
+
.number(12)
|
|
88
|
+
.auto(() => 18)
|
|
89
|
+
.range(0, 48)
|
|
90
|
+
.initial(auto),
|
|
91
|
+
}),
|
|
92
|
+
initialState: () => ({ n: 0 }),
|
|
93
|
+
render: () => null,
|
|
94
|
+
canvas: { layers: [{ id: 'main', draw }] },
|
|
95
|
+
});
|
|
96
|
+
const { container } = render(
|
|
97
|
+
<Lab instruments={[instrument as Instrument]} defaultInstrument="Auto" />,
|
|
98
|
+
);
|
|
99
|
+
await new Promise((r) => requestAnimationFrame(() => r(null)));
|
|
100
|
+
|
|
101
|
+
expect(draw).toHaveBeenCalledWith(
|
|
102
|
+
expect.anything(),
|
|
103
|
+
expect.objectContaining({ config: { gap: 18 } }),
|
|
104
|
+
);
|
|
105
|
+
// The panel is handed the raw config, but an auto row holds what the
|
|
106
|
+
// resolver decided, so pinning it keeps the number the instrument drew.
|
|
107
|
+
// The row hides its control while auto, which takes it out of the
|
|
108
|
+
// accessibility tree — so this reads the element rather than the role.
|
|
109
|
+
expect(container.querySelector('input[type=range]')).toHaveValue('18');
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
77
113
|
describe('LabProps.instruments', () => {
|
|
78
114
|
it('accepts a typed instrument with no cast', () => {
|
|
79
115
|
// The assignment is the assertion: this file fails to compile if
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { fireEvent, render, screen } from '@testing-library/react';
|
|
2
2
|
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { auto } from '../config/auto';
|
|
3
4
|
import { f } from '../config/builder';
|
|
4
5
|
import type { ConfigRule, ControlRenderer } from '../config/types';
|
|
5
6
|
import { defineInstrument } from '../instrument/defineInstrument';
|
|
@@ -132,14 +133,18 @@ describe('a nested config value, end to end', () => {
|
|
|
132
133
|
const { instrument } = nestedInstrument();
|
|
133
134
|
render(<Lab instruments={[instrument]} defaultInstrument="Nested" />);
|
|
134
135
|
expect(screen.getByText('20')).toBeInTheDocument();
|
|
135
|
-
fireEvent.change(screen.getByLabelText('Cell size'), {
|
|
136
|
+
fireEvent.change(screen.getByLabelText('Cell size'), {
|
|
137
|
+
target: { value: '40' },
|
|
138
|
+
});
|
|
136
139
|
expect(screen.getByText('40')).toBeInTheDocument();
|
|
137
140
|
});
|
|
138
141
|
|
|
139
142
|
it('hands onConfigChange the whole tree, with the sibling branch intact', () => {
|
|
140
143
|
const { instrument, seen } = nestedInstrument();
|
|
141
144
|
render(<Lab instruments={[instrument]} defaultInstrument="Nested" />);
|
|
142
|
-
fireEvent.change(screen.getByLabelText('Cell size'), {
|
|
145
|
+
fireEvent.change(screen.getByLabelText('Cell size'), {
|
|
146
|
+
target: { value: '40' },
|
|
147
|
+
});
|
|
143
148
|
expect(seen.at(-1)?.config).toEqual({
|
|
144
149
|
showGrid: true,
|
|
145
150
|
grid: { size: 40, color: '#ffffff' },
|
|
@@ -150,3 +155,71 @@ describe('a nested config value, end to end', () => {
|
|
|
150
155
|
});
|
|
151
156
|
});
|
|
152
157
|
});
|
|
158
|
+
|
|
159
|
+
describe('a config written as auto', () => {
|
|
160
|
+
const unpinnable = () => {
|
|
161
|
+
const seen: { config: unknown; prev: unknown }[] = [];
|
|
162
|
+
const instrument = defineInstrument({
|
|
163
|
+
name: 'Unpinnable',
|
|
164
|
+
config: f.schema({
|
|
165
|
+
width: f.number(432),
|
|
166
|
+
gap: f.number(12).auto((c) => (c.width as number) / 24),
|
|
167
|
+
}),
|
|
168
|
+
initialState: () => ({ gap: 12 }),
|
|
169
|
+
onConfigChange: (config, prev, state) => {
|
|
170
|
+
seen.push({ config, prev });
|
|
171
|
+
return { ...state, gap: (config as { gap: number }).gap };
|
|
172
|
+
},
|
|
173
|
+
chrome: [
|
|
174
|
+
{
|
|
175
|
+
id: 'unpin-gap',
|
|
176
|
+
region: 'toolbar' as const,
|
|
177
|
+
render: (ctx) => (
|
|
178
|
+
<button type="button" onClick={() => ctx.setConfig('gap', auto)}>
|
|
179
|
+
unpin gap
|
|
180
|
+
</button>
|
|
181
|
+
),
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
render: (ctx) => <output>{String((ctx.state as { gap: number }).gap)}</output>,
|
|
185
|
+
});
|
|
186
|
+
return { instrument, seen };
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
it('hands onConfigChange the resolved config, never the sentinel', () => {
|
|
190
|
+
const { instrument, seen } = unpinnable();
|
|
191
|
+
render(<Lab instruments={[instrument]} defaultInstrument="Unpinnable" />);
|
|
192
|
+
fireEvent.click(screen.getByText('unpin gap'));
|
|
193
|
+
expect(seen.at(-1)?.config).toEqual({ width: 432, gap: 18 });
|
|
194
|
+
expect(seen.at(-1)?.prev).toEqual({ width: 432, gap: 12 });
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('keeps the state that write derived free of the sentinel', () => {
|
|
198
|
+
const { instrument } = unpinnable();
|
|
199
|
+
render(<Lab instruments={[instrument]} defaultInstrument="Unpinnable" />);
|
|
200
|
+
fireEvent.click(screen.getByText('unpin gap'));
|
|
201
|
+
expect(screen.getByText('18')).toBeInTheDocument();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("shows the trial's unpinned path in the settings panel", () => {
|
|
205
|
+
const instrument = defineInstrument({
|
|
206
|
+
name: 'Ghosted',
|
|
207
|
+
config: f.schema({
|
|
208
|
+
width: f.number(432),
|
|
209
|
+
gap: f
|
|
210
|
+
.number(12)
|
|
211
|
+
.initial(auto)
|
|
212
|
+
.auto((c) => (c.width as number) / 24)
|
|
213
|
+
.range(0, 48),
|
|
214
|
+
}),
|
|
215
|
+
initialState: () => ({}),
|
|
216
|
+
render: () => null,
|
|
217
|
+
});
|
|
218
|
+
const { container } = render(<Lab instruments={[instrument]} defaultInstrument="Ghosted" />);
|
|
219
|
+
expect(screen.getByRole('button', { name: /Gap/ })).toHaveAttribute('aria-pressed', 'false');
|
|
220
|
+
expect(screen.getByRole('button', { name: /Width/ })).toHaveAttribute('aria-pressed', 'true');
|
|
221
|
+
// 432 / 24. An auto row holds what the resolver decided rather than
|
|
222
|
+
// the pinned 12 underneath it, and un-pinning writes back what it drew.
|
|
223
|
+
expect((container.querySelector('input[type=range]') as HTMLInputElement).value).toBe('18');
|
|
224
|
+
});
|
|
225
|
+
});
|
package/src/trial/Trial.less
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from '@
|
|
1
|
+
import type { Meta, StoryObj } from '@weasel-js/forge';
|
|
2
2
|
import type { Instrument } from '../instrument/types';
|
|
3
3
|
import { LabContext, type LabContextValue } from '../lab/LabContext';
|
|
4
4
|
import { LabStoreContext } from '../state/context';
|
|
@@ -35,6 +35,9 @@ function Harness() {
|
|
|
35
35
|
closeTrial: noop,
|
|
36
36
|
resetTrial: noop,
|
|
37
37
|
reorderTrials: noop,
|
|
38
|
+
swapTrial: noop,
|
|
39
|
+
focusedTrialId: record.id,
|
|
40
|
+
focusTrial: noop,
|
|
38
41
|
savedSnapshots: [],
|
|
39
42
|
saveSnapshot: noop,
|
|
40
43
|
loadSnapshot: noop,
|
package/src/trial/Trial.test.tsx
CHANGED
package/src/trial/Trial.tsx
CHANGED
|
@@ -11,6 +11,8 @@ import { fitStage, Stage } from '../canvas/Stage';
|
|
|
11
11
|
import type { CanvasLayerDescriptor } from '../canvas/useLayerScheduler';
|
|
12
12
|
import { applyCamera, type ViewportSize } from '../canvas/worldSpec';
|
|
13
13
|
import type { TrialContribution } from '../chrome/types';
|
|
14
|
+
import { useConfigSchema } from '../config/useConfigSchema';
|
|
15
|
+
import { useResolvedConfig } from '../config/useResolvedConfig';
|
|
14
16
|
import { DragOverlay, useDragDrop } from '../dragdrop/DragDropRuntime';
|
|
15
17
|
import { Palette } from '../dragdrop/Palette';
|
|
16
18
|
import type {
|
|
@@ -143,6 +145,11 @@ function TrialRuntime({
|
|
|
143
145
|
const setLabTool = useStore(store, (s) => s.setLabTool);
|
|
144
146
|
const setTrialTool = useStore(store, (s) => s.setTrialTool);
|
|
145
147
|
|
|
148
|
+
// Everything below reads `config`; `record.config` is the panel's view, and
|
|
149
|
+
// reaches it through `record` on `TrialChrome`.
|
|
150
|
+
const schema = useConfigSchema(instrument);
|
|
151
|
+
const { config } = useResolvedConfig(schema, record.config, record.auto);
|
|
152
|
+
|
|
146
153
|
const busRef = useRef<EventBus | null>(null);
|
|
147
154
|
if (busRef.current === null) busRef.current = createEventBus();
|
|
148
155
|
const bus = busRef.current;
|
|
@@ -193,15 +200,15 @@ function TrialRuntime({
|
|
|
193
200
|
const targetsRef = useRef<() => readonly AnnotationTargetInfo[]>(() => []);
|
|
194
201
|
targetsRef.current = () =>
|
|
195
202
|
annotationsCap
|
|
196
|
-
? annotationsCap.targets(record.state,
|
|
203
|
+
? annotationsCap.targets(record.state, config, { id: record.id, view: record.view })
|
|
197
204
|
: [];
|
|
198
205
|
|
|
199
206
|
// One store for the trial's lifetime.
|
|
200
207
|
const annotationsRef = useRef<ReturnType<typeof annotationsFromJSON> | null>(null);
|
|
201
208
|
// Read through refs for the same reason `targets` is: the store is built
|
|
202
209
|
// once, and an export must draw against the config the trial holds now.
|
|
203
|
-
const configRef = useRef<unknown>(
|
|
204
|
-
configRef.current =
|
|
210
|
+
const configRef = useRef<unknown>(config);
|
|
211
|
+
configRef.current = config;
|
|
205
212
|
// The instrument can be swapped under a live trial, so the capability is read at use too.
|
|
206
213
|
const capRef = useRef(annotationsCap);
|
|
207
214
|
capRef.current = annotationsCap;
|
|
@@ -289,14 +296,14 @@ function TrialRuntime({
|
|
|
289
296
|
// this — with a runner that yields nothing and is never started.
|
|
290
297
|
const job = useJob({
|
|
291
298
|
capability: jobCap ?? { run: async function* () {}, onItem: (_i, st) => st },
|
|
292
|
-
config
|
|
299
|
+
config,
|
|
293
300
|
state: record.state,
|
|
294
301
|
setState: (next) => updateTrialState(record.id, next as never),
|
|
295
302
|
});
|
|
296
303
|
|
|
297
304
|
const renderCtx: RenderContext<unknown, unknown> = {
|
|
298
305
|
state: record.state,
|
|
299
|
-
config
|
|
306
|
+
config,
|
|
300
307
|
setState: (next) => {
|
|
301
308
|
snapshotIfNeeded('state.change');
|
|
302
309
|
updateTrialState(record.id, next);
|
|
@@ -365,8 +372,8 @@ function TrialRuntime({
|
|
|
365
372
|
const loupeCap = useMemo(() => {
|
|
366
373
|
const declared = instrument.loupe;
|
|
367
374
|
if (declared == null) return null;
|
|
368
|
-
return resolveLoupe(typeof declared === 'function' ? declared(
|
|
369
|
-
}, [instrument.loupe,
|
|
375
|
+
return resolveLoupe(typeof declared === 'function' ? declared(config) : declared);
|
|
376
|
+
}, [instrument.loupe, config]);
|
|
370
377
|
const loupeBindings: LoupeBindings | undefined = loupeCap
|
|
371
378
|
? { on: loupeOn, toggle: () => setLoupeOn((v) => !v) }
|
|
372
379
|
: undefined;
|
|
@@ -384,10 +391,10 @@ function TrialRuntime({
|
|
|
384
391
|
// Camera applied here, so a layer draws in world coordinates. `zoom`
|
|
385
392
|
// stays in the args for line widths, which must not scale with it.
|
|
386
393
|
applyCamera(ctx, view, frame);
|
|
387
|
-
layer.draw(ctx, { state: record.state, config
|
|
394
|
+
layer.draw(ctx, { state: record.state, config, zoom: view.zoom });
|
|
388
395
|
},
|
|
389
396
|
}));
|
|
390
|
-
}, [instrument.canvas, record.state,
|
|
397
|
+
}, [instrument.canvas, record.state, config, layerVisibility, layerOrder]);
|
|
391
398
|
|
|
392
399
|
const layerDescriptors: LayerDescriptor[] = useMemo(() => {
|
|
393
400
|
if (!instrument.layers) return [];
|
|
@@ -400,7 +407,7 @@ function TrialRuntime({
|
|
|
400
407
|
view: view2d ?? DEFAULT_VIEW,
|
|
401
408
|
worldSpec: instrument.canvas?.worldSpec,
|
|
402
409
|
state: record.state,
|
|
403
|
-
config
|
|
410
|
+
config,
|
|
404
411
|
setState: (next) => {
|
|
405
412
|
snapshotIfNeeded('canvas.itemAdded');
|
|
406
413
|
updateTrialState(record.id, next as never);
|
|
@@ -414,8 +421,8 @@ function TrialRuntime({
|
|
|
414
421
|
const paletteItems: PaletteItem[] = useMemo(() => {
|
|
415
422
|
if (!instrument.dragDrop) return [];
|
|
416
423
|
const p = instrument.dragDrop.palette;
|
|
417
|
-
return typeof p === 'function' ? p(record.state,
|
|
418
|
-
}, [instrument.dragDrop, record.state,
|
|
424
|
+
return typeof p === 'function' ? p(record.state, config) : p;
|
|
425
|
+
}, [instrument.dragDrop, record.state, config]);
|
|
419
426
|
|
|
420
427
|
const layersWithFeedback: CanvasLayerDescriptor[] = useMemo(() => {
|
|
421
428
|
if (!dragDropResult.drag?.feedback) return canvasLayers;
|
|
@@ -449,7 +456,7 @@ function TrialRuntime({
|
|
|
449
456
|
capability={loupeCap}
|
|
450
457
|
enabled={loupeOn}
|
|
451
458
|
state={record.state}
|
|
452
|
-
config={
|
|
459
|
+
config={config}
|
|
453
460
|
view={view2d ?? DEFAULT_VIEW}
|
|
454
461
|
worldSpec={instrument.canvas?.worldSpec}
|
|
455
462
|
hostRef={loupeHostRef}
|
|
@@ -553,7 +560,7 @@ function TrialRuntime({
|
|
|
553
560
|
<AnnotationTargets
|
|
554
561
|
capability={annotationsCap}
|
|
555
562
|
state={record.state}
|
|
556
|
-
config={
|
|
563
|
+
config={config}
|
|
557
564
|
trial={{ id: record.id, view: record.view }}
|
|
558
565
|
annotations={annotations}
|
|
559
566
|
activeToolId={labTool}
|
|
@@ -9,8 +9,10 @@ import { TitleBarRegion } from '../chrome/regions/TitleBarRegion';
|
|
|
9
9
|
import { ToolbarRegion } from '../chrome/regions/ToolbarRegion';
|
|
10
10
|
import { ViewportRegion } from '../chrome/regions/ViewportRegion';
|
|
11
11
|
import type { TrialChromeContext, TrialContribution, TrialRegion } from '../chrome/types';
|
|
12
|
-
import {
|
|
12
|
+
import { applyConfigWrite, resolveAutoConfig } from '../config/autoConfig';
|
|
13
|
+
import { hasConfigPath, schemaNodeAtPath } from '../config/path';
|
|
13
14
|
import { useConfigSchema } from '../config/useConfigSchema';
|
|
15
|
+
import { useResolvedConfig } from '../config/useResolvedConfig';
|
|
14
16
|
import type { Instrument } from '../instrument/types';
|
|
15
17
|
import type { JobHandle } from '../job/types';
|
|
16
18
|
import { useLabContext } from '../lab/LabContext';
|
|
@@ -110,6 +112,13 @@ export function TrialChrome({
|
|
|
110
112
|
const view2d = as2DView(record.view);
|
|
111
113
|
|
|
112
114
|
const configSchema = useConfigSchema(instrument);
|
|
115
|
+
// What the instrument is looking at right now. The panel reads `record.config`
|
|
116
|
+
// — the pinned values — but anything instrument-facing gets the resolved one.
|
|
117
|
+
const { config: resolvedConfig, auto } = useResolvedConfig(
|
|
118
|
+
configSchema,
|
|
119
|
+
record.config,
|
|
120
|
+
record.auto,
|
|
121
|
+
);
|
|
113
122
|
|
|
114
123
|
const title = record.title ?? instrument.title ?? instrument.name;
|
|
115
124
|
const collapsedSections = record.collapsedSections ?? NO_SECTIONS;
|
|
@@ -136,6 +145,7 @@ export function TrialChrome({
|
|
|
136
145
|
configSchema,
|
|
137
146
|
configFields: instrument.config ? [] : (instrument.configSchema?.() ?? []),
|
|
138
147
|
config: record.config,
|
|
148
|
+
auto,
|
|
139
149
|
setConfig: (path, value) => {
|
|
140
150
|
const prevConfig = record.config as Record<string, unknown>;
|
|
141
151
|
if (
|
|
@@ -149,8 +159,9 @@ export function TrialChrome({
|
|
|
149
159
|
}
|
|
150
160
|
updateTrialConfig(trialId, path, value);
|
|
151
161
|
if (instrument.onConfigChange) {
|
|
152
|
-
const
|
|
153
|
-
const
|
|
162
|
+
const next = applyConfigWrite(prevConfig, record.auto ?? [], path, value);
|
|
163
|
+
const nextConfig = resolveAutoConfig(configSchema, next.config, new Set(next.autoPaths));
|
|
164
|
+
const nextState = instrument.onConfigChange(nextConfig, resolvedConfig, record.state);
|
|
154
165
|
updateTrialState(trialId, nextState as never);
|
|
155
166
|
}
|
|
156
167
|
},
|
|
@@ -184,6 +195,8 @@ export function TrialChrome({
|
|
|
184
195
|
activeToolId,
|
|
185
196
|
setActiveTool,
|
|
186
197
|
configSchema,
|
|
198
|
+
resolvedConfig,
|
|
199
|
+
auto,
|
|
187
200
|
undockedIds,
|
|
188
201
|
undockPanelAction,
|
|
189
202
|
dockPanelAction,
|
|
@@ -226,6 +239,7 @@ export function TrialChrome({
|
|
|
226
239
|
<section
|
|
227
240
|
className="lk-trial"
|
|
228
241
|
data-trial-id={trialId}
|
|
242
|
+
data-focused={lab.trials.length > 1 && lab.focusedTrialId === trialId ? 'true' : undefined}
|
|
229
243
|
aria-label={`Trial ${title}`}
|
|
230
244
|
tabIndex={-1}
|
|
231
245
|
onKeyDown={handleKeyDown}
|