@weasel-js/labkit 1.6.1 → 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-Wo6OwIVM.d.ts → CanvasStackContext-D2jnIwfk.d.ts} +1 -1
- package/dist/_dts/{LayerList.d-BoELAMsM.d.ts → LayerList.d-DiuEArKq.d.ts} +2 -2
- 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-Cr8-zoup.d.ts → Select.d-Bensr_MT.d.ts} +11 -1
- package/dist/_dts/{Stage-DWBBi8IU.d.ts → Stage-CCWfQQWM.d.ts} +58 -18
- package/dist/_dts/index-B2Yj5aA0.d.ts +274 -0
- package/dist/_dts/{index-CKyX0ME_.d.ts → index-CsYqUHD3.d.ts} +2 -2
- package/dist/_dts/{types-DTuViBvc.d.ts → types-CRPSGFAp.d.ts} +13 -4
- package/dist/_dts/{types-B7yBTTF9.d.ts → types-CSm9jgol.d.ts} +2 -2
- package/dist/_dts/{useTrialState-CMaV6UD4.d.ts → useTrialState-CTZPgeZ7.d.ts} +1 -1
- package/dist/canvas/index.d.ts +6 -6
- package/dist/canvas/index.js +2 -2
- package/dist/chrome/index.d.ts +5 -5
- package/dist/chrome/index.js +8 -8
- package/dist/{chunk-QGMOA2YE.js → chunk-275JRRVD.js} +67 -70
- package/dist/chunk-275JRRVD.js.map +1 -0
- package/dist/{chunk-KHK5FGER.js → chunk-4LJSC7MB.js} +8 -10
- package/dist/chunk-4LJSC7MB.js.map +1 -0
- package/dist/{chunk-SLAQPUI4.js → chunk-7BO7HNGF.js} +5 -5
- package/dist/{chunk-SLAQPUI4.js.map → chunk-7BO7HNGF.js.map} +1 -1
- package/dist/chunk-CZZPXUOU.js +3 -0
- package/dist/{chunk-SBXNQW4G.js.map → chunk-CZZPXUOU.js.map} +1 -1
- package/dist/{chunk-6FLXL3I7.js → chunk-FMK7UQ6V.js} +19 -4
- package/dist/chunk-FMK7UQ6V.js.map +1 -0
- package/dist/{chunk-O6FFFOHS.js → chunk-FZ7YFQN7.js} +16 -8
- package/dist/chunk-FZ7YFQN7.js.map +1 -0
- package/dist/{chunk-GHGO52SO.js → chunk-GOZLNM4L.js} +5 -5
- package/dist/{chunk-GHGO52SO.js.map → chunk-GOZLNM4L.js.map} +1 -1
- package/dist/{chunk-4TT3PNPR.js → chunk-I5DUQ5HM.js} +33 -36
- package/dist/chunk-I5DUQ5HM.js.map +1 -0
- package/dist/{chunk-NEXR37YJ.js → chunk-J7OWWTCW.js} +4 -5
- package/dist/chunk-J7OWWTCW.js.map +1 -0
- package/dist/{chunk-IRTGUQ5O.js → chunk-KMJ7RWD3.js} +16 -6
- package/dist/chunk-KMJ7RWD3.js.map +1 -0
- package/dist/{chunk-S4NCI32V.js → chunk-M7HP3FTK.js} +4 -4
- package/dist/{chunk-S4NCI32V.js.map → chunk-M7HP3FTK.js.map} +1 -1
- package/dist/chunk-MIM5R4ZT.js +14972 -0
- package/dist/chunk-MIM5R4ZT.js.map +1 -0
- package/dist/{chunk-ASOK76ZH.js → chunk-T5IBSPAM.js} +59 -26
- package/dist/chunk-T5IBSPAM.js.map +1 -0
- package/dist/{chunk-DWMSB4CA.js → chunk-UNAZSLWE.js} +82 -16
- package/dist/chunk-UNAZSLWE.js.map +1 -0
- package/dist/{chunk-TJ77A4A3.js → chunk-Y23G2VZL.js} +5 -9
- package/dist/chunk-Y23G2VZL.js.map +1 -0
- package/dist/{chunk-HKV7LRHY.js → chunk-YT75VYRD.js} +4 -4
- package/dist/{chunk-HKV7LRHY.js.map → chunk-YT75VYRD.js.map} +1 -1
- package/dist/{chunk-CGHF6RZV.js → chunk-ZIFPHIWZ.js} +9 -7
- package/dist/chunk-ZIFPHIWZ.js.map +1 -0
- package/dist/config/index.d.ts +16 -9
- package/dist/config/index.js +4 -4
- package/dist/controls/index.d.ts +2 -2
- package/dist/controls/index.js +5 -5
- package/dist/dragdrop/index.d.ts +3 -3
- package/dist/index.d.ts +263 -24
- package/dist/index.js +376 -237
- 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 +4 -4
- package/dist/loupe/index.js +2 -2
- package/dist/overview/index.d.ts +2 -2
- package/dist/overview/index.js +9 -8
- package/dist/overview/index.js.map +1 -1
- package/dist/passthrough/weasel-ui.d.ts +208 -38
- package/dist/passthrough/weasel-ui.js +3 -3
- package/dist/primitives/index.d.ts +3 -178
- package/dist/primitives/index.js +6 -6
- package/dist/state/index.d.ts +5 -5
- package/dist/state/index.js +4 -4
- package/dist/styles.css +91 -83
- package/dist/undo/index.d.ts +3 -3
- package/package.json +13 -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/paint.test.ts +3 -14
- package/src/annotations/paint.ts +3 -19
- package/src/annotations/svgNodes.test.ts +6 -9
- package/src/annotations/svgNodes.ts +3 -3
- package/src/canvas/AGENTS.md +1 -0
- package/src/canvas/CameraInput.tsx +44 -33
- package/src/canvas/CanvasStack.tsx +6 -1
- package/src/canvas/LinkedCursor.less +1 -1
- package/src/canvas/Stage.test.tsx +33 -4
- package/src/canvas/Stage.tsx +6 -1
- package/src/canvas/cameraRegistry.test.ts +55 -0
- package/src/canvas/cameraRegistry.ts +86 -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/chrome/ChromeRegions.stories.tsx +1 -0
- package/src/chrome/regions/PaletteRegion.less +0 -7
- package/src/chrome/regions/SidebarRegion.less +12 -2
- package/src/config/builder.test.ts +2 -1
- package/src/config/builder.ts +12 -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 +13 -9
- package/src/controls/ControlPanel.less +1 -1
- package/src/controls/ControlPanel.stories.tsx +3 -2
- package/src/controls/ControlPanel.test.tsx +64 -11
- package/src/controls/ControlPanel.tsx +52 -2
- package/src/index.ts +15 -1
- package/src/lab/Lab.tsx +83 -72
- 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/LoupeGestures.tsx +1 -1
- package/src/overview/TrialOverview.tsx +7 -5
- package/src/passthrough/weasel-ui.test.ts +2 -0
- package/src/passthrough/weasel-ui.ts +16 -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 +2 -2
- package/src/state/Persistence.browser.test.tsx +47 -0
- package/src/state/store.test.ts +5 -1
- package/src/state/usePersistedState.test.tsx +1 -1
- package/src/styles.less +1 -0
- 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.less +0 -13
- package/src/trial/Trial.tsx +12 -6
- package/src/trial/TrialChrome.tsx +21 -3
- package/dist/_dts/PrefsRow.d-DFpPIeIC.d.ts +0 -39
- package/dist/_dts/PropertyField.d-D0ndfIIt.d.ts +0 -404
- 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-DWMSB4CA.js.map +0 -1
- package/dist/chunk-IRTGUQ5O.js.map +0 -1
- 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-SBXNQW4G.js +0 -3
- package/dist/chunk-TJ77A4A3.js.map +0 -1
- package/dist/chunk-TYN6MLXX.js +0 -13864
- package/dist/chunk-TYN6MLXX.js.map +0 -1
- package/src/state/Persistence.stories.tsx +0 -75
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { useActionsRegistry } from '@weasel-js/core';
|
|
2
|
+
import { createContext, useContext, useEffect } from 'react';
|
|
3
|
+
import type { CameraView } from './CameraInput';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Cameras by key, for chrome that acts on a camera it does not contain — the
|
|
7
|
+
* lab's zoom controls, which drive whichever trial has the focus.
|
|
8
|
+
*/
|
|
9
|
+
export interface CameraRegistry {
|
|
10
|
+
/** Publish `camera` under `key` and return its release. Registrants for one
|
|
11
|
+
* key stack, newest live. */
|
|
12
|
+
register(key: string, camera: CameraView): () => void;
|
|
13
|
+
get(key: string): CameraView | null;
|
|
14
|
+
/** Fires after any register or release. Shaped for `useSyncExternalStore`. */
|
|
15
|
+
subscribe(listener: () => void): () => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function createCameraRegistry(): CameraRegistry {
|
|
19
|
+
const stacks = new Map<string, CameraView[]>();
|
|
20
|
+
const listeners = new Set<() => void>();
|
|
21
|
+
const notify = (): void => {
|
|
22
|
+
for (const l of listeners) l();
|
|
23
|
+
};
|
|
24
|
+
return {
|
|
25
|
+
register(key, camera) {
|
|
26
|
+
const stack = stacks.get(key) ?? [];
|
|
27
|
+
stack.push(camera);
|
|
28
|
+
stacks.set(key, stack);
|
|
29
|
+
notify();
|
|
30
|
+
let released = false;
|
|
31
|
+
return () => {
|
|
32
|
+
if (released) return;
|
|
33
|
+
released = true;
|
|
34
|
+
const cur = stacks.get(key);
|
|
35
|
+
const i = cur?.lastIndexOf(camera) ?? -1;
|
|
36
|
+
if (!cur || i === -1) return;
|
|
37
|
+
cur.splice(i, 1);
|
|
38
|
+
if (cur.length === 0) stacks.delete(key);
|
|
39
|
+
notify();
|
|
40
|
+
};
|
|
41
|
+
},
|
|
42
|
+
get: (key) => stacks.get(key)?.at(-1) ?? null,
|
|
43
|
+
subscribe(listener) {
|
|
44
|
+
listeners.add(listener);
|
|
45
|
+
return () => {
|
|
46
|
+
listeners.delete(listener);
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The registry a lab keeps its trials' cameras in. */
|
|
53
|
+
export const CameraRegistryContext = createContext<CameraRegistry | null>(null);
|
|
54
|
+
|
|
55
|
+
/** Where a camera publishes itself. The host binds the key — a trial provides
|
|
56
|
+
* one that registers under its own id — so the camera needs no name. */
|
|
57
|
+
export const CameraPublishContext = createContext<((camera: CameraView) => () => void) | null>(
|
|
58
|
+
null,
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
/** Publish `camera` to the host in scope for as long as the caller is mounted,
|
|
62
|
+
* and again each time its input scope becomes the active one — a registry
|
|
63
|
+
* key stacks newest-live, so the camera last used is the one chrome reaches.
|
|
64
|
+
* Call it inside the camera's scope. */
|
|
65
|
+
export function usePublishCamera(camera: CameraView): void {
|
|
66
|
+
const publish = useContext(CameraPublishContext);
|
|
67
|
+
const actions = useActionsRegistry();
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (!publish) return;
|
|
70
|
+
let release = publish(camera);
|
|
71
|
+
if (!actions) return release;
|
|
72
|
+
let was = actions.isActive();
|
|
73
|
+
const off = actions.subscribe(() => {
|
|
74
|
+
const now = actions.isActive();
|
|
75
|
+
if (now && !was) {
|
|
76
|
+
release();
|
|
77
|
+
release = publish(camera);
|
|
78
|
+
}
|
|
79
|
+
was = now;
|
|
80
|
+
});
|
|
81
|
+
return () => {
|
|
82
|
+
off();
|
|
83
|
+
release();
|
|
84
|
+
};
|
|
85
|
+
}, [publish, camera, actions]);
|
|
86
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { CameraView } from './CameraInput';
|
|
3
|
+
import { zoomCameraTo } from './cameraZoom';
|
|
4
|
+
|
|
5
|
+
function fakeCamera(
|
|
6
|
+
host: { width: number; height: number } | null,
|
|
7
|
+
range = { min: 0.1, max: 32 },
|
|
8
|
+
): CameraView & {
|
|
9
|
+
view: { x: number; y: number; scale: { x: number; y: number } };
|
|
10
|
+
} {
|
|
11
|
+
const cam = {
|
|
12
|
+
view: { x: 0, y: 0, scale: { x: 1, y: 1 } },
|
|
13
|
+
get: () => cam.view,
|
|
14
|
+
set: (v: { x: number; y: number; scale: { x: number; y: number } }) => {
|
|
15
|
+
const s = Math.min(range.max, Math.max(range.min, v.scale.x));
|
|
16
|
+
cam.view = { ...v, scale: { x: s, y: s } };
|
|
17
|
+
},
|
|
18
|
+
hostSize: () => host,
|
|
19
|
+
zoomRange: () => range,
|
|
20
|
+
};
|
|
21
|
+
return cam;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe('zoomCameraTo', () => {
|
|
25
|
+
it('keeps the middle of the view where it was', () => {
|
|
26
|
+
const cam = fakeCamera({ width: 200, height: 100 });
|
|
27
|
+
zoomCameraTo(cam, 2);
|
|
28
|
+
// The world point under the screen middle (100, 50) is still under it.
|
|
29
|
+
expect(cam.view.x + 100 / cam.view.scale.x).toBeCloseTo(100);
|
|
30
|
+
expect(cam.view.y + 50 / cam.view.scale.y).toBeCloseTo(50);
|
|
31
|
+
expect(cam.view.scale.x).toBe(2);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('anchors at the origin when the camera cannot measure its host', () => {
|
|
35
|
+
const cam = fakeCamera(null);
|
|
36
|
+
zoomCameraTo(cam, 2);
|
|
37
|
+
expect(cam.view).toEqual({ x: 0, y: 0, scale: { x: 2, y: 2 } });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('stops at the edge of the camera range', () => {
|
|
41
|
+
const cam = fakeCamera({ width: 200, height: 100 }, { min: 0.5, max: 1.2 });
|
|
42
|
+
zoomCameraTo(cam, 5);
|
|
43
|
+
expect(cam.view.scale.x).toBe(1.2);
|
|
44
|
+
expect(cam.view.x + 100 / cam.view.scale.x).toBeCloseTo(100);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { View } from '@weasel-js/core';
|
|
2
|
+
import type { CameraView } from './CameraInput';
|
|
3
|
+
|
|
4
|
+
/** `view` at `scale`, about the screen point `at`. */
|
|
5
|
+
export function atScale(view: View, scale: number, at: { x: number; y: number }): View {
|
|
6
|
+
return {
|
|
7
|
+
x: view.x + at.x / view.scale.x - at.x / scale,
|
|
8
|
+
y: view.y + at.y / view.scale.y - at.y / scale,
|
|
9
|
+
scale: { x: scale, y: scale },
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** The middle of the camera's host, in its own screen space; the origin when it cannot measure. */
|
|
14
|
+
export function viewMiddle(camera: CameraView): { x: number; y: number } {
|
|
15
|
+
const size = camera.hostSize?.();
|
|
16
|
+
return size ? { x: size.width / 2, y: size.height / 2 } : { x: 0, y: 0 };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Zoom `camera` to `zoom` about the middle of its view, within its range. */
|
|
20
|
+
export function zoomCameraTo(camera: CameraView, zoom: number): void {
|
|
21
|
+
const { min, max } = camera.zoomRange();
|
|
22
|
+
const scale = Math.min(max, Math.max(min, zoom));
|
|
23
|
+
camera.set(atScale(camera.get(), scale, viewMiddle(camera)));
|
|
24
|
+
}
|
package/src/canvas/index.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
export type {
|
|
1
|
+
export type {
|
|
2
|
+
CameraContextValue,
|
|
3
|
+
CameraInputProps,
|
|
4
|
+
CameraView,
|
|
5
|
+
CameraViewOptions,
|
|
6
|
+
ZoomRange,
|
|
7
|
+
} from './CameraInput';
|
|
2
8
|
export {
|
|
3
9
|
CameraContext,
|
|
4
10
|
CameraInput,
|
|
5
11
|
CameraScope,
|
|
6
|
-
CameraScopeContext,
|
|
7
12
|
STAGE_VIEW_ID,
|
|
8
13
|
useCameraView,
|
|
9
14
|
} from './CameraInput';
|
|
@@ -15,6 +20,13 @@ export type { CanvasStackContextValue, CanvasStackSurface } from './CanvasStackC
|
|
|
15
20
|
export { CanvasStackContext } from './CanvasStackContext';
|
|
16
21
|
export type { ZoomAtOptions } from './camera';
|
|
17
22
|
export { centerOn, zoomAt } from './camera';
|
|
23
|
+
export type { CameraRegistry } from './cameraRegistry';
|
|
24
|
+
export {
|
|
25
|
+
CameraPublishContext,
|
|
26
|
+
CameraRegistryContext,
|
|
27
|
+
createCameraRegistry,
|
|
28
|
+
usePublishCamera,
|
|
29
|
+
} from './cameraRegistry';
|
|
18
30
|
export { clampZoomAbout, frameLocalToWorld, fromCameraView, toCameraView } from './cameraView';
|
|
19
31
|
export { screenToWorld, worldToScreen } from './canvasCoords';
|
|
20
32
|
export { LinkedCursor } from './LinkedCursor';
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Two cameras under one host scope — what a trial gives every canvas inside it,
|
|
3
|
+
* and what a forge index page puts several of in. Each must answer only for
|
|
4
|
+
* itself: the newest must not take the others' gestures.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { act, render } from '@testing-library/react';
|
|
8
|
+
import { WeaselProvider } from '@weasel-js/core';
|
|
9
|
+
import { beforeAll, describe, expect, it, vi } from 'vitest';
|
|
10
|
+
import type { ViewTransform } from '../instrument/types';
|
|
11
|
+
import { CanvasStack } from './CanvasStack';
|
|
12
|
+
|
|
13
|
+
beforeAll(() => {
|
|
14
|
+
HTMLCanvasElement.prototype.getContext = vi.fn(
|
|
15
|
+
() => null,
|
|
16
|
+
) as unknown as HTMLCanvasElement['getContext'];
|
|
17
|
+
const proto = HTMLElement.prototype as unknown as Record<string, unknown>;
|
|
18
|
+
proto.setPointerCapture = vi.fn();
|
|
19
|
+
proto.releasePointerCapture = vi.fn();
|
|
20
|
+
proto.hasPointerCapture = vi.fn(() => true);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
function pointer(el: Element, type: string, x: number, y: number, buttons = 1) {
|
|
24
|
+
el.dispatchEvent(
|
|
25
|
+
new PointerEvent(type, {
|
|
26
|
+
bubbles: true,
|
|
27
|
+
cancelable: true,
|
|
28
|
+
pointerId: 1,
|
|
29
|
+
clientX: x,
|
|
30
|
+
clientY: y,
|
|
31
|
+
button: 0,
|
|
32
|
+
buttons,
|
|
33
|
+
}),
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const VIEW: ViewTransform = { zoom: 1, pan: { x: 0, y: 0 } };
|
|
38
|
+
|
|
39
|
+
function mountTwo() {
|
|
40
|
+
const views = [vi.fn(), vi.fn()];
|
|
41
|
+
const taps = [vi.fn(), vi.fn()];
|
|
42
|
+
const { container } = render(
|
|
43
|
+
<WeaselProvider isolate>
|
|
44
|
+
{(['a', 'b'] as const).map((id, i) => (
|
|
45
|
+
<CanvasStack
|
|
46
|
+
key={id}
|
|
47
|
+
layers={[]}
|
|
48
|
+
view={VIEW}
|
|
49
|
+
onViewChange={views[i]}
|
|
50
|
+
onHitTest={taps[i]}
|
|
51
|
+
width={100}
|
|
52
|
+
height={100}
|
|
53
|
+
/>
|
|
54
|
+
))}
|
|
55
|
+
</WeaselProvider>,
|
|
56
|
+
);
|
|
57
|
+
const hosts = [...container.querySelectorAll<HTMLElement>('.lk-canvas-stack')];
|
|
58
|
+
return { hosts, views, taps };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
describe('cameras sharing a host scope', () => {
|
|
62
|
+
it('pans the camera the drag began on, not the newest', () => {
|
|
63
|
+
const { hosts, views } = mountTwo();
|
|
64
|
+
act(() => {
|
|
65
|
+
pointer(hosts[0]!, 'pointerdown', 50, 50);
|
|
66
|
+
pointer(hosts[0]!, 'pointermove', 60, 55);
|
|
67
|
+
pointer(hosts[0]!, 'pointermove', 80, 70);
|
|
68
|
+
pointer(hosts[0]!, 'pointerup', 80, 70, 0);
|
|
69
|
+
});
|
|
70
|
+
expect(views[0]).toHaveBeenCalled();
|
|
71
|
+
expect(views[1]).not.toHaveBeenCalled();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('reports a tap to the canvas it landed on', () => {
|
|
75
|
+
const { hosts, taps } = mountTwo();
|
|
76
|
+
act(() => {
|
|
77
|
+
pointer(hosts[0]!, 'pointerdown', 50, 50);
|
|
78
|
+
pointer(hosts[0]!, 'pointerup', 50, 50, 0);
|
|
79
|
+
});
|
|
80
|
+
expect(taps[0]).toHaveBeenCalledTimes(1);
|
|
81
|
+
expect(taps[1]).not.toHaveBeenCalled();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('still drives the newest camera from its own canvas', () => {
|
|
85
|
+
const { hosts, views } = mountTwo();
|
|
86
|
+
act(() => {
|
|
87
|
+
pointer(hosts[1]!, 'pointerdown', 50, 50);
|
|
88
|
+
pointer(hosts[1]!, 'pointermove', 80, 70);
|
|
89
|
+
pointer(hosts[1]!, 'pointerup', 80, 70, 0);
|
|
90
|
+
});
|
|
91
|
+
expect(views[1]).toHaveBeenCalled();
|
|
92
|
+
expect(views[0]).not.toHaveBeenCalled();
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -4,11 +4,4 @@
|
|
|
4
4
|
padding: var(--wzl-space-xs);
|
|
5
5
|
border-right: var(--wzl-border-w) solid var(--wzl-line-subtle);
|
|
6
6
|
background: var(--wzl-surface-sunken);
|
|
7
|
-
|
|
8
|
-
// labkit's `:where(button)` default sets an explicit height, and because
|
|
9
|
-
// :where() carries no specificity it also wins over ToolButton, which sizes
|
|
10
|
-
// itself from padding.
|
|
11
|
-
button {
|
|
12
|
-
height: auto;
|
|
13
|
-
}
|
|
14
7
|
}
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
--_s-tone: var(--wzl-tone, var(--_s-surface));
|
|
24
24
|
--_s-accent: var(--wzl-tone);
|
|
25
25
|
--_s-title-font: var(--wzl-font-display);
|
|
26
|
+
--_s-title-weight: var(--wzl-font-weight-bold);
|
|
26
27
|
--_s-title-size: var(--wzl-font-size-sm);
|
|
27
28
|
--_s-title-case: uppercase;
|
|
28
29
|
--_s-title-tracking: 0.08em;
|
|
@@ -36,6 +37,7 @@
|
|
|
36
37
|
--_s-tone: var(--wzl-tone, var(--wzl-stance-scope-tone, var(--_s-surface)));
|
|
37
38
|
--_s-accent: var(--wzl-stance-scope-accent, var(--wzl-tone));
|
|
38
39
|
--_s-title-font: var(--wzl-stance-scope-title-font, var(--wzl-font-display));
|
|
40
|
+
--_s-title-weight: var(--wzl-stance-scope-title-weight, var(--wzl-font-weight-bold));
|
|
39
41
|
--_s-title-size: var(--wzl-stance-scope-title-size, var(--wzl-font-size-sm));
|
|
40
42
|
--_s-title-case: var(--wzl-stance-scope-title-case, uppercase);
|
|
41
43
|
--_s-title-tracking: var(--wzl-stance-scope-title-tracking, 0.08em);
|
|
@@ -49,6 +51,7 @@
|
|
|
49
51
|
--_s-tone: var(--wzl-tone, var(--wzl-stance-aside-tone, var(--_s-surface)));
|
|
50
52
|
--_s-accent: var(--wzl-stance-aside-accent, var(--wzl-tone));
|
|
51
53
|
--_s-title-font: var(--wzl-stance-aside-title-font, var(--wzl-font-display));
|
|
54
|
+
--_s-title-weight: var(--wzl-stance-aside-title-weight, var(--wzl-font-weight-bold));
|
|
52
55
|
--_s-title-size: var(--wzl-stance-aside-title-size, var(--wzl-font-size-sm));
|
|
53
56
|
--_s-title-case: var(--wzl-stance-aside-title-case, uppercase);
|
|
54
57
|
--_s-title-tracking: var(--wzl-stance-aside-title-tracking, 0.08em);
|
|
@@ -62,6 +65,7 @@
|
|
|
62
65
|
--_s-tone: var(--wzl-tone, var(--wzl-stance-advanced-tone, var(--_s-surface)));
|
|
63
66
|
--_s-accent: var(--wzl-stance-advanced-accent, var(--wzl-tone));
|
|
64
67
|
--_s-title-font: var(--wzl-stance-advanced-title-font, var(--wzl-font-display));
|
|
68
|
+
--_s-title-weight: var(--wzl-stance-advanced-title-weight, var(--wzl-font-weight-bold));
|
|
65
69
|
--_s-title-size: var(--wzl-stance-advanced-title-size, var(--wzl-font-size-sm));
|
|
66
70
|
--_s-title-case: var(--wzl-stance-advanced-title-case, uppercase);
|
|
67
71
|
--_s-title-tracking: var(--wzl-stance-advanced-title-tracking, 0.08em);
|
|
@@ -75,6 +79,7 @@
|
|
|
75
79
|
--_s-tone: var(--wzl-tone, var(--wzl-stance-debug-tone, var(--_s-surface)));
|
|
76
80
|
--_s-accent: var(--wzl-stance-debug-accent, var(--wzl-tone));
|
|
77
81
|
--_s-title-font: var(--wzl-stance-debug-title-font, var(--wzl-font-display));
|
|
82
|
+
--_s-title-weight: var(--wzl-stance-debug-title-weight, var(--wzl-font-weight-bold));
|
|
78
83
|
--_s-title-size: var(--wzl-stance-debug-title-size, var(--wzl-font-size-sm));
|
|
79
84
|
--_s-title-case: var(--wzl-stance-debug-title-case, uppercase);
|
|
80
85
|
--_s-title-tracking: var(--wzl-stance-debug-title-tracking, 0.08em);
|
|
@@ -88,6 +93,7 @@
|
|
|
88
93
|
--_s-tone: var(--wzl-tone, var(--wzl-stance-danger-tone, var(--_s-surface)));
|
|
89
94
|
--_s-accent: var(--wzl-stance-danger-accent, var(--wzl-tone));
|
|
90
95
|
--_s-title-font: var(--wzl-stance-danger-title-font, var(--wzl-font-display));
|
|
96
|
+
--_s-title-weight: var(--wzl-stance-danger-title-weight, var(--wzl-font-weight-bold));
|
|
91
97
|
--_s-title-size: var(--wzl-stance-danger-title-size, var(--wzl-font-size-sm));
|
|
92
98
|
--_s-title-case: var(--wzl-stance-danger-title-case, uppercase);
|
|
93
99
|
--_s-title-tracking: var(--wzl-stance-danger-title-tracking, 0.08em);
|
|
@@ -101,6 +107,7 @@
|
|
|
101
107
|
--_s-tone: var(--wzl-tone, var(--wzl-stance-notice-tone, var(--_s-surface)));
|
|
102
108
|
--_s-accent: var(--wzl-stance-notice-accent, var(--wzl-tone));
|
|
103
109
|
--_s-title-font: var(--wzl-stance-notice-title-font, var(--wzl-font-display));
|
|
110
|
+
--_s-title-weight: var(--wzl-stance-notice-title-weight, var(--wzl-font-weight-bold));
|
|
104
111
|
--_s-title-size: var(--wzl-stance-notice-title-size, var(--wzl-font-size-sm));
|
|
105
112
|
--_s-title-case: var(--wzl-stance-notice-title-case, uppercase);
|
|
106
113
|
--_s-title-tracking: var(--wzl-stance-notice-title-tracking, 0.08em);
|
|
@@ -114,6 +121,7 @@
|
|
|
114
121
|
--_s-tone: var(--wzl-tone, var(--wzl-stance-important-tone, var(--_s-surface)));
|
|
115
122
|
--_s-accent: var(--wzl-stance-important-accent, var(--wzl-tone));
|
|
116
123
|
--_s-title-font: var(--wzl-stance-important-title-font, var(--wzl-font-display));
|
|
124
|
+
--_s-title-weight: var(--wzl-stance-important-title-weight, var(--wzl-font-weight-bold));
|
|
117
125
|
--_s-title-size: var(--wzl-stance-important-title-size, var(--wzl-font-size-sm));
|
|
118
126
|
--_s-title-case: var(--wzl-stance-important-title-case, uppercase);
|
|
119
127
|
--_s-title-tracking: var(--wzl-stance-important-title-tracking, 0.08em);
|
|
@@ -127,6 +135,7 @@
|
|
|
127
135
|
--_s-tone: var(--wzl-tone, var(--wzl-stance-preview-tone, var(--_s-surface)));
|
|
128
136
|
--_s-accent: var(--wzl-stance-preview-accent, var(--wzl-tone));
|
|
129
137
|
--_s-title-font: var(--wzl-stance-preview-title-font, var(--wzl-font-display));
|
|
138
|
+
--_s-title-weight: var(--wzl-stance-preview-title-weight, var(--wzl-font-weight-bold));
|
|
130
139
|
--_s-title-size: var(--wzl-stance-preview-title-size, var(--wzl-font-size-sm));
|
|
131
140
|
--_s-title-case: var(--wzl-stance-preview-title-case, uppercase);
|
|
132
141
|
--_s-title-tracking: var(--wzl-stance-preview-title-tracking, 0.08em);
|
|
@@ -174,12 +183,13 @@
|
|
|
174
183
|
.lk-sidebar-section__head {
|
|
175
184
|
flex: 1;
|
|
176
185
|
width: 100%;
|
|
177
|
-
padding: var(--wzl-space-xs);
|
|
186
|
+
padding: var(--wzl-space-xs) var(--wzl-space-sm);
|
|
178
187
|
border: 0;
|
|
179
188
|
background: transparent;
|
|
180
189
|
color: var(--_s-title-color);
|
|
181
190
|
font-family: var(--_s-title-font);
|
|
182
191
|
font-size: var(--_s-title-size);
|
|
192
|
+
font-weight: var(--_s-title-weight);
|
|
183
193
|
letter-spacing: var(--_s-title-tracking);
|
|
184
194
|
text-align: left;
|
|
185
195
|
text-transform: var(--_s-title-case);
|
|
@@ -193,5 +203,5 @@
|
|
|
193
203
|
// The section body is the only thing that insets a panel — the panels
|
|
194
204
|
// themselves carry no padding, so this is the whole gutter.
|
|
195
205
|
.lk-sidebar-section__body {
|
|
196
|
-
padding: 0 var(--wzl-space-
|
|
206
|
+
padding: 0 var(--wzl-space-sm) var(--wzl-space-sm);
|
|
197
207
|
}
|
|
@@ -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,
|
|
@@ -141,10 +142,10 @@ export class NumberNode extends BaseNode<number> {
|
|
|
141
142
|
return this.ann({ suffix });
|
|
142
143
|
}
|
|
143
144
|
|
|
144
|
-
/** Show the value
|
|
145
|
-
* only: the stored value stays a plain number
|
|
146
|
-
|
|
147
|
-
return this.ann({
|
|
145
|
+
/** Show, speak and read the value through a display — `compact()` reads
|
|
146
|
+
* `2.00M`. Presentation only: the stored value stays a plain number. */
|
|
147
|
+
display(display: Display): this {
|
|
148
|
+
return this.ann({ display });
|
|
148
149
|
}
|
|
149
150
|
|
|
150
151
|
/**
|
|
@@ -286,6 +287,12 @@ export class GroupNode<S extends ConfigShape> implements ConfigBranch<S> {
|
|
|
286
287
|
showIf(predicate: (config: Record<string, unknown>) => boolean): GroupNode<S> {
|
|
287
288
|
return this.with(this.annotations, { ...this.options, showIf: predicate });
|
|
288
289
|
}
|
|
290
|
+
|
|
291
|
+
/** Put a button on the group's heading that returns every value beneath it
|
|
292
|
+
* to its default, leaving the rest of the config alone. */
|
|
293
|
+
resettable(): GroupNode<S> {
|
|
294
|
+
return this.with(this.annotations, { ...this.options, resettable: true });
|
|
295
|
+
}
|
|
289
296
|
}
|
|
290
297
|
|
|
291
298
|
/** 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,9 @@ 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
|
+
icon?: string;
|
|
21
19
|
}
|
|
22
20
|
|
|
23
21
|
/** Everything a leaf can carry beyond its kind and default. A flat union of
|
|
@@ -32,8 +30,8 @@ export interface Annotations {
|
|
|
32
30
|
max?: number;
|
|
33
31
|
step?: number;
|
|
34
32
|
suffix?: string;
|
|
35
|
-
/** How a number's value
|
|
36
|
-
|
|
33
|
+
/** How a number's value shows, speaks and reads back — `compact()` reads `2.00M`. */
|
|
34
|
+
display?: Display;
|
|
37
35
|
/** Display-unit conversion for a number stored in a canonical unit — the
|
|
38
36
|
* value, its bounds and its step all convert at the control's edge. */
|
|
39
37
|
unit?: PrefNumberUnit;
|
|
@@ -107,6 +105,9 @@ export interface ResolvedConfig {
|
|
|
107
105
|
renderers: Readonly<Record<string, ControlRenderer>>;
|
|
108
106
|
/** Node-level `.dialog` rows, keyed by path. */
|
|
109
107
|
dialogs: Readonly<Record<string, DialogSpec>>;
|
|
108
|
+
/** Paths of the groups marked `.resettable()`, whose heading carries a
|
|
109
|
+
* button that puts every value beneath it back to its default. */
|
|
110
|
+
resettable?: ReadonlySet<string>;
|
|
110
111
|
}
|
|
111
112
|
|
|
112
113
|
/** Options for a row drawn as a button that opens a dialog. */
|
|
@@ -162,6 +163,9 @@ export interface BranchOptions {
|
|
|
162
163
|
/** Show this node only while the predicate holds. On a group it hides the
|
|
163
164
|
* whole subtree; the values stay in config either way. */
|
|
164
165
|
showIf?: (config: Record<string, unknown>) => boolean;
|
|
166
|
+
/** Put a button on this group's heading that returns every value beneath it
|
|
167
|
+
* to its default. Groups only. */
|
|
168
|
+
resettable?: boolean;
|
|
165
169
|
}
|
|
166
170
|
|
|
167
171
|
/** 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'),
|