@weasel-js/labkit 1.6.0 → 1.7.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/dist/_dts/{CanvasStackContext-DQgcH1ny.d.ts → CanvasStackContext-D2jnIwfk.d.ts} +1 -1
- package/dist/_dts/FloatingPanel-7j0IcXwz.d.ts +30 -0
- package/dist/_dts/{LayerList.d-2c9yup5Y.d.ts → LayerList.d-DiuEArKq.d.ts} +4 -4
- package/dist/_dts/PrefsRow.d-TOZ7BgD9.d.ts +79 -0
- package/dist/_dts/PropertyPanel.d-D-1paCbE.d.ts +141 -0
- package/dist/_dts/{Select.d-BchGMC2Y.d.ts → Select.d-Bensr_MT.d.ts} +15 -6
- package/dist/_dts/Stage-CCWfQQWM.d.ts +202 -0
- package/dist/_dts/index-B2Yj5aA0.d.ts +274 -0
- package/dist/_dts/{index-B0--8PEM.d.ts → index-CsYqUHD3.d.ts} +12 -13
- package/dist/_dts/{types-B7y8hX1D.d.ts → types-CRPSGFAp.d.ts} +33 -5
- package/dist/_dts/{frac-DTRq7Xpb.d.ts → types-CSm9jgol.d.ts} +700 -650
- package/dist/_dts/{useTrialState-CP4jfuKl.d.ts → useTrialState-CTZPgeZ7.d.ts} +2 -3
- package/dist/canvas/index.d.ts +40 -8
- package/dist/canvas/index.js +5 -3
- package/dist/chrome/index.d.ts +5 -6
- package/dist/chrome/index.js +9 -7
- package/dist/chunk-24W5VEJW.js +18 -0
- package/dist/chunk-24W5VEJW.js.map +1 -0
- package/dist/chunk-275JRRVD.js +728 -0
- package/dist/chunk-275JRRVD.js.map +1 -0
- package/dist/chunk-3RY6WL5T.js +38 -0
- package/dist/chunk-3RY6WL5T.js.map +1 -0
- package/dist/chunk-4LJSC7MB.js +143 -0
- package/dist/chunk-4LJSC7MB.js.map +1 -0
- package/dist/{chunk-3FI7Y4R4.js → chunk-7BO7HNGF.js} +5 -5
- package/dist/{chunk-3FI7Y4R4.js.map → chunk-7BO7HNGF.js.map} +1 -1
- package/dist/chunk-AWQG7FCC.js +19 -0
- package/dist/chunk-AWQG7FCC.js.map +1 -0
- package/dist/chunk-CZZPXUOU.js +3 -0
- package/dist/{chunk-SBXNQW4G.js.map → chunk-CZZPXUOU.js.map} +1 -1
- package/dist/{chunk-ZORVNOKX.js → chunk-FMK7UQ6V.js} +24 -145
- package/dist/chunk-FMK7UQ6V.js.map +1 -0
- package/dist/{chunk-BVL7Y574.js → chunk-FZ7YFQN7.js} +26 -10
- package/dist/chunk-FZ7YFQN7.js.map +1 -0
- package/dist/{chunk-USO6TERP.js → chunk-GOZLNM4L.js} +11 -9
- package/dist/chunk-GOZLNM4L.js.map +1 -0
- package/dist/{chunk-H5VIV7MQ.js → chunk-I5DUQ5HM.js} +88 -82
- package/dist/chunk-I5DUQ5HM.js.map +1 -0
- package/dist/chunk-J7OWWTCW.js +143 -0
- package/dist/chunk-J7OWWTCW.js.map +1 -0
- package/dist/{chunk-H6ZAOWNE.js → chunk-JVBYV6TL.js} +3 -3
- package/dist/{chunk-H6ZAOWNE.js.map → chunk-JVBYV6TL.js.map} +1 -1
- package/dist/{chunk-VQ47KWD5.js → chunk-KMJ7RWD3.js} +18 -7
- package/dist/chunk-KMJ7RWD3.js.map +1 -0
- package/dist/{chunk-ERFMC6WU.js → chunk-M7HP3FTK.js} +20 -11
- package/dist/chunk-M7HP3FTK.js.map +1 -0
- package/dist/chunk-MIM5R4ZT.js +14972 -0
- package/dist/chunk-MIM5R4ZT.js.map +1 -0
- package/dist/{chunk-HFRE4QQV.js → chunk-T5IBSPAM.js} +96 -32
- package/dist/chunk-T5IBSPAM.js.map +1 -0
- package/dist/chunk-UNAZSLWE.js +294 -0
- package/dist/chunk-UNAZSLWE.js.map +1 -0
- package/dist/{chunk-RS3HRQWU.js → chunk-WPLAANH4.js} +4 -3
- package/dist/chunk-WPLAANH4.js.map +1 -0
- package/dist/{chunk-AWH3SRPW.js → chunk-Y23G2VZL.js} +5 -37
- package/dist/chunk-Y23G2VZL.js.map +1 -0
- package/dist/{chunk-MOLIUNVH.js → chunk-YT75VYRD.js} +4 -4
- package/dist/{chunk-MOLIUNVH.js.map → chunk-YT75VYRD.js.map} +1 -1
- package/dist/chunk-ZIFPHIWZ.js +302 -0
- package/dist/chunk-ZIFPHIWZ.js.map +1 -0
- package/dist/config/index.d.ts +48 -40
- package/dist/config/index.js +4 -5
- package/dist/controls/index.d.ts +91 -7
- package/dist/controls/index.js +5 -4
- package/dist/dragdrop/index.d.ts +9 -7
- package/dist/dragdrop/index.js +3 -2
- package/dist/index.d.ts +294 -82
- package/dist/index.js +425 -402
- package/dist/index.js.map +1 -1
- package/dist/layers/index.d.ts +4 -5
- package/dist/layers/index.js +2 -1
- package/dist/loupe/index.d.ts +20 -17
- package/dist/loupe/index.js +5 -3
- package/dist/overview/index.d.ts +56 -0
- package/dist/overview/index.js +289 -0
- package/dist/overview/index.js.map +1 -0
- package/dist/passthrough/weasel-ui.d.ts +239 -69
- package/dist/passthrough/weasel-ui.js +3 -2
- package/dist/primitives/index.d.ts +5 -204
- package/dist/primitives/index.js +6 -4
- package/dist/state/index.d.ts +8 -8
- package/dist/state/index.js +4 -5
- package/dist/state/index.js.map +1 -1
- package/dist/styles.css +152 -82
- package/dist/undo/index.d.ts +4 -4
- package/package.json +17 -12
- package/src/annotations/AnnotationOverlay.tsx +2 -2
- package/src/annotations/Annotations.less +0 -10
- package/src/annotations/MarkList.tsx +4 -8
- package/src/annotations/index.ts +1 -0
- package/src/annotations/paint.test.ts +3 -14
- package/src/annotations/paint.ts +3 -19
- package/src/annotations/store.test.ts +26 -0
- package/src/annotations/store.ts +25 -6
- package/src/annotations/svgNodes.test.ts +6 -9
- package/src/annotations/svgNodes.ts +3 -3
- package/src/annotations/toolMap.test.ts +37 -2
- package/src/annotations/toolMap.ts +13 -0
- package/src/annotations/types.ts +27 -0
- package/src/canvas/AGENTS.md +9 -10
- package/src/canvas/CameraInput.test.tsx +136 -0
- package/src/canvas/CameraInput.tsx +322 -0
- package/src/canvas/CanvasStack.tsx +47 -58
- package/src/canvas/LinkedCursor.less +16 -0
- package/src/canvas/LinkedCursor.tsx +54 -0
- package/src/canvas/Stage.test.tsx +33 -4
- package/src/canvas/Stage.tsx +47 -37
- package/src/canvas/cameraRegistry.test.ts +55 -0
- package/src/canvas/cameraRegistry.ts +86 -0
- package/src/canvas/cameraView.test.ts +60 -0
- package/src/canvas/cameraView.ts +61 -0
- package/src/canvas/cameraZoom.test.ts +46 -0
- package/src/canvas/cameraZoom.ts +24 -0
- package/src/canvas/index.ts +23 -2
- package/src/canvas/sharedScope.test.tsx +94 -0
- package/src/chrome/ChromeRegions.stories.tsx +1 -0
- package/src/chrome/regions/PaletteRegion.less +0 -7
- package/src/chrome/regions/SidebarRegion.less +12 -2
- package/src/chrome/regions/SidebarRegion.test.tsx +9 -2
- package/src/config/builder.test.ts +2 -1
- package/src/config/builder.ts +15 -8
- package/src/config/entry.test.ts +3 -1
- package/src/config/fromConfigField.test.ts +14 -0
- package/src/config/fromConfigField.ts +19 -4
- package/src/config/index.ts +4 -1
- package/src/config/path.ts +2 -5
- package/src/config/resolve.ts +15 -1
- package/src/config/sectionTree.test.ts +12 -6
- package/src/config/sectionTree.ts +15 -5
- package/src/config/types.ts +34 -9
- package/src/controls/ControlPanel.less +1 -1
- package/src/controls/ControlPanel.stories.tsx +3 -2
- package/src/controls/ControlPanel.test.tsx +76 -13
- package/src/controls/ControlPanel.tsx +80 -11
- package/src/controls/inDialog.test.tsx +18 -0
- package/src/controls/inDialog.tsx +2 -10
- package/src/index.test.ts +1 -1
- package/src/index.ts +23 -5
- package/src/instrument/SineWave.smoke.test.tsx +2 -0
- package/src/instrument/types.ts +10 -1
- package/src/lab/Lab.nebula.test.tsx +11 -2
- package/src/lab/Lab.test.tsx +3 -1
- package/src/lab/Lab.tsx +87 -76
- package/src/lab/{LabFit.stories.less → LabFit.browser.test.less} +1 -1
- package/src/lab/{LabFit.stories.tsx → LabFit.browser.test.tsx} +41 -63
- package/src/lab/LabFullChrome.stories.tsx +1 -0
- package/src/lab/LabHeader.test.tsx +24 -14
- package/src/lab/LabHeader.tsx +11 -12
- package/src/lab/LabShell.less +8 -17
- package/src/lab/LabSwitcher.less +6 -6
- package/src/lab/LabZoom.test.tsx +223 -0
- package/src/lab/LabZoom.tsx +170 -0
- package/src/lab/index.ts +1 -0
- package/src/loupe/AGENTS.md +7 -6
- package/src/loupe/LoupeGestures.tsx +15 -10
- package/src/loupe/TrialLoupe.tsx +5 -5
- package/src/loupe/index.ts +1 -0
- package/src/loupe/loupeActions.ts +6 -2
- package/src/overview/AGENTS.md +38 -0
- package/src/overview/OverviewMarks.test.tsx +100 -0
- package/src/overview/OverviewMarks.tsx +93 -0
- package/src/overview/TrialOverview.less +53 -0
- package/src/overview/TrialOverview.stories.tsx +50 -0
- package/src/overview/TrialOverview.test.tsx +116 -0
- package/src/overview/TrialOverview.tsx +376 -0
- package/src/overview/index.ts +2 -0
- package/src/passthrough/weasel-ui.test.ts +2 -0
- package/src/passthrough/weasel-ui.ts +23 -4
- package/src/primitives/FloatingPanel.test.tsx +11 -0
- package/src/primitives/FloatingPanel.tsx +2 -5
- package/src/primitives/JobProgress.less +1 -1
- package/src/primitives/Legend.stories.tsx +1 -0
- package/src/primitives/Readout.browser.test.tsx +137 -0
- package/src/primitives/Readout.less +12 -0
- package/src/primitives/Readout.stories.tsx +58 -0
- package/src/primitives/Readout.test.tsx +93 -0
- package/src/primitives/Readout.tsx +56 -0
- package/src/primitives/index.ts +2 -0
- package/src/specimen/Specimen.stories.tsx +1 -0
- package/src/specimen/Specimen.tsx +21 -4
- package/src/state/Persistence.browser.test.tsx +47 -0
- package/src/state/labRecords.test.ts +4 -1
- package/src/state/store.test.ts +9 -5
- package/src/state/usePersistedState.test.tsx +1 -1
- package/src/state/useTrialState.test.tsx +1 -1
- package/src/styles.less +3 -0
- package/src/theme/Interstellar.stories.tsx +4 -6
- package/src/theme/base.less +21 -47
- package/src/theme/interstellar.test.ts +1 -1
- package/src/theme/interstellar.theme.json +10 -3
- package/src/tools/labTool.ts +6 -2
- package/src/trial/Trial.annotations.test.tsx +16 -0
- package/src/trial/Trial.less +0 -13
- package/src/trial/Trial.loupe.test.tsx +73 -1
- package/src/trial/Trial.stories.tsx +2 -0
- package/src/trial/Trial.tsx +23 -1
- package/src/trial/TrialChrome.tsx +21 -3
- package/src/trial/trialOps.test.ts +4 -4
- package/dist/_dts/PrefsRow.d-DFpPIeIC.d.ts +0 -39
- package/dist/_dts/inDialog-D5ZKGjiD.d.ts +0 -23
- package/dist/_dts/index-DINmCuLA.d.ts +0 -453
- package/dist/_dts/number.d-OeQYQoUO.d.ts +0 -28
- package/dist/_dts/usePanZoom-Bq73pwFe.d.ts +0 -117
- package/dist/chunk-2AYGEN57.js +0 -32
- package/dist/chunk-2AYGEN57.js.map +0 -1
- package/dist/chunk-AWH3SRPW.js.map +0 -1
- package/dist/chunk-BVL7Y574.js.map +0 -1
- package/dist/chunk-ERFMC6WU.js.map +0 -1
- package/dist/chunk-H5VIV7MQ.js.map +0 -1
- package/dist/chunk-HFRE4QQV.js.map +0 -1
- package/dist/chunk-RS3HRQWU.js.map +0 -1
- package/dist/chunk-SBXNQW4G.js +0 -3
- package/dist/chunk-TT5C5S6R.js +0 -368
- package/dist/chunk-TT5C5S6R.js.map +0 -1
- package/dist/chunk-USO6TERP.js.map +0 -1
- package/dist/chunk-VQ47KWD5.js.map +0 -1
- package/dist/chunk-WUJXJC5O.js +0 -14568
- package/dist/chunk-WUJXJC5O.js.map +0 -1
- package/dist/chunk-ZORVNOKX.js.map +0 -1
- package/src/canvas/usePanZoom.test.ts +0 -244
- package/src/canvas/usePanZoom.ts +0 -147
- package/src/state/Persistence.stories.tsx +0 -75
package/src/theme/base.less
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
// The Oswald and Inter faces come from @weasel-js/theme/faces.css, imported at
|
|
2
2
|
// the top of styles.less.
|
|
3
3
|
|
|
4
|
+
// weasel-ui's native range skin, read for `:extend` below and emitted only
|
|
5
|
+
// through it.
|
|
6
|
+
@import (reference, less) '../../../ui/src/components/range.module.css';
|
|
7
|
+
|
|
4
8
|
// Resets and global element defaults.
|
|
5
9
|
.lk-root {
|
|
6
10
|
font-family: var(--wzl-font-ui);
|
|
@@ -21,7 +25,10 @@
|
|
|
21
25
|
// Deliberate overrides of a weasel-ui component go the other way — see the
|
|
22
26
|
// `.lk-root` prefix convention below.
|
|
23
27
|
:where(.lk-root) {
|
|
24
|
-
|
|
28
|
+
// Only a button with no class or a labkit one. Specificity cannot keep this
|
|
29
|
+
// off a weasel-ui button: whatever its module leaves unset — a height, a
|
|
30
|
+
// backdrop blur — the default fills in.
|
|
31
|
+
:where(button:not([class]), button[class^='lk-'], button[class*=' lk-']) {
|
|
25
32
|
font: inherit;
|
|
26
33
|
color: inherit;
|
|
27
34
|
background: var(--wzl-surface-raised);
|
|
@@ -33,63 +40,32 @@
|
|
|
33
40
|
backdrop-filter: blur(var(--wzl-glass-blur));
|
|
34
41
|
-webkit-backdrop-filter: blur(var(--wzl-glass-blur));
|
|
35
42
|
|
|
36
|
-
&:hover {
|
|
43
|
+
&:where(:hover) {
|
|
37
44
|
border-color: var(--wzl-accent);
|
|
38
45
|
color: var(--wzl-accent-fg);
|
|
39
46
|
}
|
|
40
47
|
|
|
41
|
-
&:focus-visible {
|
|
48
|
+
&:where(:focus-visible) {
|
|
42
49
|
outline: 2px solid var(--wzl-focus-ring);
|
|
43
50
|
outline-offset: 1px;
|
|
44
51
|
}
|
|
45
52
|
|
|
46
|
-
&:disabled {
|
|
53
|
+
&:where(:disabled) {
|
|
47
54
|
color: var(--wzl-fg-subtle);
|
|
48
55
|
cursor: not-allowed;
|
|
49
56
|
}
|
|
50
57
|
}
|
|
51
58
|
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
:where(input[type="range"]) {
|
|
56
|
-
appearance: none;
|
|
57
|
-
-webkit-appearance: none;
|
|
58
|
-
height: 12px;
|
|
59
|
-
background: transparent;
|
|
60
|
-
cursor: pointer;
|
|
61
|
-
|
|
62
|
-
&::-webkit-slider-runnable-track {
|
|
63
|
-
height: var(--wzl-slider-track-h);
|
|
64
|
-
border-radius: var(--wzl-radius-sm);
|
|
65
|
-
background: color-mix(in srgb, var(--wzl-accent) var(--wzl-slider-track-mix), transparent);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
&::-moz-range-track {
|
|
69
|
-
height: var(--wzl-slider-track-h);
|
|
70
|
-
border-radius: var(--wzl-radius-sm);
|
|
71
|
-
background: color-mix(in srgb, var(--wzl-accent) var(--wzl-slider-track-mix), transparent);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
&::-webkit-slider-thumb {
|
|
75
|
-
appearance: none;
|
|
76
|
-
-webkit-appearance: none;
|
|
77
|
-
width: var(--wzl-slider-thumb-size);
|
|
78
|
-
height: var(--wzl-slider-thumb-size);
|
|
79
|
-
border: none;
|
|
80
|
-
border-radius: 50%;
|
|
81
|
-
background: color-mix(in srgb, var(--wzl-accent) var(--wzl-slider-thumb-mix), transparent);
|
|
82
|
-
margin-top: calc((var(--wzl-slider-track-h) - var(--wzl-slider-thumb-size)) / 2);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
&::-moz-range-thumb {
|
|
86
|
-
width: var(--wzl-slider-thumb-size);
|
|
87
|
-
height: var(--wzl-slider-thumb-size);
|
|
88
|
-
border: none;
|
|
89
|
-
border-radius: 50%;
|
|
90
|
-
background: color-mix(in srgb, var(--wzl-accent) var(--wzl-slider-thumb-mix), transparent);
|
|
91
|
-
}
|
|
59
|
+
// The browser sets these in its own `monospace`, which no font token reaches.
|
|
60
|
+
:where(code, kbd, samp, pre) {
|
|
61
|
+
font-family: var(--wzl-font-mono);
|
|
92
62
|
}
|
|
63
|
+
|
|
64
|
+
// A consumer's bare range wears weasel-ui's range skin, extended from its one
|
|
65
|
+
// definition rather than copied. The skin keeps its pseudo-classes inside
|
|
66
|
+
// :where(), so nothing this emits outranks a single class and a weasel-ui
|
|
67
|
+
// component's own module class still wins.
|
|
68
|
+
:where(input[type="range"]):extend(.range all) {}
|
|
93
69
|
}
|
|
94
70
|
|
|
95
71
|
// The `.lk-root` prefix convention: a labkit class handed to a weasel-ui
|
|
@@ -151,9 +127,7 @@
|
|
|
151
127
|
}
|
|
152
128
|
|
|
153
129
|
// Headline / display text uses Oswald light at tighter line-height.
|
|
154
|
-
.lk-lab h1,
|
|
155
|
-
.lk-lab h2,
|
|
156
|
-
.lk-lab h3 {
|
|
130
|
+
:where(.lk-lab) :where(h1, h2, h3) {
|
|
157
131
|
font-family: var(--wzl-font-display);
|
|
158
132
|
font-weight: var(--wzl-font-weight-light);
|
|
159
133
|
letter-spacing: 0.01em;
|
|
@@ -18,7 +18,7 @@ describe('interstellarTheme', () => {
|
|
|
18
18
|
|
|
19
19
|
// Values carried over verbatim from the retired Less.
|
|
20
20
|
expect(dark['--wzl-surface']).toBe('#0a0a14');
|
|
21
|
-
expect(dark['--wzl-accent']).toBe('#
|
|
21
|
+
expect(dark['--wzl-accent']).toBe('#4f46e5');
|
|
22
22
|
// Light mode is weasel's own: interstellar restyles the dark only.
|
|
23
23
|
const weasel = resolveTheme(weaselTheme, { mode: 'light' });
|
|
24
24
|
expect(light['--wzl-surface']).toBe(weasel['--wzl-surface']);
|
|
@@ -81,21 +81,28 @@
|
|
|
81
81
|
"accent": {
|
|
82
82
|
"by": "mode",
|
|
83
83
|
"dark": {
|
|
84
|
-
"value": "#
|
|
84
|
+
"value": "#4f46e5",
|
|
85
85
|
"type": "color"
|
|
86
86
|
}
|
|
87
87
|
},
|
|
88
88
|
"accent-hover": {
|
|
89
89
|
"by": "mode",
|
|
90
90
|
"dark": {
|
|
91
|
-
"value": "#
|
|
91
|
+
"value": "#6d5dfc",
|
|
92
|
+
"type": "color"
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
"accent-strong": {
|
|
96
|
+
"by": "mode",
|
|
97
|
+
"dark": {
|
|
98
|
+
"value": "#7474fe",
|
|
92
99
|
"type": "color"
|
|
93
100
|
}
|
|
94
101
|
},
|
|
95
102
|
"focus-ring": {
|
|
96
103
|
"by": "mode",
|
|
97
104
|
"dark": {
|
|
98
|
-
"value": "rgba(
|
|
105
|
+
"value": "rgba(116, 116, 254, 0.5)",
|
|
99
106
|
"type": "color"
|
|
100
107
|
}
|
|
101
108
|
},
|
package/src/tools/labTool.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
|
+
import { labAnnotationTools } from '../annotations/toolMap';
|
|
1
2
|
import type { InstrumentList } from '../instrument/types';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* The tool the lab's slot holds, once its default is applied: a lab with any
|
|
5
6
|
* annotating instrument starts in `pointer`, so a first click reaches the
|
|
6
|
-
* instrument rather than the marks over it
|
|
7
|
+
* instrument rather than the marks over it, or in the first annotation tool
|
|
8
|
+
* its rail carries when `pointer` is not among them.
|
|
7
9
|
*/
|
|
8
10
|
export function resolveLabTool(slot: string | null, instruments: InstrumentList): string | null {
|
|
9
11
|
if (slot !== null) return slot;
|
|
10
|
-
|
|
12
|
+
const tools = labAnnotationTools(instruments);
|
|
13
|
+
if (tools.some((t) => t.id === 'pointer')) return 'pointer';
|
|
14
|
+
return tools[0]?.id ?? null;
|
|
11
15
|
}
|
|
@@ -69,6 +69,22 @@ describe('an instrument that declares annotations', () => {
|
|
|
69
69
|
}
|
|
70
70
|
});
|
|
71
71
|
|
|
72
|
+
it('gets only the tools it names', () => {
|
|
73
|
+
const narrow = defineInstrument<Record<string, never>, Record<string, never>>({
|
|
74
|
+
...annotating,
|
|
75
|
+
name: 'Narrow',
|
|
76
|
+
annotations: {
|
|
77
|
+
targets: () => [{ id: 'pane', ref: { current: null }, content: { w: 200, h: 100 } }],
|
|
78
|
+
tools: ['pointer', 'select'],
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
render(<Lab instruments={[narrow]} defaultInstrument="Narrow" />);
|
|
82
|
+
expect(screen.getByRole('button', { name: 'Interact' })).toBeTruthy();
|
|
83
|
+
expect(screen.getByRole('button', { name: 'Select' })).toBeTruthy();
|
|
84
|
+
expect(screen.queryByRole('button', { name: 'Freehand' })).toBeNull();
|
|
85
|
+
expect(screen.queryByRole('button', { name: 'Rectangle' })).toBeNull();
|
|
86
|
+
});
|
|
87
|
+
|
|
72
88
|
it('puts the store in reach of its own render', () => {
|
|
73
89
|
render(<Lab instruments={[annotating]} defaultInstrument="Annotating" />);
|
|
74
90
|
expect(screen.getByTestId('pane').dataset.marks).toBe('0');
|
package/src/trial/Trial.less
CHANGED
|
@@ -109,19 +109,6 @@
|
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
.lk-toolbar__zoom-label {
|
|
113
|
-
font-size: var(--wzl-font-size-sm);
|
|
114
|
-
color: var(--wzl-fg-muted);
|
|
115
|
-
padding: 0 var(--wzl-space-sm);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
.lk-sidebar__placeholder {
|
|
119
|
-
padding: var(--wzl-space-sm);
|
|
120
|
-
color: var(--wzl-fg-muted);
|
|
121
|
-
font-size: var(--wzl-font-size-sm);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
|
|
125
112
|
.lk-trial__titlebar {
|
|
126
113
|
display: flex;
|
|
127
114
|
align-items: center;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { fireEvent, render, screen } from '@testing-library/react';
|
|
1
|
+
import { act, fireEvent, 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
4
|
import { defineInstrument } from '../instrument/defineInstrument';
|
|
@@ -126,3 +126,75 @@ describe('the loupe capability', () => {
|
|
|
126
126
|
expect(lens(container)).toBeNull();
|
|
127
127
|
});
|
|
128
128
|
});
|
|
129
|
+
|
|
130
|
+
describe('the loupe and the trial camera share one dispatcher', () => {
|
|
131
|
+
const frame = () => new Promise<void>((r) => requestAnimationFrame(() => r()));
|
|
132
|
+
|
|
133
|
+
function mountDrawn() {
|
|
134
|
+
const zooms: number[] = [];
|
|
135
|
+
const drawn = defineInstrument<Record<string, never>, Record<string, never>>({
|
|
136
|
+
name: 'Zoomed',
|
|
137
|
+
defaultConfig: () => ({}),
|
|
138
|
+
initialState: () => ({}),
|
|
139
|
+
render: () => null,
|
|
140
|
+
canvas: {
|
|
141
|
+
layers: [
|
|
142
|
+
{
|
|
143
|
+
id: 'main',
|
|
144
|
+
draw: (_ctx, { zoom }) => {
|
|
145
|
+
zooms.push(zoom);
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
},
|
|
150
|
+
loupe: true,
|
|
151
|
+
});
|
|
152
|
+
const r = render(<Lab instruments={[drawn]} defaultInstrument="Zoomed" />);
|
|
153
|
+
const stack = r.container.querySelector('.lk-canvas-stack');
|
|
154
|
+
if (!stack) throw new Error('no canvas stack');
|
|
155
|
+
return { ...r, stack, zooms };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const wheel = (el: Element) => {
|
|
159
|
+
const e = new WheelEvent('wheel', {
|
|
160
|
+
deltaY: -300,
|
|
161
|
+
clientX: 50,
|
|
162
|
+
clientY: 50,
|
|
163
|
+
bubbles: true,
|
|
164
|
+
cancelable: true,
|
|
165
|
+
});
|
|
166
|
+
el.dispatchEvent(e);
|
|
167
|
+
return e;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
it('zooms the camera while the lens is down', async () => {
|
|
171
|
+
const h = mountDrawn();
|
|
172
|
+
await act(async () => {
|
|
173
|
+
await frame();
|
|
174
|
+
});
|
|
175
|
+
await act(async () => {
|
|
176
|
+
wheel(h.stack);
|
|
177
|
+
await frame();
|
|
178
|
+
});
|
|
179
|
+
expect(h.zooms.at(-1)).toBeGreaterThan(1);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('gives the wheel to the lens while it is up, and leaves the camera alone', async () => {
|
|
183
|
+
const user = userEvent.setup();
|
|
184
|
+
const h = mountDrawn();
|
|
185
|
+
await user.click(screen.getByRole('button', { name: 'Loupe' }));
|
|
186
|
+
pointAt(h.stack, 60, 40);
|
|
187
|
+
expect(lens(h.container)).not.toBeNull();
|
|
188
|
+
await act(async () => {
|
|
189
|
+
await frame();
|
|
190
|
+
});
|
|
191
|
+
const before = h.zooms.at(-1);
|
|
192
|
+
let e!: WheelEvent;
|
|
193
|
+
await act(async () => {
|
|
194
|
+
e = wheel(h.stack);
|
|
195
|
+
await frame();
|
|
196
|
+
});
|
|
197
|
+
expect(e.defaultPrevented).toBe(true);
|
|
198
|
+
expect(h.zooms.at(-1)).toBe(before);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createPointerStore } from '@weasel-js/core';
|
|
1
2
|
import type { Meta, StoryObj } from '@weasel-js/forge';
|
|
2
3
|
import type { Instrument } from '../instrument/types';
|
|
3
4
|
import { LabContext, type LabContextValue } from '../lab/LabContext';
|
|
@@ -63,6 +64,7 @@ function Harness() {
|
|
|
63
64
|
setZoom: () => {},
|
|
64
65
|
activeToolId: null,
|
|
65
66
|
visibleLayers: [],
|
|
67
|
+
pointer: createPointerStore(),
|
|
66
68
|
},
|
|
67
69
|
emit: () => {},
|
|
68
70
|
})}
|
package/src/trial/Trial.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createPointerStore, PointerContextProvider, WeaselProvider } from '@weasel-js/core';
|
|
1
2
|
import { type ReactNode, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
3
|
import { useStore } from 'zustand/react';
|
|
3
4
|
import { AnnotationsContext } from '../annotations/AnnotationsContext';
|
|
@@ -5,8 +6,10 @@ import { AnnotationTargets } from '../annotations/AnnotationTargets';
|
|
|
5
6
|
import { AnnotationPreloadContext } from '../annotations/preload';
|
|
6
7
|
import { annotationsFromJSON } from '../annotations/store';
|
|
7
8
|
import type { AnnotationStorage, AnnotationTargetInfo } from '../annotations/types';
|
|
9
|
+
import type { CameraView } from '../canvas/CameraInput';
|
|
8
10
|
import { CameraWheelContext, type CameraWheelSlot } from '../canvas/CameraWheelContext';
|
|
9
11
|
import { CanvasStack } from '../canvas/CanvasStack';
|
|
12
|
+
import { CameraPublishContext, CameraRegistryContext } from '../canvas/cameraRegistry';
|
|
10
13
|
import { fitStage, Stage } from '../canvas/Stage';
|
|
11
14
|
import type { CanvasLayerDescriptor } from '../canvas/useLayerScheduler';
|
|
12
15
|
import { applyCamera, type ViewportSize } from '../canvas/worldSpec';
|
|
@@ -134,6 +137,13 @@ function TrialRuntime({
|
|
|
134
137
|
suppress,
|
|
135
138
|
}: TrialRuntimeProps) {
|
|
136
139
|
const wheelSlot = useRef<CameraWheelSlot['current']>(null);
|
|
140
|
+
const cameras = useContext(CameraRegistryContext);
|
|
141
|
+
const trialId = record.id;
|
|
142
|
+
const publishCamera = useMemo(
|
|
143
|
+
() => (cameras ? (camera: CameraView) => cameras.register(trialId, camera) : null),
|
|
144
|
+
[cameras, trialId],
|
|
145
|
+
);
|
|
146
|
+
const [pointer] = useState(createPointerStore);
|
|
137
147
|
const canvasContainerRef = useRef<HTMLDivElement | null>(null);
|
|
138
148
|
const loupeHostRef = useRef<HTMLDivElement | null>(null);
|
|
139
149
|
const updateTrialState = useStore(store, (s) => s.updateTrialState);
|
|
@@ -328,6 +338,7 @@ function TrialRuntime({
|
|
|
328
338
|
},
|
|
329
339
|
activeToolId: resolvedToolId,
|
|
330
340
|
visibleLayers,
|
|
341
|
+
pointer,
|
|
331
342
|
},
|
|
332
343
|
emit: (event) => {
|
|
333
344
|
snapshotIfNeeded(event);
|
|
@@ -589,6 +600,17 @@ function TrialRuntime({
|
|
|
589
600
|
/>
|
|
590
601
|
) : null;
|
|
591
602
|
|
|
603
|
+
// The trial's root registries, isolated from the trial beside it. Each
|
|
604
|
+
// camera inside mounts an input scope under them; chrome here reaches the
|
|
605
|
+
// one last used. The pointer store is the trial's, shared by all of them.
|
|
606
|
+
const scopedBody = (
|
|
607
|
+
<WeaselProvider isolate>
|
|
608
|
+
<PointerContextProvider store={pointer}>
|
|
609
|
+
<CameraPublishContext.Provider value={publishCamera}>{body}</CameraPublishContext.Provider>
|
|
610
|
+
</PointerContextProvider>
|
|
611
|
+
</WeaselProvider>
|
|
612
|
+
);
|
|
613
|
+
|
|
592
614
|
return (
|
|
593
615
|
<TrialIdProvider trialId={record.id}>
|
|
594
616
|
<AnnotationsContext.Provider value={annotationsCap ? annotations : null}>
|
|
@@ -607,7 +629,7 @@ function TrialRuntime({
|
|
|
607
629
|
activeToolId={resolvedToolId}
|
|
608
630
|
setActiveTool={setActiveTool}
|
|
609
631
|
>
|
|
610
|
-
{
|
|
632
|
+
{scopedBody}
|
|
611
633
|
{annotationOverlays}
|
|
612
634
|
</TrialChrome>
|
|
613
635
|
</CameraWheelContext.Provider>
|
|
@@ -1,5 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type KeyboardEvent,
|
|
3
|
+
type ReactNode,
|
|
4
|
+
useContext,
|
|
5
|
+
useMemo,
|
|
6
|
+
useSyncExternalStore,
|
|
7
|
+
} from 'react';
|
|
2
8
|
import { useStore } from 'zustand/react';
|
|
9
|
+
import { CameraRegistryContext } from '../canvas/cameraRegistry';
|
|
10
|
+
import { zoomCameraTo } from '../canvas/cameraZoom';
|
|
3
11
|
import { builtinContributions } from '../chrome/builtins';
|
|
4
12
|
import { mergeContributions, suppressContributions } from '../chrome/merge';
|
|
5
13
|
import { PaletteRegion } from '../chrome/regions/PaletteRegion';
|
|
@@ -68,6 +76,7 @@ const NO_OP = (): void => {};
|
|
|
68
76
|
/** One frozen empty map, so a trial that has folded nothing keeps a stable
|
|
69
77
|
* context identity across renders. */
|
|
70
78
|
const NO_SECTIONS: Readonly<Record<string, boolean>> = Object.freeze({});
|
|
79
|
+
const noSubscribe = (): (() => void) => NO_OP;
|
|
71
80
|
|
|
72
81
|
/** The frame around a running instrument. Builds one chrome context, assembles
|
|
73
82
|
* the contributions the instrument, the runtime and the lab declare, and hands
|
|
@@ -123,10 +132,18 @@ export function TrialChrome({
|
|
|
123
132
|
const title = record.title ?? instrument.title ?? instrument.name;
|
|
124
133
|
const collapsedSections = record.collapsedSections ?? NO_SECTIONS;
|
|
125
134
|
|
|
135
|
+
// The trial's own camera, where it has one: zoom goes through it, about the
|
|
136
|
+
// middle of the view and inside the instrument's range, as the lab's does.
|
|
137
|
+
const cameras = useContext(CameraRegistryContext);
|
|
138
|
+
const camera = useSyncExternalStore(
|
|
139
|
+
cameras?.subscribe ?? noSubscribe,
|
|
140
|
+
() => cameras?.get(trialId) ?? null,
|
|
141
|
+
);
|
|
142
|
+
|
|
126
143
|
const ctx = useMemo<TrialChromeContext>(() => {
|
|
127
144
|
const setZoom = (z: number): void => {
|
|
128
|
-
if (
|
|
129
|
-
updateTrialView(trialId, withZoom(view2d, z));
|
|
145
|
+
if (camera) zoomCameraTo(camera, z);
|
|
146
|
+
else if (view2d) updateTrialView(trialId, withZoom(view2d, z));
|
|
130
147
|
};
|
|
131
148
|
return {
|
|
132
149
|
trialId,
|
|
@@ -203,6 +220,7 @@ export function TrialChrome({
|
|
|
203
220
|
setTrialTitle,
|
|
204
221
|
setTrialSectionCollapsed,
|
|
205
222
|
collapsedSections,
|
|
223
|
+
camera,
|
|
206
224
|
]);
|
|
207
225
|
|
|
208
226
|
const contributions = useMemo(
|
|
@@ -132,7 +132,7 @@ describe('addTrial', () => {
|
|
|
132
132
|
const out = addTrial([], instruments, 'Schemed', { config: { gap: auto } });
|
|
133
133
|
expect(out[0]?.auto).toContain('gap');
|
|
134
134
|
expect(out[0]?.autoSeed).toEqual(['gap']);
|
|
135
|
-
expect((out[0]?.config as { gap: number })
|
|
135
|
+
expect((out[0]?.config as { gap: number } | undefined)?.gap).toBe(12);
|
|
136
136
|
expect(out[0]?.configSeed).not.toHaveProperty('gap');
|
|
137
137
|
});
|
|
138
138
|
|
|
@@ -141,13 +141,13 @@ describe('addTrial', () => {
|
|
|
141
141
|
const out = addTrial([], instruments, 'Counter', {
|
|
142
142
|
config: { at: when } as unknown as Partial<CounterConfig>,
|
|
143
143
|
});
|
|
144
|
-
expect((out[0]?.config as { at: unknown })
|
|
144
|
+
expect((out[0]?.config as { at: unknown } | undefined)?.at).toBe(when);
|
|
145
145
|
});
|
|
146
146
|
|
|
147
147
|
it('honors auto at a nested seed path', () => {
|
|
148
148
|
const out = addTrial([], instruments, 'Schemed', { config: { grid: { size: auto } } });
|
|
149
149
|
expect(out[0]?.auto).toContain('grid.size');
|
|
150
|
-
expect((out[0]?.config as { grid: { size: number } })
|
|
150
|
+
expect((out[0]?.config as { grid: { size: number } } | undefined)?.grid.size).toBe(4);
|
|
151
151
|
});
|
|
152
152
|
|
|
153
153
|
it('leaves a trial with no auto field when nothing is unpinned', () => {
|
|
@@ -222,7 +222,7 @@ describe('resetTrial', () => {
|
|
|
222
222
|
const arr = addTrial([], instruments, 'Counter');
|
|
223
223
|
head(arr).config = { step: 99 } as CounterConfig;
|
|
224
224
|
resetTrial(arr, head(arr).id, instruments);
|
|
225
|
-
expect((arr[0]?.config as CounterConfig)
|
|
225
|
+
expect((arr[0]?.config as CounterConfig | undefined)?.step).toBe(99);
|
|
226
226
|
});
|
|
227
227
|
|
|
228
228
|
it('restores the seed the trial was opened with, not the bare defaults', () => {
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { Stance } from '@weasel-js/theme';
|
|
2
|
-
import { ReactNode } from 'react';
|
|
3
|
-
import { ToolPrefLeaf } from '@weasel-js/core';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* What a surface holds, and which of its peers it is. Every surface that takes
|
|
7
|
-
* these takes both, and the theme decides how each looks.
|
|
8
|
-
*/
|
|
9
|
-
interface StanceProps {
|
|
10
|
-
/** The class of content the surface holds. Omitted, it draws as it always has. */
|
|
11
|
-
stance?: Stance;
|
|
12
|
-
/**
|
|
13
|
-
* Which of its peers this is: an index into the theme's tone list, wrapping,
|
|
14
|
-
* or a color given directly. Independent of `stance`, and allowed without one.
|
|
15
|
-
*/
|
|
16
|
-
tone?: number | string;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** What a {@link PrefRenderer} is given for the leaf it is rendering. */
|
|
20
|
-
interface PrefRenderContext {
|
|
21
|
-
/** Dotted path of the leaf within the schema root. */
|
|
22
|
-
path: string;
|
|
23
|
-
/** The schema node. App renderers narrow this to their own kind shape. */
|
|
24
|
-
pref: ToolPrefLeaf;
|
|
25
|
-
/** Current value — `values` at `path`, falling back to `pref.default`. */
|
|
26
|
-
value: unknown;
|
|
27
|
-
setValue: (value: unknown) => void;
|
|
28
|
-
/** Whether this leaf is currently auto — not pinned, computed by the owner. */
|
|
29
|
-
auto: boolean;
|
|
30
|
-
/** Toggle this leaf's auto state. */
|
|
31
|
-
setAuto: (next: boolean) => void;
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* Renders the control cell for one preference leaf. Returning `null`
|
|
35
|
-
* collapses the row.
|
|
36
|
-
*/
|
|
37
|
-
type PrefRenderer = (ctx: PrefRenderContext) => ReactNode;
|
|
38
|
-
|
|
39
|
-
export type { PrefRenderer as P, StanceProps as S, PrefRenderContext as a };
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { ReactNode } from 'react';
|
|
2
|
-
import { C as ControlRenderer } from './types-B7y8hX1D.js';
|
|
3
|
-
|
|
4
|
-
/** Options for {@link inDialog}. */
|
|
5
|
-
interface InDialogOptions {
|
|
6
|
-
/** What the row's button shows of the value. Defaults to a list joined with
|
|
7
|
-
* commas, or the value itself. */
|
|
8
|
-
summary?: (value: unknown) => ReactNode;
|
|
9
|
-
/** The dialog's heading. Defaults to the row's label. */
|
|
10
|
-
title?: ReactNode;
|
|
11
|
-
}
|
|
12
|
-
/** The one-line summary a dialog row shows when it is given none. */
|
|
13
|
-
declare function summarizeValue(value: unknown): string;
|
|
14
|
-
/**
|
|
15
|
-
* Moves a control into a modal: the row becomes a button showing a summary of
|
|
16
|
-
* the value, and the button opens a dialog whose body is `body`, handed the
|
|
17
|
-
* same context a row renderer gets. For a leaf whose editor will never fit a
|
|
18
|
-
* sidebar row.
|
|
19
|
-
*/
|
|
20
|
-
declare function inDialog(body: ControlRenderer, opts?: InDialogOptions): ControlRenderer;
|
|
21
|
-
|
|
22
|
-
export { inDialog as i, summarizeValue as s };
|
|
23
|
-
export type { InDialogOptions as I };
|