@weasel-js/labkit 1.7.0 → 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-D2jnIwfk.d.ts → CanvasStackContext-Cjy6I-Iv.d.ts} +4 -1
- package/dist/_dts/{Stage-CCWfQQWM.d.ts → Stage-D6t7o0pF.d.ts} +3 -2
- package/dist/_dts/{index-CsYqUHD3.d.ts → index-BTmaRy6a.d.ts} +10 -6
- package/dist/_dts/{types-CSm9jgol.d.ts → index-DCMA4Fy1.d.ts} +59 -113
- package/dist/_dts/{types-CRPSGFAp.d.ts → types-BUBSIZuV.d.ts} +8 -7
- package/dist/_dts/{useTrialState-CTZPgeZ7.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 +7 -5
- package/dist/chrome/index.js +10 -9
- 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-7BO7HNGF.js → chunk-CEGFXTBD.js} +8 -61
- package/dist/chunk-CEGFXTBD.js.map +1 -0
- package/dist/{chunk-275JRRVD.js → chunk-E23Q6A3Y.js} +232 -37
- package/dist/chunk-E23Q6A3Y.js.map +1 -0
- package/dist/{chunk-GOZLNM4L.js → chunk-E6NARGU7.js} +5 -5
- package/dist/{chunk-GOZLNM4L.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-FMK7UQ6V.js → chunk-FB4MIZOO.js} +61 -44
- 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-I5DUQ5HM.js → chunk-GAHELLJT.js} +75 -72
- package/dist/chunk-GAHELLJT.js.map +1 -0
- package/dist/{chunk-YT75VYRD.js → chunk-IBVRMHW5.js} +5 -5
- package/dist/{chunk-YT75VYRD.js.map → chunk-IBVRMHW5.js.map} +1 -1
- package/dist/{chunk-T5IBSPAM.js → chunk-JFOJESJF.js} +33 -23
- 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-Y23G2VZL.js → chunk-READTDQX.js} +2 -2
- 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-ZIFPHIWZ.js → chunk-T6DLGEJB.js} +12 -14
- 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-UNAZSLWE.js → chunk-VYVGVBGY.js} +17 -20
- package/dist/chunk-VYVGVBGY.js.map +1 -0
- package/dist/{chunk-KMJ7RWD3.js → chunk-WLMDTI2Q.js} +3 -3
- package/dist/{chunk-KMJ7RWD3.js.map → chunk-WLMDTI2Q.js.map} +1 -1
- package/dist/{chunk-J7OWWTCW.js → chunk-XE5HJOPL.js} +5 -6
- 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-FZ7YFQN7.js → chunk-YCFKW5RX.js} +3 -3
- 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 +18 -10
- 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 +60 -714
- package/dist/index.js +357 -334
- 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 +15 -14
- 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 -2871
- package/dist/passthrough/weasel-ui.js +3 -2
- package/dist/primitives/index.d.ts +213 -3
- package/dist/primitives/index.js +8 -7
- package/dist/state/index.d.ts +7 -5
- package/dist/state/index.js +6 -6
- package/dist/styles.css +60 -44
- 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 +11 -11
- package/src/annotations/AnnotationOverlay.abandon.test.tsx +143 -0
- package/src/annotations/AnnotationOverlay.tsx +50 -31
- package/src/annotations/Annotations.overlay.test.tsx +31 -0
- 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 +74 -33
- package/src/annotations/paint.ts +70 -32
- package/src/annotations/store.test.ts +212 -2
- package/src/annotations/store.ts +130 -27
- package/src/annotations/svgNodes.test.ts +22 -10
- package/src/annotations/svgNodes.ts +3 -3
- package/src/annotations/types.ts +29 -10
- package/src/canvas/CameraInput.test.tsx +30 -0
- package/src/canvas/CameraInput.tsx +16 -15
- package/src/canvas/CanvasStack.tsx +5 -5
- package/src/canvas/CanvasStackContext.ts +4 -1
- package/src/canvas/Stage.tsx +3 -3
- package/src/canvas/cameraRegistry.ts +1 -0
- package/src/canvas/worldSpec.ts +3 -1
- 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.test.tsx +2 -3
- package/src/chrome/regions/PaletteRegion.tsx +3 -4
- 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.ts +6 -0
- package/src/config/types.ts +2 -1
- package/src/controls/ControlPanel.test.tsx +37 -3
- package/src/controls/ControlPanel.tsx +12 -3
- 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 +1 -1
- package/src/instrument/types.ts +0 -6
- package/src/job/useJob.ts +14 -9
- package/src/lab/Lab.tsx +5 -1
- package/src/lab/LabShell.less +1 -0
- package/src/lab/LabSwitcher.browser.test.tsx +27 -0
- package/src/lab/LabSwitcher.less +2 -0
- package/src/lab/LabZoom.tsx +4 -4
- package/src/lab/Workspace.tsx +48 -52
- 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/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 +3 -3
- package/src/passthrough/weasel-ui.test.ts +1 -1
- package/src/passthrough/weasel-ui.ts +1 -1
- package/src/primitives/FloatingPanel.tsx +7 -3
- package/src/primitives/JobProgress.less +1 -0
- package/src/primitives/Readout.test.tsx +2 -2
- package/src/primitives/Split.tsx +69 -41
- package/src/primitives/Toolbar.tsx +3 -3
- package/src/primitives/ZoomControl.less +1 -2
- package/src/specimen/Specimen.tsx +0 -1
- package/src/state/undock.ts +3 -0
- package/src/state/useOpenOnce.ts +20 -16
- package/src/state/view.ts +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/trial/Trial.annotations.test.tsx +73 -0
- package/src/trial/Trial.less +0 -16
- package/src/trial/Trial.loupe.test.tsx +28 -11
- package/src/trial/Trial.tsx +74 -62
- package/src/trial/TrialChrome.tsx +6 -3
- package/src/trial/index.ts +2 -0
- package/src/trial/loupeSwitch.ts +15 -0
- package/dist/_dts/LayerList.d-DiuEArKq.d.ts +0 -83
- package/dist/_dts/PrefsRow.d-TOZ7BgD9.d.ts +0 -79
- package/dist/_dts/PropertyPanel.d-D-1paCbE.d.ts +0 -141
- package/dist/_dts/Select.d-Bensr_MT.d.ts +0 -579
- package/dist/_dts/index-B2Yj5aA0.d.ts +0 -274
- package/dist/chunk-275JRRVD.js.map +0 -1
- package/dist/chunk-4LJSC7MB.js +0 -143
- package/dist/chunk-4LJSC7MB.js.map +0 -1
- package/dist/chunk-7BO7HNGF.js.map +0 -1
- package/dist/chunk-CQLQPQ4P.js.map +0 -1
- package/dist/chunk-FMK7UQ6V.js.map +0 -1
- package/dist/chunk-FZ7YFQN7.js.map +0 -1
- package/dist/chunk-HXXTULDN.js.map +0 -1
- package/dist/chunk-I5DUQ5HM.js.map +0 -1
- package/dist/chunk-IJJUK4FU.js.map +0 -1
- package/dist/chunk-J7OWWTCW.js.map +0 -1
- package/dist/chunk-M7HP3FTK.js +0 -549
- package/dist/chunk-M7HP3FTK.js.map +0 -1
- package/dist/chunk-MIM5R4ZT.js +0 -14972
- package/dist/chunk-MIM5R4ZT.js.map +0 -1
- package/dist/chunk-RWNDDKOH.js.map +0 -1
- package/dist/chunk-T5IBSPAM.js.map +0 -1
- package/dist/chunk-UNAZSLWE.js.map +0 -1
- package/dist/chunk-WPLAANH4.js.map +0 -1
- package/dist/chunk-Y23G2VZL.js.map +0 -1
- package/dist/chunk-ZIFPHIWZ.js.map +0 -1
- package/src/primitives/useRovingTabIndex.test.ts +0 -48
- package/src/primitives/useRovingTabIndex.ts +0 -88
|
@@ -15,6 +15,7 @@ export interface CameraRegistry {
|
|
|
15
15
|
subscribe(listener: () => void): () => void;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/** An empty in-memory `CameraRegistry`. */
|
|
18
19
|
export function createCameraRegistry(): CameraRegistry {
|
|
19
20
|
const stacks = new Map<string, CameraView[]>();
|
|
20
21
|
const listeners = new Set<() => void>();
|
package/src/canvas/worldSpec.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { Point, ViewTransform } from '../instrument/types';
|
|
|
5
5
|
* world (0,0) at the element's top-left, y growing downward. */
|
|
6
6
|
export interface WorldSpec {
|
|
7
7
|
/** Where world (0,0) sits at `pan` zero, as a fraction of the viewport.
|
|
8
|
-
* `{x:0,y:0}` top-left (default); `{x:0.5,y:0.5}`
|
|
8
|
+
* `{x:0,y:0}` top-left (default); `{x:0.5,y:0.5}` center. */
|
|
9
9
|
origin?: Point;
|
|
10
10
|
/** Which way the world y axis runs on screen. Default `'down'`. */
|
|
11
11
|
yAxis?: 'down' | 'up';
|
|
@@ -25,8 +25,10 @@ export interface WorldFrame {
|
|
|
25
25
|
yDir: 1 | -1;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/** The frame an absent `WorldSpec` resolves to: origin top-left, y down. */
|
|
28
29
|
export const DEFAULT_FRAME: WorldFrame = { originPx: { x: 0, y: 0 }, yDir: 1 };
|
|
29
30
|
|
|
31
|
+
/** Resolve `spec` against a viewport of `size`; `undefined` gives `DEFAULT_FRAME`. */
|
|
30
32
|
export function resolveFrame(spec: WorldSpec | undefined, size: ViewportSize): WorldFrame {
|
|
31
33
|
if (!spec) return DEFAULT_FRAME;
|
|
32
34
|
const origin = spec.origin ?? { x: 0, y: 0 };
|
|
@@ -23,6 +23,7 @@ const ctx: TrialChromeContext = {
|
|
|
23
23
|
canRedo: false,
|
|
24
24
|
undo: () => {},
|
|
25
25
|
redo: () => {},
|
|
26
|
+
hasLoupe: false,
|
|
26
27
|
loupeOn: false,
|
|
27
28
|
toggleLoupe: () => {},
|
|
28
29
|
configFields: [],
|
|
@@ -72,21 +73,21 @@ describe('builtinContributions', () => {
|
|
|
72
73
|
expect(ids({ ...bare, undo: {} })).toContain('redo');
|
|
73
74
|
});
|
|
74
75
|
|
|
75
|
-
it('contributes the loupe toggle only
|
|
76
|
+
it('contributes the loupe toggle only while a lens is mounted', () => {
|
|
76
77
|
expect(ids(bare)).not.toContain('loupe');
|
|
77
|
-
|
|
78
|
-
expect(
|
|
78
|
+
const mounted = builtinContributions(bare, { ...ctx, hasLoupe: true });
|
|
79
|
+
expect(mounted.map((c) => c.id)).toContain('loupe');
|
|
79
80
|
});
|
|
80
81
|
|
|
81
82
|
it('offers the loupe to a DOM instrument, which declares no canvas', () => {
|
|
82
|
-
const contributions = builtinContributions({ ...
|
|
83
|
+
const contributions = builtinContributions(bare, { ...ctx, hasLoupe: true });
|
|
83
84
|
const loupe = contributions.find((c) => c.id === 'loupe');
|
|
84
85
|
expect(loupe?.region).toBe('toolbar');
|
|
85
86
|
});
|
|
86
87
|
|
|
87
88
|
it('reports the loupe switch position, so the button reads as held', () => {
|
|
88
|
-
const off = builtinContributions({ ...
|
|
89
|
-
const on = builtinContributions({ ...
|
|
89
|
+
const off = builtinContributions(bare, { ...ctx, hasLoupe: true });
|
|
90
|
+
const on = builtinContributions(bare, { ...ctx, hasLoupe: true, loupeOn: true });
|
|
90
91
|
expect(off.find((c) => c.id === 'loupe')?.item).toMatchObject({ pressed: false });
|
|
91
92
|
expect(on.find((c) => c.id === 'loupe')?.item).toMatchObject({ pressed: true });
|
|
92
93
|
});
|
package/src/chrome/builtins.tsx
CHANGED
|
@@ -39,6 +39,10 @@ function groupFolds(
|
|
|
39
39
|
return out;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/** Built-in ids contributed only while runtime state says so — the loupe
|
|
43
|
+
* toggle appears once a lens mounts, after the trial's first render. */
|
|
44
|
+
export const TRANSIENT_BUILTINS: ReadonlySet<string> = new Set(['loupe']);
|
|
45
|
+
|
|
42
46
|
/**
|
|
43
47
|
* The contributions a trial gets from what its instrument declared. This is
|
|
44
48
|
* the whole "declaring a capability provides the chrome" rule — it replaces
|
|
@@ -93,7 +97,7 @@ export function builtinContributions(
|
|
|
93
97
|
});
|
|
94
98
|
}
|
|
95
99
|
|
|
96
|
-
if (
|
|
100
|
+
if (ctx.hasLoupe) {
|
|
97
101
|
out.push({
|
|
98
102
|
id: 'loupe',
|
|
99
103
|
region: 'toolbar',
|
package/src/chrome/index.ts
CHANGED
package/src/chrome/merge.test.ts
CHANGED
|
@@ -44,6 +44,11 @@ describe('suppressContributions', () => {
|
|
|
44
44
|
);
|
|
45
45
|
});
|
|
46
46
|
|
|
47
|
+
it('passes over an absent id it was told may come and go', () => {
|
|
48
|
+
const out = suppressContributions([toolbarItem('a')], ['late'], new Set(['late']));
|
|
49
|
+
expect(out.map((c) => c.id)).toEqual(['a']);
|
|
50
|
+
});
|
|
51
|
+
|
|
47
52
|
it('is a no-op for an empty suppress list', () => {
|
|
48
53
|
const bundle = [toolbarItem('a')];
|
|
49
54
|
expect(suppressContributions(bundle, [])).toEqual(bundle);
|
package/src/chrome/merge.ts
CHANGED
|
@@ -30,15 +30,17 @@ export function mergeContributions<T extends { id: string }>(
|
|
|
30
30
|
/**
|
|
31
31
|
* Drop contributions by id. Throws when an id is not present: a typo that
|
|
32
32
|
* silently suppresses nothing is the same class of bug as a duplicate id
|
|
33
|
-
* silently winning.
|
|
33
|
+
* silently winning. `transient` names ids that are legitimately absent at
|
|
34
|
+
* times — contributed only while some runtime state holds.
|
|
34
35
|
*/
|
|
35
36
|
export function suppressContributions<T extends { id: string }>(
|
|
36
37
|
bundle: readonly T[],
|
|
37
38
|
ids: readonly string[],
|
|
39
|
+
transient: ReadonlySet<string> = new Set(),
|
|
38
40
|
): T[] {
|
|
39
41
|
const present = new Set(bundle.map((c) => c.id));
|
|
40
42
|
for (const id of ids) {
|
|
41
|
-
if (!present.has(id)) {
|
|
43
|
+
if (!present.has(id) && !transient.has(id)) {
|
|
42
44
|
throw new Error(
|
|
43
45
|
`[labkit] cannot suppress "${id}": no contribution with that id. ` +
|
|
44
46
|
`Present ids: ${[...present].join(', ')}`,
|
|
@@ -62,16 +62,15 @@ describe('PaletteRegion', () => {
|
|
|
62
62
|
|
|
63
63
|
it('walks the vertical strip with ArrowDown, and leaves ArrowRight to the page', () => {
|
|
64
64
|
render(<PaletteRegion contributions={[tool('brush'), tool('eraser')]} ctx={ctxWith(null)} />);
|
|
65
|
-
const strip = screen.getByRole('toolbar', { name: 'Tools' });
|
|
66
65
|
const brush = screen.getByRole('button', { name: 'brush' });
|
|
67
66
|
const eraser = screen.getByRole('button', { name: 'eraser' });
|
|
68
67
|
|
|
69
68
|
brush.focus();
|
|
70
|
-
fireEvent.keyDown(
|
|
69
|
+
fireEvent.keyDown(brush, { key: 'ArrowDown' });
|
|
71
70
|
expect(document.activeElement).toBe(eraser);
|
|
72
71
|
expect(eraser).toHaveAttribute('tabindex', '0');
|
|
73
72
|
|
|
74
|
-
fireEvent.keyDown(
|
|
73
|
+
fireEvent.keyDown(eraser, { key: 'ArrowRight' });
|
|
75
74
|
expect(document.activeElement).toBe(eraser);
|
|
76
75
|
});
|
|
77
76
|
});
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { ToolButton, ToolGroup } from '../../passthrough/weasel-ui';
|
|
2
|
-
import { useRovingTabIndex } from '../../primitives/useRovingTabIndex';
|
|
1
|
+
import { ToolButton, ToolGroup, useRovingTabIndex } from '../../passthrough/weasel-ui';
|
|
3
2
|
import type { RegionContribution, ToolItem, ToolSlotContext, TrialChromeContext } from '../types';
|
|
4
3
|
|
|
5
4
|
/** Props for `<PaletteRegion>`. */
|
|
@@ -19,11 +18,11 @@ export function PaletteRegion<TCtx extends ToolSlotContext = TrialChromeContext>
|
|
|
19
18
|
ctx,
|
|
20
19
|
region = 'palette',
|
|
21
20
|
}: PaletteRegionProps<TCtx>) {
|
|
22
|
-
const {
|
|
21
|
+
const { rootRef, onKeyDown } = useRovingTabIndex<HTMLDivElement>({ orientation: 'vertical' });
|
|
23
22
|
if (contributions.length === 0) return null;
|
|
24
23
|
return (
|
|
25
24
|
<div
|
|
26
|
-
ref={
|
|
25
|
+
ref={rootRef}
|
|
27
26
|
className="lk-palette-region"
|
|
28
27
|
role="toolbar"
|
|
29
28
|
aria-label="Tools"
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import '@weasel-js/theme/tokens.css';
|
|
2
|
+
import '../../styles.less';
|
|
3
|
+
import './ViewportRegion.browser.test.less';
|
|
4
|
+
import { cleanup, render } from '@testing-library/react';
|
|
5
|
+
import { afterEach, expect, test } from 'vitest';
|
|
6
|
+
import { ZoomControl } from '../../primitives/ZoomControl';
|
|
7
|
+
import type { TrialChromeContext, TrialContribution } from '../types';
|
|
8
|
+
import { ViewportRegion } from './ViewportRegion';
|
|
9
|
+
|
|
10
|
+
// Whether a rendered control shrinks to the cluster or runs out past it is
|
|
11
|
+
// layout, which only a real browser has.
|
|
12
|
+
|
|
13
|
+
afterEach(cleanup);
|
|
14
|
+
|
|
15
|
+
const ctx = { zoom: 1, setZoom: () => {} } as unknown as TrialChromeContext;
|
|
16
|
+
|
|
17
|
+
const contributions: TrialContribution[] = [
|
|
18
|
+
{
|
|
19
|
+
id: 'zoom-control',
|
|
20
|
+
region: 'viewport',
|
|
21
|
+
render: (c) => <ZoomControl zoom={c.zoom ?? 1} onZoomChange={c.setZoom} />,
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
test('a zoom control on its own row shrinks its slider to fit a narrow well', () => {
|
|
26
|
+
const { container } = render(
|
|
27
|
+
<div className="lk-root">
|
|
28
|
+
<div className="lk-viewport-test-well">
|
|
29
|
+
<ViewportRegion contributions={contributions} ctx={ctx} />
|
|
30
|
+
</div>
|
|
31
|
+
</div>,
|
|
32
|
+
);
|
|
33
|
+
const cluster = container.querySelector('.lk-viewport-controls')?.getBoundingClientRect();
|
|
34
|
+
const zoom = container.querySelector('.lk-zoom')?.getBoundingClientRect();
|
|
35
|
+
if (!cluster || !zoom) throw new Error('nothing rendered');
|
|
36
|
+
expect(zoom.left).toBeGreaterThanOrEqual(cluster.left);
|
|
37
|
+
expect(zoom.right).toBeLessThanOrEqual(cluster.right);
|
|
38
|
+
});
|
|
@@ -17,6 +17,12 @@
|
|
|
17
17
|
backdrop-filter: blur(var(--wzl-glass-blur));
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
// Without this a rendered control's floor is its content's min-content, and the
|
|
21
|
+
// zoom slider's fixed width then runs it out past the cluster on a narrow well.
|
|
22
|
+
.lk-viewport-controls__item {
|
|
23
|
+
min-width: 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
20
26
|
.lk-viewport-controls__button {
|
|
21
27
|
display: inline-flex;
|
|
22
28
|
align-items: center;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useRovingTabIndex } from '../../
|
|
1
|
+
import { useRovingTabIndex } from '../../passthrough/weasel-ui';
|
|
2
2
|
import type { TrialChromeContext, TrialContribution } from '../types';
|
|
3
3
|
|
|
4
4
|
/** Props for `<ViewportRegion>`. */
|
|
@@ -12,18 +12,24 @@ export interface ViewportRegionProps {
|
|
|
12
12
|
* rather than in the toolbar, which acts on the trial itself.
|
|
13
13
|
*/
|
|
14
14
|
export function ViewportRegion({ contributions, ctx }: ViewportRegionProps) {
|
|
15
|
-
const {
|
|
15
|
+
const { rootRef, onKeyDown } = useRovingTabIndex<HTMLDivElement>({ orientation: 'horizontal' });
|
|
16
16
|
if (contributions.length === 0) return null;
|
|
17
17
|
return (
|
|
18
18
|
<div
|
|
19
|
-
ref={
|
|
19
|
+
ref={rootRef}
|
|
20
20
|
className="lk-viewport-controls"
|
|
21
21
|
role="toolbar"
|
|
22
22
|
aria-label="View"
|
|
23
23
|
onKeyDown={onKeyDown}
|
|
24
24
|
>
|
|
25
25
|
{contributions.map((c) => {
|
|
26
|
-
if (c.render)
|
|
26
|
+
if (c.render) {
|
|
27
|
+
return (
|
|
28
|
+
<span key={c.id} className="lk-viewport-controls__item">
|
|
29
|
+
{c.render(ctx)}
|
|
30
|
+
</span>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
27
33
|
if (c.region !== 'viewport' || !c.item) return null;
|
|
28
34
|
const { icon: Icon, label, disabled, onActivate } = c.item;
|
|
29
35
|
return (
|
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
|
}
|
package/src/config/builder.ts
CHANGED
|
@@ -124,6 +124,7 @@ export abstract class BaseNode<T> implements ConfigNode<T> {
|
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
/** A numeric leaf, built by `f.number`. */
|
|
127
128
|
export class NumberNode extends BaseNode<number> {
|
|
128
129
|
readonly kind = 'number';
|
|
129
130
|
|
|
@@ -169,6 +170,7 @@ export class NumberNode extends BaseNode<number> {
|
|
|
169
170
|
}
|
|
170
171
|
}
|
|
171
172
|
|
|
173
|
+
/** A true/false leaf, built by `f.boolean`; a checkbox unless `.toggle()`. */
|
|
172
174
|
export class BooleanNode extends BaseNode<boolean> {
|
|
173
175
|
readonly kind = 'boolean';
|
|
174
176
|
|
|
@@ -178,6 +180,7 @@ export class BooleanNode extends BaseNode<boolean> {
|
|
|
178
180
|
}
|
|
179
181
|
}
|
|
180
182
|
|
|
183
|
+
/** A text leaf, built by `f.string`. */
|
|
181
184
|
export class StringNode extends BaseNode<string> {
|
|
182
185
|
readonly kind = 'string';
|
|
183
186
|
|
|
@@ -195,6 +198,7 @@ export class StringNode extends BaseNode<string> {
|
|
|
195
198
|
}
|
|
196
199
|
}
|
|
197
200
|
|
|
201
|
+
/** A string-list leaf, built by `f.list`. */
|
|
198
202
|
export class ListNode extends BaseNode<string[]> {
|
|
199
203
|
readonly kind = 'list';
|
|
200
204
|
|
|
@@ -204,6 +208,7 @@ export class ListNode extends BaseNode<string[]> {
|
|
|
204
208
|
}
|
|
205
209
|
}
|
|
206
210
|
|
|
211
|
+
/** A color leaf holding a hex string, built by `f.color`. */
|
|
207
212
|
export class ColorNode extends BaseNode<string> {
|
|
208
213
|
readonly kind = 'color';
|
|
209
214
|
|
|
@@ -214,6 +219,7 @@ export class ColorNode extends BaseNode<string> {
|
|
|
214
219
|
}
|
|
215
220
|
}
|
|
216
221
|
|
|
222
|
+
/** A fixed-choice leaf, built by `f.enum`; a select unless `.radio()`. */
|
|
217
223
|
export class EnumNode<T extends string> extends BaseNode<T> {
|
|
218
224
|
readonly kind = 'enum';
|
|
219
225
|
|
package/src/config/types.ts
CHANGED
|
@@ -14,7 +14,8 @@ export interface ConfigOption {
|
|
|
14
14
|
value: string;
|
|
15
15
|
label: string;
|
|
16
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
|
|
17
|
+
* place of `label`, which stays the accessible name and becomes the tooltip;
|
|
18
|
+
* a select draws it beside `label`. */
|
|
18
19
|
icon?: string;
|
|
19
20
|
}
|
|
20
21
|
|
|
@@ -919,6 +919,30 @@ describe('<ControlPanel> option icons', () => {
|
|
|
919
919
|
expect(ok.textContent).toBe('');
|
|
920
920
|
expect(screen.getByRole('radio', { name: 'bad' }).textContent).toBe('bad');
|
|
921
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
|
+
});
|
|
922
946
|
});
|
|
923
947
|
|
|
924
948
|
describe('<ControlPanel> pair', () => {
|
|
@@ -1129,14 +1153,20 @@ describe('<ControlPanel> resettable group', () => {
|
|
|
1129
1153
|
const schema = resolveConfigSchema(
|
|
1130
1154
|
f.schema({
|
|
1131
1155
|
name: f.string('a'),
|
|
1132
|
-
modes: f
|
|
1156
|
+
modes: f
|
|
1157
|
+
.group({ one: f.number(0), two: f.number(0), inner: f.group({ three: f.number(1) }) })
|
|
1158
|
+
.resettable(),
|
|
1133
1159
|
plain: f.group({ four: f.number(0) }),
|
|
1134
1160
|
}),
|
|
1135
1161
|
);
|
|
1136
1162
|
|
|
1137
1163
|
it('puts a reset button on the group heading, disabled while nothing has changed', () => {
|
|
1138
1164
|
render(
|
|
1139
|
-
<ControlPanel
|
|
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
|
+
/>,
|
|
1140
1170
|
);
|
|
1141
1171
|
expect(screen.getByRole('button', { name: 'Reset Modes' })).toBeDisabled();
|
|
1142
1172
|
expect(screen.queryByRole('button', { name: 'Reset Plain' })).toBeNull();
|
|
@@ -1145,7 +1175,11 @@ describe('<ControlPanel> resettable group', () => {
|
|
|
1145
1175
|
it('writes the default of every changed value beneath the group and nothing else', () => {
|
|
1146
1176
|
const setConfig = vi.fn();
|
|
1147
1177
|
render(
|
|
1148
|
-
<ControlPanel
|
|
1178
|
+
<ControlPanel
|
|
1179
|
+
schema={schema}
|
|
1180
|
+
config={{ name: 'b', modes: { one: 2, two: 0, inner: { three: 5 } }, plain: { four: 3 } }}
|
|
1181
|
+
setConfig={setConfig}
|
|
1182
|
+
/>,
|
|
1149
1183
|
);
|
|
1150
1184
|
fireEvent.click(screen.getByRole('button', { name: 'Reset Modes' }));
|
|
1151
1185
|
expect(setConfig.mock.calls).toEqual([
|
|
@@ -123,7 +123,9 @@ const NO_AUTO: ReadonlySet<string> = new Set();
|
|
|
123
123
|
function leavesUnder(group: PrefGroup, at: string): [string, unknown][] {
|
|
124
124
|
return Object.entries(group.children).flatMap(([key, child]) => {
|
|
125
125
|
const path = at === '' ? key : `${at}.${key}`;
|
|
126
|
-
return isPrefLeaf(child)
|
|
126
|
+
return isPrefLeaf(child)
|
|
127
|
+
? [[path, child.default] as [string, unknown]]
|
|
128
|
+
: leavesUnder(child, path);
|
|
127
129
|
});
|
|
128
130
|
}
|
|
129
131
|
|
|
@@ -140,7 +142,9 @@ function ResetGroup({
|
|
|
140
142
|
config: Record<string, unknown>;
|
|
141
143
|
setConfig: (path: string, value: unknown) => void;
|
|
142
144
|
}) {
|
|
143
|
-
const changed = leavesUnder(group, path).filter(
|
|
145
|
+
const changed = leavesUnder(group, path).filter(
|
|
146
|
+
([p, d]) => !Object.is(valueAtPath(config, p), d),
|
|
147
|
+
);
|
|
144
148
|
return (
|
|
145
149
|
<Button
|
|
146
150
|
iconOnly
|
|
@@ -267,7 +271,12 @@ export function ControlPanel<TC extends Record<string, unknown>>({
|
|
|
267
271
|
span
|
|
268
272
|
actions={
|
|
269
273
|
resolved.resettable?.has(path) ? (
|
|
270
|
-
<ResetGroup
|
|
274
|
+
<ResetGroup
|
|
275
|
+
group={found}
|
|
276
|
+
path={path}
|
|
277
|
+
config={config as Record<string, unknown>}
|
|
278
|
+
setConfig={setConfig}
|
|
279
|
+
/>
|
|
271
280
|
) : undefined
|
|
272
281
|
}
|
|
273
282
|
{...fold(path, undefined, rows.grid)}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { openPointerSession, type PointerSession, useVisibleRaf } from '@weasel-js/core';
|
|
1
|
+
import { openPointerSession, type PointerSession, useLatest, useVisibleRaf } from '@weasel-js/core';
|
|
2
|
+
import { boxContainsPoint } from '@weasel-js/geom';
|
|
2
3
|
import {
|
|
3
4
|
type PointerEvent as ReactPointerEvent,
|
|
4
5
|
type RefObject,
|
|
@@ -18,12 +19,16 @@ import type {
|
|
|
18
19
|
} from '../instrument/types';
|
|
19
20
|
import { DragGhost } from './DragGhost';
|
|
20
21
|
|
|
22
|
+
/** A palette drag in progress. `screenPos` is in client pixels; `feedback` is
|
|
23
|
+
* the last `onDragOver` answer — `null` off the canvas or without one. */
|
|
21
24
|
export interface DragState {
|
|
22
25
|
item: PaletteItem;
|
|
23
26
|
screenPos: Point;
|
|
24
27
|
feedback: DragFeedback | null;
|
|
25
28
|
}
|
|
26
29
|
|
|
30
|
+
/** Inputs to `useDragDrop`: the capability, the element that counts as the
|
|
31
|
+
* drop target, and the trial state a drop rewrites. */
|
|
27
32
|
export interface UseDragDropArgs<TS, TC> {
|
|
28
33
|
capability: DragDropCapability<TS, TC>;
|
|
29
34
|
canvasContainerRef: RefObject<HTMLElement | null>;
|
|
@@ -37,11 +42,18 @@ export interface UseDragDropArgs<TS, TC> {
|
|
|
37
42
|
emit: (event: string) => void;
|
|
38
43
|
}
|
|
39
44
|
|
|
45
|
+
/** The live drag, and the handler a palette item calls on pointerdown. */
|
|
40
46
|
export interface UseDragDropResult {
|
|
41
47
|
drag: DragState | null;
|
|
42
48
|
startDrag: (item: PaletteItem, e: ReactPointerEvent) => void;
|
|
43
49
|
}
|
|
44
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Run palette-to-canvas drags for a `DragDropCapability`. Releasing over the
|
|
53
|
+
* container converts the pointer to world coordinates, applies `onDrop`'s
|
|
54
|
+
* state and emits `'canvas.itemAdded'`; releasing anywhere else, or a canceled
|
|
55
|
+
* gesture, drops nothing. `onDragOver` is probed at most once per frame.
|
|
56
|
+
*/
|
|
45
57
|
export function useDragDrop<TS, TC>({
|
|
46
58
|
capability,
|
|
47
59
|
canvasContainerRef,
|
|
@@ -53,8 +65,8 @@ export function useDragDrop<TS, TC>({
|
|
|
53
65
|
worldSpec,
|
|
54
66
|
}: UseDragDropArgs<TS, TC>): UseDragDropResult {
|
|
55
67
|
const [drag, setDrag] = useState<DragState | null>(null);
|
|
68
|
+
// Owned by the handlers below, which write it alongside every `setDrag`.
|
|
56
69
|
const dragRef = useRef<DragState | null>(null);
|
|
57
|
-
dragRef.current = drag;
|
|
58
70
|
/** The move the next frame will resolve. Doubles as the throttle's flag:
|
|
59
71
|
* non-null means a frame is already queued. */
|
|
60
72
|
const pendingPos = useRef<Point | null>(null);
|
|
@@ -62,20 +74,14 @@ export function useDragDrop<TS, TC>({
|
|
|
62
74
|
|
|
63
75
|
/** A session keeps the closure it was opened with, so everything the drop
|
|
64
76
|
* reads has to come from here rather than from that closure. */
|
|
65
|
-
const liveRef =
|
|
66
|
-
liveRef.current = { capability, state, config, setState, emit, view, worldSpec };
|
|
77
|
+
const liveRef = useLatest({ capability, state, config, setState, emit, view, worldSpec });
|
|
67
78
|
|
|
68
79
|
const isOverCanvas = useCallback(
|
|
69
80
|
(screenPos: Point): boolean => {
|
|
70
81
|
const el = canvasContainerRef.current;
|
|
71
82
|
if (!el) return false;
|
|
72
83
|
const r = el.getBoundingClientRect();
|
|
73
|
-
return (
|
|
74
|
-
screenPos.x >= r.left &&
|
|
75
|
-
screenPos.x <= r.right &&
|
|
76
|
-
screenPos.y >= r.top &&
|
|
77
|
-
screenPos.y <= r.bottom
|
|
78
|
-
);
|
|
84
|
+
return boxContainsPoint([r.left, r.top, r.right, r.bottom], screenPos.x, screenPos.y);
|
|
79
85
|
},
|
|
80
86
|
[canvasContainerRef],
|
|
81
87
|
);
|
|
@@ -92,7 +98,7 @@ export function useDragDrop<TS, TC>({
|
|
|
92
98
|
frame,
|
|
93
99
|
);
|
|
94
100
|
},
|
|
95
|
-
[canvasContainerRef],
|
|
101
|
+
[canvasContainerRef, liveRef],
|
|
96
102
|
);
|
|
97
103
|
|
|
98
104
|
const frameLoop = useVisibleRaf(() => {
|
|
@@ -106,7 +112,8 @@ export function useDragDrop<TS, TC>({
|
|
|
106
112
|
const world = screenToWorldFromContainer(screenPos);
|
|
107
113
|
if (world) feedback = live.capability.onDragOver(world, active.item, live.state, live.config);
|
|
108
114
|
}
|
|
109
|
-
|
|
115
|
+
dragRef.current = { ...active, screenPos, feedback };
|
|
116
|
+
setDrag(dragRef.current);
|
|
110
117
|
});
|
|
111
118
|
|
|
112
119
|
const clearDrag = useCallback(() => {
|
|
@@ -154,7 +161,7 @@ export function useDragDrop<TS, TC>({
|
|
|
154
161
|
onCancel: clearDrag,
|
|
155
162
|
});
|
|
156
163
|
},
|
|
157
|
-
[clearDrag, frameLoop, isOverCanvas, screenToWorldFromContainer],
|
|
164
|
+
[clearDrag, frameLoop, isOverCanvas, liveRef, screenToWorldFromContainer],
|
|
158
165
|
);
|
|
159
166
|
|
|
160
167
|
useEffect(() => () => sessionRef.current?.cancel(), []);
|
|
@@ -162,10 +169,12 @@ export function useDragDrop<TS, TC>({
|
|
|
162
169
|
return { drag, startDrag };
|
|
163
170
|
}
|
|
164
171
|
|
|
172
|
+
/** Props for `DragOverlay`. */
|
|
165
173
|
export interface DragOverlayProps {
|
|
166
174
|
drag: DragState | null;
|
|
167
175
|
}
|
|
168
176
|
|
|
177
|
+
/** The ghost for `drag`, or nothing when no drag is live. */
|
|
169
178
|
export function DragOverlay({ drag }: DragOverlayProps) {
|
|
170
179
|
if (!drag) return null;
|
|
171
180
|
return <DragGhost item={drag.item} screenPos={drag.screenPos} />;
|
|
@@ -2,11 +2,14 @@ import { type ReactPortal, useEffect, useState } from 'react';
|
|
|
2
2
|
import { createPortal } from 'react-dom';
|
|
3
3
|
import type { PaletteItem, Point } from '../instrument/types';
|
|
4
4
|
|
|
5
|
+
/** Props for `DragGhost`. `screenPos` is in client pixels. */
|
|
5
6
|
export interface DragGhostProps {
|
|
6
7
|
item: PaletteItem;
|
|
7
8
|
screenPos: Point;
|
|
8
9
|
}
|
|
9
10
|
|
|
11
|
+
/** The dragged item's label, portaled to `document.body` just below-right of
|
|
12
|
+
* `screenPos`. Renders nothing until mounted, so it is SSR-safe. */
|
|
10
13
|
export function DragGhost({ item, screenPos }: DragGhostProps): ReactPortal | null {
|
|
11
14
|
const [mounted, setMounted] = useState(false);
|
|
12
15
|
useEffect(() => setMounted(true), []);
|
package/src/dragdrop/Palette.tsx
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import type { PointerEvent as ReactPointerEvent } from 'react';
|
|
2
2
|
import type { PaletteItem } from '../instrument/types';
|
|
3
3
|
|
|
4
|
+
/** Props for `Palette`. `onDragStart` is usually `useDragDrop`'s `startDrag`. */
|
|
4
5
|
export interface PaletteProps {
|
|
5
6
|
items: PaletteItem[];
|
|
6
7
|
onDragStart: (item: PaletteItem, e: ReactPointerEvent) => void;
|
|
7
8
|
className?: string;
|
|
8
9
|
}
|
|
9
10
|
|
|
11
|
+
/** One button per item, each starting a drag on pointerdown. */
|
|
10
12
|
export function Palette({ items, onDragStart, className }: PaletteProps) {
|
|
11
13
|
return (
|
|
12
14
|
<div className={className ? `lk-palette ${className}` : 'lk-palette'}>
|
package/src/index.test.ts
CHANGED
|
@@ -49,6 +49,11 @@ describe('public export surface', () => {
|
|
|
49
49
|
expect(labkit).toHaveProperty('useTrialState');
|
|
50
50
|
expect(labkit).toHaveProperty('useTrialId');
|
|
51
51
|
});
|
|
52
|
+
|
|
53
|
+
it('leaves the loupe to its own entry, so a lab without one does not load it', () => {
|
|
54
|
+
expect(labkit).not.toHaveProperty('TrialLoupe');
|
|
55
|
+
expect(labkit).not.toHaveProperty('useLoupe');
|
|
56
|
+
});
|
|
52
57
|
});
|
|
53
58
|
|
|
54
59
|
describe('ui re-exports', () => {
|
package/src/index.ts
CHANGED
|
@@ -80,6 +80,7 @@ export type {
|
|
|
80
80
|
AnnotationMeaning,
|
|
81
81
|
AnnotationPatch,
|
|
82
82
|
AnnotationQuery,
|
|
83
|
+
AnnotationSelectionMode,
|
|
83
84
|
AnnotationStatus,
|
|
84
85
|
AnnotationStoreOptions,
|
|
85
86
|
AnnotationsApi,
|
|
@@ -267,7 +268,6 @@ export * from './job';
|
|
|
267
268
|
export * from './lab';
|
|
268
269
|
export type { LayerListItem, LayerListProps, LayerMove } from './layers';
|
|
269
270
|
export { LayerList, moveLayers } from './layers';
|
|
270
|
-
export * from './loupe';
|
|
271
271
|
export * from './primitives';
|
|
272
272
|
export { SPECIMEN_SECTIONS, Specimen } from './specimen/Specimen';
|
|
273
273
|
export {
|
package/src/instrument/types.ts
CHANGED
|
@@ -6,7 +6,6 @@ import type { TrialContribution } from '../chrome/types';
|
|
|
6
6
|
import type { ConfigPath, ConfigSchema } from '../config/types';
|
|
7
7
|
import type { ConfigField } from '../controls/types';
|
|
8
8
|
import type { JobCapability, JobHandle } from '../job/types';
|
|
9
|
-
import type { LoupeDeclaration } from '../loupe/types';
|
|
10
9
|
import type { TrialInfo } from '../state/types';
|
|
11
10
|
import type { ToolCapability } from '../tools/types';
|
|
12
11
|
|
|
@@ -182,11 +181,6 @@ export interface Instrument<TS = unknown, TC = unknown, TItem = unknown> {
|
|
|
182
181
|
* is allowed to mean. Declaring it is what makes the trial provide the
|
|
183
182
|
* annotation overlay and its chrome. */
|
|
184
183
|
annotations?: AnnotationsCapability<TS, TC>;
|
|
185
|
-
/** Magnification. `true` takes every default and re-draws the instrument's
|
|
186
|
-
* canvas layers through a zoomed camera; an instrument whose content is DOM
|
|
187
|
-
* gives a `render` that draws it again at a camera it is handed. A function
|
|
188
|
-
* is re-read as the config changes, so a setting can drive the lens. */
|
|
189
|
-
loupe?: LoupeDeclaration<TS, TC> | ((config: TC) => LoupeDeclaration<TS, TC>);
|
|
190
184
|
/** Chrome this instrument contributes beyond what its capabilities imply. */
|
|
191
185
|
chrome?: TrialContribution[];
|
|
192
186
|
/** Work too slow to do during a render. The runtime starts it, aborts it on
|