@weasel-js/labkit 1.6.1 → 1.7.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 +3 -3
- package/dist/_dts/{CanvasStackContext-Wo6OwIVM.d.ts → CanvasStackContext-Cjy6I-Iv.d.ts} +4 -1
- package/dist/_dts/{Stage-DWBBi8IU.d.ts → Stage-D6t7o0pF.d.ts} +59 -18
- package/dist/_dts/{index-CKyX0ME_.d.ts → index-BTmaRy6a.d.ts} +10 -6
- package/dist/_dts/{types-B7yBTTF9.d.ts → index-DCMA4Fy1.d.ts} +59 -113
- package/dist/_dts/{types-DTuViBvc.d.ts → types-BUBSIZuV.d.ts} +18 -8
- package/dist/_dts/{useTrialState-CMaV6UD4.d.ts → useTrialState-Bh8bzFgE.d.ts} +1 -1
- package/dist/canvas/index.d.ts +8 -6
- package/dist/canvas/index.js +6 -6
- package/dist/chrome/index.d.ts +6 -4
- package/dist/chrome/index.js +11 -10
- package/dist/chunk-2ES4W47N.js +8 -0
- package/dist/chunk-2ES4W47N.js.map +1 -0
- package/dist/{chunk-WPLAANH4.js → chunk-2N7UBSS2.js} +12 -12
- package/dist/chunk-2N7UBSS2.js.map +1 -0
- package/dist/chunk-4KSC7NXL.js +15491 -0
- package/dist/chunk-4KSC7NXL.js.map +1 -0
- package/dist/{chunk-SLAQPUI4.js → chunk-CEGFXTBD.js} +10 -63
- package/dist/chunk-CEGFXTBD.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-QGMOA2YE.js → chunk-E23Q6A3Y.js} +296 -104
- package/dist/chunk-E23Q6A3Y.js.map +1 -0
- package/dist/{chunk-GHGO52SO.js → chunk-E6NARGU7.js} +5 -5
- package/dist/{chunk-GHGO52SO.js.map → chunk-E6NARGU7.js.map} +1 -1
- package/dist/{chunk-IJJUK4FU.js → chunk-EESJUSYR.js} +5 -6
- package/dist/chunk-EESJUSYR.js.map +1 -0
- package/dist/{chunk-HXXTULDN.js → chunk-EFXRM6XQ.js} +8 -11
- package/dist/chunk-EFXRM6XQ.js.map +1 -0
- package/dist/chunk-F4ACIK7Z.js +210 -0
- package/dist/chunk-F4ACIK7Z.js.map +1 -0
- package/dist/{chunk-6FLXL3I7.js → chunk-FB4MIZOO.js} +77 -45
- package/dist/chunk-FB4MIZOO.js.map +1 -0
- package/dist/{chunk-AWQG7FCC.js → chunk-FG3EXZDV.js} +3 -3
- package/dist/{chunk-AWQG7FCC.js.map → chunk-FG3EXZDV.js.map} +1 -1
- package/dist/{chunk-4TT3PNPR.js → chunk-GAHELLJT.js} +95 -95
- package/dist/chunk-GAHELLJT.js.map +1 -0
- package/dist/{chunk-HKV7LRHY.js → chunk-IBVRMHW5.js} +5 -5
- package/dist/{chunk-HKV7LRHY.js.map → chunk-IBVRMHW5.js.map} +1 -1
- package/dist/{chunk-ASOK76ZH.js → chunk-JFOJESJF.js} +72 -29
- package/dist/chunk-JFOJESJF.js.map +1 -0
- package/dist/{chunk-RWNDDKOH.js → chunk-PMLZP7RT.js} +18 -15
- package/dist/chunk-PMLZP7RT.js.map +1 -0
- package/dist/{chunk-TJ77A4A3.js → chunk-READTDQX.js} +5 -9
- package/dist/chunk-READTDQX.js.map +1 -0
- package/dist/{chunk-JVBYV6TL.js → chunk-ROGJWDQA.js} +3 -3
- package/dist/{chunk-JVBYV6TL.js.map → chunk-ROGJWDQA.js.map} +1 -1
- package/dist/{chunk-CGHF6RZV.js → chunk-T6DLGEJB.js} +18 -18
- package/dist/chunk-T6DLGEJB.js.map +1 -0
- package/dist/{chunk-24W5VEJW.js → chunk-ULYYNWGP.js} +2 -2
- package/dist/{chunk-24W5VEJW.js.map → chunk-ULYYNWGP.js.map} +1 -1
- package/dist/{chunk-DWMSB4CA.js → chunk-VYVGVBGY.js} +95 -32
- package/dist/chunk-VYVGVBGY.js.map +1 -0
- package/dist/{chunk-IRTGUQ5O.js → chunk-WLMDTI2Q.js} +16 -6
- package/dist/chunk-WLMDTI2Q.js.map +1 -0
- package/dist/{chunk-NEXR37YJ.js → chunk-XE5HJOPL.js} +6 -8
- package/dist/chunk-XE5HJOPL.js.map +1 -0
- package/dist/chunk-XVUKDSYV.js +38 -0
- package/dist/chunk-XVUKDSYV.js.map +1 -0
- package/dist/{chunk-O6FFFOHS.js → chunk-YCFKW5RX.js} +16 -8
- package/dist/chunk-YCFKW5RX.js.map +1 -0
- package/dist/{chunk-CQLQPQ4P.js → chunk-ZQIHJXP4.js} +2 -2
- package/dist/chunk-ZQIHJXP4.js.map +1 -0
- package/dist/config/index.d.ts +28 -13
- package/dist/config/index.js +4 -4
- package/dist/controls/index.d.ts +4 -7
- package/dist/controls/index.js +6 -5
- package/dist/dragdrop/index.d.ts +23 -3
- package/dist/dragdrop/index.js +4 -3
- package/dist/fonts/oswald-tabular-digits.woff2 +0 -0
- package/dist/index.d.ts +72 -487
- package/dist/index.js +603 -441
- package/dist/index.js.map +1 -1
- package/dist/job/index.d.ts +8 -0
- package/dist/job/index.js +1 -1
- package/dist/layers/index.d.ts +6 -5
- package/dist/layers/index.js +3 -2
- package/dist/loupe/index.d.ts +100 -30
- package/dist/loupe/index.js +585 -6
- package/dist/loupe/index.js.map +1 -1
- package/dist/overview/index.d.ts +4 -2
- package/dist/overview/index.js +20 -18
- package/dist/overview/index.js.map +1 -1
- package/dist/passthrough/weasel-canvas.d.ts +2 -0
- package/dist/passthrough/weasel-ui.d.ts +2 -2701
- package/dist/passthrough/weasel-ui.js +4 -3
- package/dist/primitives/index.d.ts +37 -2
- package/dist/primitives/index.js +9 -8
- package/dist/state/index.d.ts +7 -5
- package/dist/state/index.js +6 -6
- package/dist/styles.css +150 -126
- package/dist/surface/index.d.ts +10 -2
- package/dist/surface/index.js +1 -1
- package/dist/undo/index.d.ts +5 -3
- package/package.json +14 -13
- package/src/annotations/AnnotationOverlay.abandon.test.tsx +143 -0
- package/src/annotations/AnnotationOverlay.tsx +52 -33
- package/src/annotations/Annotations.less +0 -10
- package/src/annotations/Annotations.overlay.test.tsx +31 -0
- package/src/annotations/MarkList.tsx +4 -8
- package/src/annotations/capture.browser.test.ts +41 -0
- package/src/annotations/capture.ts +14 -7
- package/src/annotations/drawOne.ts +2 -2
- package/src/annotations/frac.test.ts +44 -1
- package/src/annotations/frac.ts +46 -18
- package/src/annotations/index.ts +1 -0
- package/src/annotations/paint.test.ts +76 -46
- package/src/annotations/paint.ts +65 -43
- package/src/annotations/store.test.ts +212 -2
- package/src/annotations/store.ts +130 -27
- package/src/annotations/svgNodes.test.ts +28 -19
- package/src/annotations/svgNodes.ts +6 -6
- package/src/annotations/types.ts +29 -10
- package/src/canvas/AGENTS.md +1 -0
- package/src/canvas/CameraInput.test.tsx +30 -0
- package/src/canvas/CameraInput.tsx +60 -48
- package/src/canvas/CanvasStack.tsx +11 -6
- package/src/canvas/CanvasStackContext.ts +4 -1
- package/src/canvas/LinkedCursor.less +1 -1
- package/src/canvas/Stage.test.tsx +33 -4
- package/src/canvas/Stage.tsx +9 -4
- package/src/canvas/cameraRegistry.test.ts +55 -0
- package/src/canvas/cameraRegistry.ts +87 -0
- package/src/canvas/cameraZoom.test.ts +46 -0
- package/src/canvas/cameraZoom.ts +24 -0
- package/src/canvas/index.ts +14 -2
- package/src/canvas/sharedScope.test.tsx +94 -0
- package/src/canvas/worldSpec.ts +3 -1
- package/src/chrome/ChromeRegions.stories.tsx +1 -0
- package/src/chrome/builtins.test.ts +7 -6
- package/src/chrome/builtins.tsx +5 -1
- package/src/chrome/index.ts +1 -1
- package/src/chrome/merge.test.ts +5 -0
- package/src/chrome/merge.ts +4 -2
- package/src/chrome/regions/PaletteRegion.less +0 -7
- package/src/chrome/regions/PaletteRegion.test.tsx +2 -3
- package/src/chrome/regions/PaletteRegion.tsx +3 -4
- package/src/chrome/regions/SidebarRegion.less +12 -2
- package/src/chrome/regions/ViewportRegion.browser.test.less +6 -0
- package/src/chrome/regions/ViewportRegion.browser.test.tsx +38 -0
- package/src/chrome/regions/ViewportRegion.less +6 -0
- package/src/chrome/regions/ViewportRegion.tsx +10 -4
- package/src/chrome/types.ts +4 -2
- package/src/config/auto.ts +1 -0
- package/src/config/builder.test.ts +2 -1
- package/src/config/builder.ts +18 -5
- package/src/config/fromConfigField.test.ts +14 -0
- package/src/config/fromConfigField.ts +18 -4
- package/src/config/path.ts +2 -5
- package/src/config/resolve.ts +4 -0
- package/src/config/types.ts +14 -9
- package/src/controls/ControlPanel.less +1 -1
- package/src/controls/ControlPanel.stories.tsx +3 -2
- package/src/controls/ControlPanel.test.tsx +98 -11
- package/src/controls/ControlPanel.tsx +61 -2
- package/src/dragdrop/DragDropRuntime.tsx +22 -13
- package/src/dragdrop/DragGhost.tsx +3 -0
- package/src/dragdrop/Palette.tsx +2 -0
- package/src/index.test.ts +5 -0
- package/src/index.ts +16 -2
- package/src/instrument/types.ts +0 -6
- package/src/job/useJob.ts +14 -9
- package/src/lab/Lab.tsx +88 -73
- 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 +9 -17
- package/src/lab/LabSwitcher.browser.test.tsx +27 -0
- package/src/lab/LabSwitcher.less +8 -6
- package/src/lab/LabZoom.test.tsx +223 -0
- package/src/lab/LabZoom.tsx +170 -0
- package/src/lab/Workspace.tsx +48 -52
- package/src/lab/index.ts +1 -0
- package/src/loupe/AGENTS.md +28 -15
- package/src/loupe/CanvasLoupe.tsx +2 -3
- package/src/loupe/DomLoupe.tsx +1 -7
- package/src/loupe/Loupe.less +16 -0
- package/src/loupe/LoupeGestures.tsx +1 -1
- package/src/loupe/TrialLoupe.tsx +120 -57
- package/src/loupe/index.ts +1 -6
- package/src/loupe/types.test.ts +3 -3
- package/src/loupe/types.ts +24 -27
- package/src/loupe/useLoupe.test.tsx +29 -4
- package/src/loupe/useLoupe.ts +20 -17
- package/src/overview/OverviewMarks.tsx +12 -4
- package/src/overview/TrialOverview.tsx +10 -8
- package/src/passthrough/weasel-ui.test.ts +2 -0
- package/src/passthrough/weasel-ui.ts +17 -5
- package/src/primitives/FloatingPanel.test.tsx +11 -0
- package/src/primitives/FloatingPanel.tsx +8 -7
- package/src/primitives/JobProgress.less +2 -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/Split.tsx +69 -41
- package/src/primitives/Toolbar.tsx +3 -3
- package/src/primitives/ZoomControl.less +1 -2
- package/src/primitives/index.ts +2 -0
- package/src/specimen/Specimen.stories.tsx +1 -0
- package/src/specimen/Specimen.tsx +2 -3
- package/src/state/Persistence.browser.test.tsx +47 -0
- package/src/state/store.test.ts +5 -1
- package/src/state/undock.ts +3 -0
- package/src/state/useOpenOnce.ts +20 -16
- package/src/state/usePersistedState.test.tsx +1 -1
- package/src/state/view.ts +1 -0
- package/src/styles.less +1 -0
- package/src/surface/index.ts +1 -1
- package/src/surface/useTiledSurface.test.tsx +14 -0
- package/src/surface/useTiledSurface.ts +11 -3
- package/src/theme/Interstellar.stories.tsx +3 -1
- 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/trial/Trial.annotations.test.tsx +73 -0
- package/src/trial/Trial.less +0 -29
- package/src/trial/Trial.loupe.test.tsx +28 -11
- package/src/trial/Trial.tsx +85 -67
- package/src/trial/TrialChrome.tsx +27 -6
- package/src/trial/index.ts +2 -0
- package/src/trial/loupeSwitch.ts +15 -0
- package/dist/_dts/LayerList.d-BoELAMsM.d.ts +0 -83
- package/dist/_dts/PrefsRow.d-DFpPIeIC.d.ts +0 -39
- package/dist/_dts/PropertyField.d-D0ndfIIt.d.ts +0 -404
- package/dist/_dts/Select.d-Cr8-zoup.d.ts +0 -569
- package/dist/chunk-4TT3PNPR.js.map +0 -1
- package/dist/chunk-6FLXL3I7.js.map +0 -1
- package/dist/chunk-ASOK76ZH.js.map +0 -1
- package/dist/chunk-CGHF6RZV.js.map +0 -1
- package/dist/chunk-CQLQPQ4P.js.map +0 -1
- package/dist/chunk-DWMSB4CA.js.map +0 -1
- package/dist/chunk-HXXTULDN.js.map +0 -1
- package/dist/chunk-IJJUK4FU.js.map +0 -1
- package/dist/chunk-IRTGUQ5O.js.map +0 -1
- package/dist/chunk-KHK5FGER.js +0 -145
- package/dist/chunk-KHK5FGER.js.map +0 -1
- package/dist/chunk-NEXR37YJ.js.map +0 -1
- package/dist/chunk-O6FFFOHS.js.map +0 -1
- package/dist/chunk-QGMOA2YE.js.map +0 -1
- package/dist/chunk-RWNDDKOH.js.map +0 -1
- package/dist/chunk-S4NCI32V.js +0 -549
- package/dist/chunk-S4NCI32V.js.map +0 -1
- package/dist/chunk-SBXNQW4G.js +0 -3
- package/dist/chunk-SLAQPUI4.js.map +0 -1
- package/dist/chunk-TJ77A4A3.js.map +0 -1
- package/dist/chunk-TYN6MLXX.js +0 -13864
- package/dist/chunk-TYN6MLXX.js.map +0 -1
- package/dist/chunk-WPLAANH4.js.map +0 -1
- package/src/primitives/useRovingTabIndex.test.ts +0 -48
- package/src/primitives/useRovingTabIndex.ts +0 -88
- package/src/state/Persistence.stories.tsx +0 -75
package/src/chrome/types.ts
CHANGED
|
@@ -162,8 +162,10 @@ export interface TrialChromeContext extends ToolSlotContext, SidebarSlotContext
|
|
|
162
162
|
undo: () => void;
|
|
163
163
|
redo: () => void;
|
|
164
164
|
|
|
165
|
-
/** Whether the trial's
|
|
166
|
-
*
|
|
165
|
+
/** Whether a `<TrialLoupe>` following the trial's toggle is mounted in it.
|
|
166
|
+
* The loupe toggle is offered only while one is. */
|
|
167
|
+
hasLoupe: boolean;
|
|
168
|
+
/** Whether the trial's loupe is turned on. */
|
|
167
169
|
loupeOn: boolean;
|
|
168
170
|
toggleLoupe: () => void;
|
|
169
171
|
|
package/src/config/auto.ts
CHANGED
|
@@ -10,6 +10,7 @@ export const auto: unique symbol = Symbol('weasel.auto');
|
|
|
10
10
|
/** The type of the `auto` sentinel, for widening a value parameter. */
|
|
11
11
|
export type Auto = typeof auto;
|
|
12
12
|
|
|
13
|
+
/** Whether `value` is the `auto` sentinel. */
|
|
13
14
|
export function isAuto(value: unknown): value is Auto {
|
|
14
15
|
return value === auto;
|
|
15
16
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { compact } from '@weasel-js/quantity';
|
|
1
2
|
import { describe, expect, it } from 'vitest';
|
|
2
3
|
import { auto } from './auto';
|
|
3
4
|
import { f } from './builder';
|
|
@@ -37,7 +38,7 @@ describe('builder', () => {
|
|
|
37
38
|
});
|
|
38
39
|
|
|
39
40
|
it('annotates a number with a display format', () => {
|
|
40
|
-
expect(f.number(0).
|
|
41
|
+
expect(f.number(0).display(compact()).annotations.display).toEqual(compact());
|
|
41
42
|
});
|
|
42
43
|
|
|
43
44
|
it('keeps a custom node kind across a chain', () => {
|
package/src/config/builder.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Display } from '@weasel-js/quantity';
|
|
2
|
+
import type { PrefLeaf, PrefNumberUnit } from '@weasel-js/ui';
|
|
2
3
|
import { type Auto, isAuto } from './auto';
|
|
3
4
|
import type {
|
|
4
5
|
Annotations,
|
|
@@ -123,6 +124,7 @@ export abstract class BaseNode<T> implements ConfigNode<T> {
|
|
|
123
124
|
}
|
|
124
125
|
}
|
|
125
126
|
|
|
127
|
+
/** A numeric leaf, built by `f.number`. */
|
|
126
128
|
export class NumberNode extends BaseNode<number> {
|
|
127
129
|
readonly kind = 'number';
|
|
128
130
|
|
|
@@ -141,10 +143,10 @@ export class NumberNode extends BaseNode<number> {
|
|
|
141
143
|
return this.ann({ suffix });
|
|
142
144
|
}
|
|
143
145
|
|
|
144
|
-
/** Show the value
|
|
145
|
-
* only: the stored value stays a plain number
|
|
146
|
-
|
|
147
|
-
return this.ann({
|
|
146
|
+
/** Show, speak and read the value through a display — `compact()` reads
|
|
147
|
+
* `2.00M`. Presentation only: the stored value stays a plain number. */
|
|
148
|
+
display(display: Display): this {
|
|
149
|
+
return this.ann({ display });
|
|
148
150
|
}
|
|
149
151
|
|
|
150
152
|
/**
|
|
@@ -168,6 +170,7 @@ export class NumberNode extends BaseNode<number> {
|
|
|
168
170
|
}
|
|
169
171
|
}
|
|
170
172
|
|
|
173
|
+
/** A true/false leaf, built by `f.boolean`; a checkbox unless `.toggle()`. */
|
|
171
174
|
export class BooleanNode extends BaseNode<boolean> {
|
|
172
175
|
readonly kind = 'boolean';
|
|
173
176
|
|
|
@@ -177,6 +180,7 @@ export class BooleanNode extends BaseNode<boolean> {
|
|
|
177
180
|
}
|
|
178
181
|
}
|
|
179
182
|
|
|
183
|
+
/** A text leaf, built by `f.string`. */
|
|
180
184
|
export class StringNode extends BaseNode<string> {
|
|
181
185
|
readonly kind = 'string';
|
|
182
186
|
|
|
@@ -194,6 +198,7 @@ export class StringNode extends BaseNode<string> {
|
|
|
194
198
|
}
|
|
195
199
|
}
|
|
196
200
|
|
|
201
|
+
/** A string-list leaf, built by `f.list`. */
|
|
197
202
|
export class ListNode extends BaseNode<string[]> {
|
|
198
203
|
readonly kind = 'list';
|
|
199
204
|
|
|
@@ -203,6 +208,7 @@ export class ListNode extends BaseNode<string[]> {
|
|
|
203
208
|
}
|
|
204
209
|
}
|
|
205
210
|
|
|
211
|
+
/** A color leaf holding a hex string, built by `f.color`. */
|
|
206
212
|
export class ColorNode extends BaseNode<string> {
|
|
207
213
|
readonly kind = 'color';
|
|
208
214
|
|
|
@@ -213,6 +219,7 @@ export class ColorNode extends BaseNode<string> {
|
|
|
213
219
|
}
|
|
214
220
|
}
|
|
215
221
|
|
|
222
|
+
/** A fixed-choice leaf, built by `f.enum`; a select unless `.radio()`. */
|
|
216
223
|
export class EnumNode<T extends string> extends BaseNode<T> {
|
|
217
224
|
readonly kind = 'enum';
|
|
218
225
|
|
|
@@ -286,6 +293,12 @@ export class GroupNode<S extends ConfigShape> implements ConfigBranch<S> {
|
|
|
286
293
|
showIf(predicate: (config: Record<string, unknown>) => boolean): GroupNode<S> {
|
|
287
294
|
return this.with(this.annotations, { ...this.options, showIf: predicate });
|
|
288
295
|
}
|
|
296
|
+
|
|
297
|
+
/** Put a button on the group's heading that returns every value beneath it
|
|
298
|
+
* to its default, leaving the rest of the config alone. */
|
|
299
|
+
resettable(): GroupNode<S> {
|
|
300
|
+
return this.with(this.annotations, { ...this.options, resettable: true });
|
|
301
|
+
}
|
|
289
302
|
}
|
|
290
303
|
|
|
291
304
|
/** Every default in a shape, nested the way the shape is. */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import type { ConfigField } from '../controls/types';
|
|
3
3
|
import { fromConfigFields } from './fromConfigField';
|
|
4
|
+
import { schemaNodeAtPath } from './path';
|
|
4
5
|
import { isLeafVisible } from './visible';
|
|
5
6
|
|
|
6
7
|
const leaf = (fields: ConfigField[], k: string) =>
|
|
@@ -76,6 +77,19 @@ describe('fromConfigFields', () => {
|
|
|
76
77
|
]);
|
|
77
78
|
expect(Object.keys(r.group.children)).toEqual(['z', 'a']);
|
|
78
79
|
});
|
|
80
|
+
|
|
81
|
+
it('files a dotted key at the path it names, the one config reads and writes use', () => {
|
|
82
|
+
const r = fromConfigFields([
|
|
83
|
+
{ type: 'number', key: 'pose.x', label: 'X', default: 0 },
|
|
84
|
+
{ type: 'number', key: 'pose.y', label: 'Y', default: 0 },
|
|
85
|
+
{ type: 'checkbox', key: 'on', label: 'On', default: true },
|
|
86
|
+
]);
|
|
87
|
+
expect(schemaNodeAtPath(r.group, 'pose.x')).toMatchObject({ kind: 'number', name: 'X' });
|
|
88
|
+
expect(schemaNodeAtPath(r.group, 'pose.y')).toMatchObject({ kind: 'number', name: 'Y' });
|
|
89
|
+
// The intermediate is headless: a flat field list never asked for a heading.
|
|
90
|
+
expect(schemaNodeAtPath(r.group, 'pose')).toMatchObject({ name: '' });
|
|
91
|
+
expect(Object.keys(r.group.children)).toEqual(['pose', 'on']);
|
|
92
|
+
});
|
|
79
93
|
});
|
|
80
94
|
|
|
81
95
|
describe('isLeafVisible', () => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PrefLeaf } from '@weasel-js/ui';
|
|
1
|
+
import type { PrefGroup, PrefLeaf } from '@weasel-js/ui';
|
|
2
2
|
import type { ConfigField } from '../controls/types';
|
|
3
3
|
import type { LeafPatch, ResolvedConfig } from './types';
|
|
4
4
|
|
|
@@ -53,9 +53,13 @@ function toLeaf(field: ConfigField): LeafPatch {
|
|
|
53
53
|
*
|
|
54
54
|
* Rules do not run here: a hand-written `ConfigField` already states its label
|
|
55
55
|
* and control, which is everything the rule chain would have inferred.
|
|
56
|
+
*
|
|
57
|
+
* A dotted `key` is a config path, as it is to every read and write, so its
|
|
58
|
+
* leaf is filed under headless groups at that path rather than as one child
|
|
59
|
+
* whose name contains dots — which no path lookup would find.
|
|
56
60
|
*/
|
|
57
61
|
export function fromConfigFields(fields: readonly ConfigField[]): ResolvedConfig {
|
|
58
|
-
const
|
|
62
|
+
const root: PrefGroup = { name: '', children: {} };
|
|
59
63
|
for (const field of fields) {
|
|
60
64
|
const patch = toLeaf(field);
|
|
61
65
|
for (const key of Object.keys(patch)) {
|
|
@@ -63,10 +67,20 @@ export function fromConfigFields(fields: readonly ConfigField[]): ResolvedConfig
|
|
|
63
67
|
delete (patch as Record<string, unknown>)[key];
|
|
64
68
|
}
|
|
65
69
|
}
|
|
66
|
-
|
|
70
|
+
const segments = field.key.split('.');
|
|
71
|
+
const leafKey = segments.pop() as string;
|
|
72
|
+
let group = root;
|
|
73
|
+
for (const segment of segments) {
|
|
74
|
+
const existing = group.children[segment];
|
|
75
|
+
if (existing === undefined || !('children' in existing) || 'kind' in existing) {
|
|
76
|
+
group.children[segment] = { name: '', children: {} };
|
|
77
|
+
}
|
|
78
|
+
group = group.children[segment] as PrefGroup;
|
|
79
|
+
}
|
|
80
|
+
group.children[leafKey] = { ...patch, default: field.default } as PrefLeaf;
|
|
67
81
|
}
|
|
68
82
|
return {
|
|
69
|
-
group:
|
|
83
|
+
group: root,
|
|
70
84
|
sections: [],
|
|
71
85
|
showIf: new Map(),
|
|
72
86
|
renderers: {},
|
package/src/config/path.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isPlainObject } from '@weasel-js/core';
|
|
1
2
|
import type { PrefGroup, PrefLeaf } from '@weasel-js/ui';
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -9,11 +10,7 @@ import type { PrefGroup, PrefLeaf } from '@weasel-js/ui';
|
|
|
9
10
|
/** A plain object a config tree can be walked into. An array, a `Date`, a
|
|
10
11
|
* `Map` or a class instance is a leaf value: a walk that descended into one
|
|
11
12
|
* would rebuild it as a bare `{}`. */
|
|
12
|
-
export
|
|
13
|
-
if (typeof value !== 'object' || value === null) return false;
|
|
14
|
-
const proto = Object.getPrototypeOf(value);
|
|
15
|
-
return proto === Object.prototype || proto === null;
|
|
16
|
-
}
|
|
13
|
+
export const isRecord = isPlainObject;
|
|
17
14
|
|
|
18
15
|
/** The value at a dotted path. `undefined` when a segment is missing or the
|
|
19
16
|
* walk hits something that is not a record. */
|
package/src/config/resolve.ts
CHANGED
|
@@ -27,6 +27,7 @@ interface Sink {
|
|
|
27
27
|
showIf: Map<string, (config: Record<string, unknown>) => boolean>;
|
|
28
28
|
renderers: Record<string, ControlRenderer>;
|
|
29
29
|
dialogs: Record<string, DialogSpec>;
|
|
30
|
+
resettable: Set<string>;
|
|
30
31
|
chain: readonly ConfigRule[];
|
|
31
32
|
}
|
|
32
33
|
|
|
@@ -48,6 +49,7 @@ export function resolveConfigSchema<TC>(
|
|
|
48
49
|
showIf: new Map(),
|
|
49
50
|
renderers: {},
|
|
50
51
|
dialogs: {},
|
|
52
|
+
resettable: new Set(),
|
|
51
53
|
chain: [...rules, ...builtinRules],
|
|
52
54
|
};
|
|
53
55
|
const group = resolveShape(schema.nodes, '', '', sink);
|
|
@@ -57,6 +59,7 @@ export function resolveConfigSchema<TC>(
|
|
|
57
59
|
showIf: sink.showIf,
|
|
58
60
|
renderers: sink.renderers,
|
|
59
61
|
dialogs: sink.dialogs,
|
|
62
|
+
resettable: sink.resettable,
|
|
60
63
|
};
|
|
61
64
|
}
|
|
62
65
|
|
|
@@ -89,6 +92,7 @@ function resolveShape(shape: ConfigShape, at: string, name: string, sink: Sink):
|
|
|
89
92
|
if (section.pack !== undefined && spec.pack === undefined) spec.pack = section.pack;
|
|
90
93
|
}
|
|
91
94
|
if (predicate) sink.showIf.set(path, predicate);
|
|
95
|
+
if (isConfigBranch(entry) && entry.options.resettable) sink.resettable.add(path);
|
|
92
96
|
if (!isConfigBranch(entry) && entry.options.render) sink.renderers[path] = entry.options.render;
|
|
93
97
|
if (!isConfigBranch(entry) && entry.options.dialog) sink.dialogs[path] = entry.options.dialog;
|
|
94
98
|
}
|
package/src/config/types.ts
CHANGED
|
@@ -1,10 +1,5 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
PrefLeaf,
|
|
4
|
-
PrefNumberFormat,
|
|
5
|
-
PrefNumberUnit,
|
|
6
|
-
PrefRenderer,
|
|
7
|
-
} from '@weasel-js/ui';
|
|
1
|
+
import type { Display } from '@weasel-js/quantity';
|
|
2
|
+
import type { PrefGroup, PrefLeaf, PrefNumberUnit, PrefRenderer } from '@weasel-js/ui';
|
|
8
3
|
import type { ReactNode } from 'react';
|
|
9
4
|
|
|
10
5
|
/**
|
|
@@ -18,6 +13,10 @@ export type ControlRenderer = PrefRenderer;
|
|
|
18
13
|
export interface ConfigOption {
|
|
19
14
|
value: string;
|
|
20
15
|
label: string;
|
|
16
|
+
/** A glyph in weasel-ui's icon set (`IconName`). A segmented row draws it in
|
|
17
|
+
* place of `label`, which stays the accessible name and becomes the tooltip;
|
|
18
|
+
* a select draws it beside `label`. */
|
|
19
|
+
icon?: string;
|
|
21
20
|
}
|
|
22
21
|
|
|
23
22
|
/** Everything a leaf can carry beyond its kind and default. A flat union of
|
|
@@ -32,8 +31,8 @@ export interface Annotations {
|
|
|
32
31
|
max?: number;
|
|
33
32
|
step?: number;
|
|
34
33
|
suffix?: string;
|
|
35
|
-
/** How a number's value
|
|
36
|
-
|
|
34
|
+
/** How a number's value shows, speaks and reads back — `compact()` reads `2.00M`. */
|
|
35
|
+
display?: Display;
|
|
37
36
|
/** Display-unit conversion for a number stored in a canonical unit — the
|
|
38
37
|
* value, its bounds and its step all convert at the control's edge. */
|
|
39
38
|
unit?: PrefNumberUnit;
|
|
@@ -107,6 +106,9 @@ export interface ResolvedConfig {
|
|
|
107
106
|
renderers: Readonly<Record<string, ControlRenderer>>;
|
|
108
107
|
/** Node-level `.dialog` rows, keyed by path. */
|
|
109
108
|
dialogs: Readonly<Record<string, DialogSpec>>;
|
|
109
|
+
/** Paths of the groups marked `.resettable()`, whose heading carries a
|
|
110
|
+
* button that puts every value beneath it back to its default. */
|
|
111
|
+
resettable?: ReadonlySet<string>;
|
|
110
112
|
}
|
|
111
113
|
|
|
112
114
|
/** Options for a row drawn as a button that opens a dialog. */
|
|
@@ -162,6 +164,9 @@ export interface BranchOptions {
|
|
|
162
164
|
/** Show this node only while the predicate holds. On a group it hides the
|
|
163
165
|
* whole subtree; the values stay in config either way. */
|
|
164
166
|
showIf?: (config: Record<string, unknown>) => boolean;
|
|
167
|
+
/** Put a button on this group's heading that returns every value beneath it
|
|
168
|
+
* to its default. Groups only. */
|
|
169
|
+
resettable?: boolean;
|
|
165
170
|
}
|
|
166
171
|
|
|
167
172
|
/** A group's own annotations: what it is called and, optionally, why. */
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ANGLE_RADIANS, prefUnit } from '@weasel-js/core';
|
|
2
2
|
import type { Meta, StoryObj } from '@weasel-js/forge';
|
|
3
|
+
import { compact } from '@weasel-js/quantity';
|
|
3
4
|
import { type PrefNumberUnit, PropertyRow } from '@weasel-js/ui';
|
|
4
5
|
import { useState } from 'react';
|
|
5
6
|
import { f } from '../config/builder';
|
|
@@ -278,7 +279,7 @@ export const Presentation: Story = {
|
|
|
278
279
|
|
|
279
280
|
const inline = f.schema({
|
|
280
281
|
folderPad: f.number(1.1).range(0, 4).step(0.1).label('Folder pad'),
|
|
281
|
-
glyphs: f.number(1_000_000).range(0, 2_000_000).
|
|
282
|
+
glyphs: f.number(1_000_000).range(0, 2_000_000).display(compact()),
|
|
282
283
|
names: f.number(60).range(0, 100),
|
|
283
284
|
labels: f.number(0).range(0, 100),
|
|
284
285
|
placement: f.enum('spread', ['spread', 'stack', 'ring']),
|
|
@@ -337,7 +338,7 @@ const sidebarSliders = f.schema({
|
|
|
337
338
|
pad: f.number(0.5).range(0, 4).step(0.25).label('Window pad'),
|
|
338
339
|
folderPad: f.number(1).range(0, 4).step(0.1).label('Folder pad'),
|
|
339
340
|
levels: f.number(2).range(0, 6).step(1).label('Levels'),
|
|
340
|
-
glyphs: f.number(1_000_000).range(0, 2_000_000).step(2000).
|
|
341
|
+
glyphs: f.number(1_000_000).range(0, 2_000_000).step(2000).display(compact()).label('Glyphs'),
|
|
341
342
|
names: f.number(40).range(0, 200).step(5).label('Names'),
|
|
342
343
|
labels: f.number(0).range(0, 30).step(1).label('Labels'),
|
|
343
344
|
text: f.number(100).range(50, 250).step(5).unit(shownIn('%')).label('Text'),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { act, fireEvent, render, screen, within } from '@testing-library/react';
|
|
2
2
|
import userEvent from '@testing-library/user-event';
|
|
3
3
|
import { TOOL_PREF_KINDS, type ToolPrefKind } from '@weasel-js/core';
|
|
4
|
+
import { compact } from '@weasel-js/quantity';
|
|
4
5
|
import type { PrefLeaf } from '@weasel-js/ui';
|
|
5
6
|
import { useState } from 'react';
|
|
6
7
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
@@ -88,7 +89,7 @@ describe('<ControlPanel> number', () => {
|
|
|
88
89
|
];
|
|
89
90
|
render(<ControlPanel fields={fields} config={{ n: 42 }} setConfig={vi.fn()} />);
|
|
90
91
|
const input = screen.getByLabelText('N') as HTMLInputElement;
|
|
91
|
-
expect(input
|
|
92
|
+
expect(input).toHaveAttribute('role', 'spinbutton');
|
|
92
93
|
expect(input.value).toBe('42');
|
|
93
94
|
});
|
|
94
95
|
|
|
@@ -143,7 +144,7 @@ describe('<ControlPanel> color', () => {
|
|
|
143
144
|
const input = screen.getByLabelText('Color') as HTMLInputElement;
|
|
144
145
|
expect(input.type).toBe('color');
|
|
145
146
|
expect(input.value).toBe('#ff0000');
|
|
146
|
-
fireEvent.
|
|
147
|
+
fireEvent.input(input, { target: { value: '#00ff00' } });
|
|
147
148
|
expect(setConfig).toHaveBeenCalledWith('c', '#00ff00');
|
|
148
149
|
});
|
|
149
150
|
});
|
|
@@ -206,7 +207,7 @@ describe('<ControlPanel> schema', () => {
|
|
|
206
207
|
[],
|
|
207
208
|
);
|
|
208
209
|
render(<ControlPanel schema={schema} config={{ a: 5, b: 5 }} setConfig={vi.fn()} />);
|
|
209
|
-
expect(screen.getByLabelText('B')).toHaveAttribute('
|
|
210
|
+
expect(screen.getByLabelText('B')).toHaveAttribute('role', 'spinbutton');
|
|
210
211
|
});
|
|
211
212
|
|
|
212
213
|
it('falls back to the leaf default when config holds no value', () => {
|
|
@@ -466,7 +467,7 @@ describe('<ControlPanel> suffix', () => {
|
|
|
466
467
|
|
|
467
468
|
it('suffixes a typed number with the suffix the leaf declares', () => {
|
|
468
469
|
const { container } = panel(f.number(20).input().suffix('px'));
|
|
469
|
-
expect(container.querySelector('input[
|
|
470
|
+
expect(container.querySelector('input[role="spinbutton"]')).not.toBeNull();
|
|
470
471
|
expect(container.textContent).toContain('px');
|
|
471
472
|
});
|
|
472
473
|
});
|
|
@@ -476,7 +477,7 @@ describe('<ControlPanel> format', () => {
|
|
|
476
477
|
render(
|
|
477
478
|
<ControlPanel
|
|
478
479
|
schema={resolveConfigSchema(
|
|
479
|
-
f.schema({ glyphs: f.number(0).range(0, 2_000_000).
|
|
480
|
+
f.schema({ glyphs: f.number(0).range(0, 2_000_000).display(compact()) }),
|
|
480
481
|
[],
|
|
481
482
|
)}
|
|
482
483
|
config={{ glyphs: 2_000_000 }}
|
|
@@ -898,6 +899,52 @@ describe('<ControlPanel> auto', () => {
|
|
|
898
899
|
});
|
|
899
900
|
});
|
|
900
901
|
|
|
902
|
+
describe('<ControlPanel> option icons', () => {
|
|
903
|
+
it("draws an option's icon in its segment and keeps the label as its name", () => {
|
|
904
|
+
const status = f
|
|
905
|
+
.enum('ok', [
|
|
906
|
+
{ value: 'ok', label: 'ok', icon: 'statusSuccess' },
|
|
907
|
+
{ value: 'bad', label: 'bad' },
|
|
908
|
+
])
|
|
909
|
+
.radio();
|
|
910
|
+
render(
|
|
911
|
+
<ControlPanel
|
|
912
|
+
schema={resolveConfigSchema(f.schema({ status }), [])}
|
|
913
|
+
config={{ status: 'ok' }}
|
|
914
|
+
setConfig={() => {}}
|
|
915
|
+
/>,
|
|
916
|
+
);
|
|
917
|
+
const ok = screen.getByRole('radio', { name: 'ok' });
|
|
918
|
+
expect(ok.querySelector('svg')).not.toBeNull();
|
|
919
|
+
expect(ok.textContent).toBe('');
|
|
920
|
+
expect(screen.getByRole('radio', { name: 'bad' }).textContent).toBe('bad');
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
it("draws an option's icon beside its label in a select's trigger and list", () => {
|
|
924
|
+
const status = f.enum('ok', [
|
|
925
|
+
{ value: 'ok', label: 'ok', icon: 'statusSuccess' },
|
|
926
|
+
{ value: 'bad', label: 'bad' },
|
|
927
|
+
]);
|
|
928
|
+
render(
|
|
929
|
+
<ControlPanel
|
|
930
|
+
schema={resolveConfigSchema(f.schema({ status }), [])}
|
|
931
|
+
config={{ status: 'ok' }}
|
|
932
|
+
setConfig={() => {}}
|
|
933
|
+
/>,
|
|
934
|
+
);
|
|
935
|
+
const trigger = screen.getByRole('button', { name: /ok/ });
|
|
936
|
+
expect(trigger.querySelector('[aria-hidden="true"] > svg')).not.toBeNull();
|
|
937
|
+
expect(trigger).toHaveTextContent('ok');
|
|
938
|
+
act(() => {
|
|
939
|
+
fireEvent.click(trigger);
|
|
940
|
+
});
|
|
941
|
+
expect(
|
|
942
|
+
screen.getByRole('option', { name: 'ok' }).querySelector('[aria-hidden="true"] > svg'),
|
|
943
|
+
).not.toBeNull();
|
|
944
|
+
expect(screen.getByRole('option', { name: 'bad' }).querySelectorAll('svg')).toHaveLength(1);
|
|
945
|
+
});
|
|
946
|
+
});
|
|
947
|
+
|
|
901
948
|
describe('<ControlPanel> pair', () => {
|
|
902
949
|
const paired = () =>
|
|
903
950
|
resolveConfigSchema(
|
|
@@ -913,8 +960,8 @@ describe('<ControlPanel> pair', () => {
|
|
|
913
960
|
it('draws two leaves sharing a pair id as one row named by the pair', () => {
|
|
914
961
|
render(<ControlPanel schema={paired()} config={config} setConfig={vi.fn()} />);
|
|
915
962
|
const row = screen.getByText('Offset').closest('div') as HTMLElement;
|
|
916
|
-
expect(within(row).getByLabelText('X')).toHaveValue(10);
|
|
917
|
-
expect(within(row).getByLabelText('Y')).toHaveValue(20);
|
|
963
|
+
expect(within(row).getByLabelText('X')).toHaveValue('10');
|
|
964
|
+
expect(within(row).getByLabelText('Y')).toHaveValue('20');
|
|
918
965
|
});
|
|
919
966
|
|
|
920
967
|
it('captions each cell of a pair with its own name, so a paired switch is not left unnamed', () => {
|
|
@@ -952,7 +999,7 @@ describe('<ControlPanel> pair', () => {
|
|
|
952
999
|
render(<ControlPanel schema={paired()} config={config} setConfig={vi.fn()} />);
|
|
953
1000
|
const row = screen.getByText('Offset').closest('div') as HTMLElement;
|
|
954
1001
|
expect(within(row).queryByLabelText('Scale')).toBeNull();
|
|
955
|
-
expect(screen.getByLabelText('Scale')).toHaveValue(1);
|
|
1002
|
+
expect(screen.getByLabelText('Scale')).toHaveValue('1');
|
|
956
1003
|
});
|
|
957
1004
|
});
|
|
958
1005
|
|
|
@@ -1045,18 +1092,18 @@ describe('<ControlPanel> color alpha', () => {
|
|
|
1045
1092
|
it('splits the stored hex into a swatch and an alpha track', () => {
|
|
1046
1093
|
panel();
|
|
1047
1094
|
expect((screen.getByLabelText('Tint') as HTMLInputElement).value).toBe('#336699');
|
|
1048
|
-
expect((screen.getByLabelText('Tint opacity') as HTMLInputElement).value).toBe('
|
|
1095
|
+
expect((screen.getByLabelText('Tint opacity') as HTMLInputElement).value).toBe('80');
|
|
1049
1096
|
});
|
|
1050
1097
|
|
|
1051
1098
|
it('keeps the stored alpha when the swatch moves', () => {
|
|
1052
1099
|
const setConfig = panel();
|
|
1053
|
-
fireEvent.
|
|
1100
|
+
fireEvent.input(screen.getByLabelText('Tint'), { target: { value: '#00ff00' } });
|
|
1054
1101
|
expect(setConfig).toHaveBeenCalledWith('tint', '#00ff00cc');
|
|
1055
1102
|
});
|
|
1056
1103
|
|
|
1057
1104
|
it('keeps the stored color when the alpha track moves', () => {
|
|
1058
1105
|
const setConfig = panel();
|
|
1059
|
-
fireEvent.
|
|
1106
|
+
fireEvent.input(screen.getByLabelText('Tint opacity'), { target: { value: '50' } });
|
|
1060
1107
|
expect(setConfig).toHaveBeenCalledWith('tint', '#33669980');
|
|
1061
1108
|
});
|
|
1062
1109
|
|
|
@@ -1101,3 +1148,43 @@ describe('<ControlPanel> title, stance and tone', () => {
|
|
|
1101
1148
|
expect(panel.querySelector('.lk-control-panel')).not.toBeNull();
|
|
1102
1149
|
});
|
|
1103
1150
|
});
|
|
1151
|
+
|
|
1152
|
+
describe('<ControlPanel> resettable group', () => {
|
|
1153
|
+
const schema = resolveConfigSchema(
|
|
1154
|
+
f.schema({
|
|
1155
|
+
name: f.string('a'),
|
|
1156
|
+
modes: f
|
|
1157
|
+
.group({ one: f.number(0), two: f.number(0), inner: f.group({ three: f.number(1) }) })
|
|
1158
|
+
.resettable(),
|
|
1159
|
+
plain: f.group({ four: f.number(0) }),
|
|
1160
|
+
}),
|
|
1161
|
+
);
|
|
1162
|
+
|
|
1163
|
+
it('puts a reset button on the group heading, disabled while nothing has changed', () => {
|
|
1164
|
+
render(
|
|
1165
|
+
<ControlPanel
|
|
1166
|
+
schema={schema}
|
|
1167
|
+
config={{ name: 'a', modes: { one: 0, two: 0, inner: { three: 1 } }, plain: { four: 0 } }}
|
|
1168
|
+
setConfig={vi.fn()}
|
|
1169
|
+
/>,
|
|
1170
|
+
);
|
|
1171
|
+
expect(screen.getByRole('button', { name: 'Reset Modes' })).toBeDisabled();
|
|
1172
|
+
expect(screen.queryByRole('button', { name: 'Reset Plain' })).toBeNull();
|
|
1173
|
+
});
|
|
1174
|
+
|
|
1175
|
+
it('writes the default of every changed value beneath the group and nothing else', () => {
|
|
1176
|
+
const setConfig = vi.fn();
|
|
1177
|
+
render(
|
|
1178
|
+
<ControlPanel
|
|
1179
|
+
schema={schema}
|
|
1180
|
+
config={{ name: 'b', modes: { one: 2, two: 0, inner: { three: 5 } }, plain: { four: 3 } }}
|
|
1181
|
+
setConfig={setConfig}
|
|
1182
|
+
/>,
|
|
1183
|
+
);
|
|
1184
|
+
fireEvent.click(screen.getByRole('button', { name: 'Reset Modes' }));
|
|
1185
|
+
expect(setConfig.mock.calls).toEqual([
|
|
1186
|
+
['modes.one', 0],
|
|
1187
|
+
['modes.inner.three', 1],
|
|
1188
|
+
]);
|
|
1189
|
+
});
|
|
1190
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getAlpha01, isBuiltinToolPref, toHex8, withAlpha01 } from '@weasel-js/core';
|
|
2
2
|
import {
|
|
3
|
+
Button,
|
|
3
4
|
DialogRow,
|
|
4
5
|
isPrefLeaf,
|
|
5
6
|
ListEditor,
|
|
@@ -17,6 +18,7 @@ import {
|
|
|
17
18
|
PropertyRow,
|
|
18
19
|
type PropertyRowLayout,
|
|
19
20
|
prefFieldProps,
|
|
21
|
+
ResetIcon,
|
|
20
22
|
type StanceProps,
|
|
21
23
|
} from '@weasel-js/ui';
|
|
22
24
|
import { Fragment, type ReactNode, useEffect, useMemo, useRef, useState } from 'react';
|
|
@@ -117,6 +119,49 @@ export interface ControlPanelProps<TC extends Record<string, unknown>> extends S
|
|
|
117
119
|
|
|
118
120
|
const NO_AUTO: ReadonlySet<string> = new Set();
|
|
119
121
|
|
|
122
|
+
/** Every leaf beneath a group, as its dotted path and its default. */
|
|
123
|
+
function leavesUnder(group: PrefGroup, at: string): [string, unknown][] {
|
|
124
|
+
return Object.entries(group.children).flatMap(([key, child]) => {
|
|
125
|
+
const path = at === '' ? key : `${at}.${key}`;
|
|
126
|
+
return isPrefLeaf(child)
|
|
127
|
+
? [[path, child.default] as [string, unknown]]
|
|
128
|
+
: leavesUnder(child, path);
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** The heading button of a `.resettable()` group: writes each changed leaf's
|
|
133
|
+
* default, and is disabled while nothing has changed. */
|
|
134
|
+
function ResetGroup({
|
|
135
|
+
group,
|
|
136
|
+
path,
|
|
137
|
+
config,
|
|
138
|
+
setConfig,
|
|
139
|
+
}: {
|
|
140
|
+
group: PrefGroup;
|
|
141
|
+
path: string;
|
|
142
|
+
config: Record<string, unknown>;
|
|
143
|
+
setConfig: (path: string, value: unknown) => void;
|
|
144
|
+
}) {
|
|
145
|
+
const changed = leavesUnder(group, path).filter(
|
|
146
|
+
([p, d]) => !Object.is(valueAtPath(config, p), d),
|
|
147
|
+
);
|
|
148
|
+
return (
|
|
149
|
+
<Button
|
|
150
|
+
iconOnly
|
|
151
|
+
variant="ghost"
|
|
152
|
+
size="sm"
|
|
153
|
+
ariaLabel={`Reset ${group.name}`}
|
|
154
|
+
tooltip={`Reset ${group.name} to defaults`}
|
|
155
|
+
disabled={changed.length === 0}
|
|
156
|
+
onClick={() => {
|
|
157
|
+
for (const [p, d] of changed) setConfig(p, d);
|
|
158
|
+
}}
|
|
159
|
+
>
|
|
160
|
+
<ResetIcon />
|
|
161
|
+
</Button>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
120
165
|
/** Render an instrument's config schema as a stack of controls, each writing
|
|
121
166
|
* back through `setConfig`. Built on the property rows, so a lab's controls
|
|
122
167
|
* are the same aligned, themed rows the rest of the kit uses. */
|
|
@@ -224,6 +269,16 @@ export function ControlPanel<TC extends Record<string, unknown>>({
|
|
|
224
269
|
// two-column grid leaves it laying its own rows out inside a cell,
|
|
225
270
|
// which overlaps them.
|
|
226
271
|
span
|
|
272
|
+
actions={
|
|
273
|
+
resolved.resettable?.has(path) ? (
|
|
274
|
+
<ResetGroup
|
|
275
|
+
group={found}
|
|
276
|
+
path={path}
|
|
277
|
+
config={config as Record<string, unknown>}
|
|
278
|
+
setConfig={setConfig}
|
|
279
|
+
/>
|
|
280
|
+
) : undefined
|
|
281
|
+
}
|
|
227
282
|
{...fold(path, undefined, rows.grid)}
|
|
228
283
|
>
|
|
229
284
|
{body(found, path, rows)}
|
|
@@ -461,7 +516,7 @@ function ControlRow<TC extends Record<string, unknown>>({
|
|
|
461
516
|
case 'number':
|
|
462
517
|
return <PropertyField {...field} {...row} span={field.control === 'slider' && wide} />;
|
|
463
518
|
case 'enum':
|
|
464
|
-
return <PropertyField {...field} {...row} span={field.control === '
|
|
519
|
+
return <PropertyField {...field} {...row} span={field.control === 'toggle' && wide} />;
|
|
465
520
|
default:
|
|
466
521
|
return <PropertyField {...field} {...row} />;
|
|
467
522
|
}
|
|
@@ -496,7 +551,11 @@ function labField(
|
|
|
496
551
|
control: extra<string>(leaf, 'control') === 'switch' ? 'switch' : 'checkbox',
|
|
497
552
|
};
|
|
498
553
|
case 'enum':
|
|
499
|
-
|
|
554
|
+
// `.radio()` asks for every option at once, as segments.
|
|
555
|
+
return {
|
|
556
|
+
...field,
|
|
557
|
+
control: extra<string>(leaf, 'control') === 'radio' ? 'toggle' : 'select',
|
|
558
|
+
};
|
|
500
559
|
case 'string':
|
|
501
560
|
return {
|
|
502
561
|
...field,
|