@weasel-js/labkit 1.5.2 → 1.6.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 -7
- package/dist/_dts/{CanvasStackContext-CMMo180l.d.ts → CanvasStackContext-Wo6OwIVM.d.ts} +1 -1
- package/dist/_dts/FloatingPanel-7j0IcXwz.d.ts +30 -0
- package/dist/_dts/LayerList.d-BoELAMsM.d.ts +83 -0
- package/dist/_dts/{PrefsForm.d-DPy9I8It.d.ts → PrefsRow.d-DFpPIeIC.d.ts} +16 -1
- package/dist/_dts/PropertyField.d-D0ndfIIt.d.ts +404 -0
- package/dist/_dts/{Select.d-ZTiwzYyT.d.ts → Select.d-Cr8-zoup.d.ts} +105 -15
- package/dist/_dts/{usePanZoom-SSTZe410.d.ts → Stage-DWBBi8IU.d.ts} +80 -35
- package/dist/_dts/{index-DlKolEIZ.d.ts → index-CKyX0ME_.d.ts} +12 -13
- package/dist/_dts/{frac-B4hFJMiN.d.ts → types-B7yBTTF9.d.ts} +709 -651
- package/dist/_dts/{types-Db-3NNk1.d.ts → types-DTuViBvc.d.ts} +21 -2
- package/dist/_dts/{useTrialState-pnAE-UBD.d.ts → useTrialState-CMaV6UD4.d.ts} +2 -3
- package/dist/canvas/index.d.ts +41 -8
- package/dist/canvas/index.js +5 -3
- package/dist/chrome/index.d.ts +6 -6
- package/dist/chrome/index.js +9 -7
- package/dist/chunk-24W5VEJW.js +18 -0
- package/dist/chunk-24W5VEJW.js.map +1 -0
- package/dist/chunk-2RVCB2PT.js +3 -0
- package/dist/chunk-2RVCB2PT.js.map +1 -0
- package/dist/chunk-3RY6WL5T.js +38 -0
- package/dist/chunk-3RY6WL5T.js.map +1 -0
- package/dist/{chunk-CKUKF72R.js → chunk-4TT3PNPR.js} +68 -55
- package/dist/chunk-4TT3PNPR.js.map +1 -0
- package/dist/{chunk-KJCUERQF.js → chunk-6FLXL3I7.js} +9 -164
- package/dist/chunk-6FLXL3I7.js.map +1 -0
- package/dist/chunk-ASOK76ZH.js +431 -0
- package/dist/chunk-ASOK76ZH.js.map +1 -0
- package/dist/chunk-AWQG7FCC.js +19 -0
- package/dist/chunk-AWQG7FCC.js.map +1 -0
- package/dist/chunk-CGHF6RZV.js +300 -0
- package/dist/chunk-CGHF6RZV.js.map +1 -0
- package/dist/chunk-DWMSB4CA.js +228 -0
- package/dist/chunk-DWMSB4CA.js.map +1 -0
- package/dist/chunk-GHGO52SO.js +92 -0
- package/dist/chunk-GHGO52SO.js.map +1 -0
- package/dist/{chunk-LRI3MBGR.js → chunk-HKV7LRHY.js} +4 -4
- package/dist/{chunk-LRI3MBGR.js.map → chunk-HKV7LRHY.js.map} +1 -1
- package/dist/{chunk-EMXAIRUJ.js → chunk-IRTGUQ5O.js} +5 -4
- package/dist/chunk-IRTGUQ5O.js.map +1 -0
- package/dist/{chunk-H6ZAOWNE.js → chunk-JVBYV6TL.js} +3 -3
- package/dist/{chunk-H6ZAOWNE.js.map → chunk-JVBYV6TL.js.map} +1 -1
- package/dist/chunk-KHK5FGER.js +145 -0
- package/dist/chunk-KHK5FGER.js.map +1 -0
- package/dist/chunk-NEXR37YJ.js +144 -0
- package/dist/chunk-NEXR37YJ.js.map +1 -0
- package/dist/{chunk-A2ACAGW6.js → chunk-O6FFFOHS.js} +32 -7
- package/dist/chunk-O6FFFOHS.js.map +1 -0
- package/dist/chunk-QGMOA2YE.js +731 -0
- package/dist/chunk-QGMOA2YE.js.map +1 -0
- package/dist/{chunk-ERFMC6WU.js → chunk-S4NCI32V.js} +19 -10
- package/dist/chunk-S4NCI32V.js.map +1 -0
- package/dist/{chunk-753Y6HE6.js → chunk-SLAQPUI4.js} +4 -4
- package/dist/chunk-SLAQPUI4.js.map +1 -0
- package/dist/{chunk-REOBGYMC.js → chunk-TJ77A4A3.js} +3 -5
- package/dist/chunk-TJ77A4A3.js.map +1 -0
- package/dist/chunk-TYN6MLXX.js +13864 -0
- package/dist/chunk-TYN6MLXX.js.map +1 -0
- package/dist/{chunk-RS3HRQWU.js → chunk-WPLAANH4.js} +4 -3
- package/dist/chunk-WPLAANH4.js.map +1 -0
- package/dist/config/index.d.ts +51 -7
- package/dist/config/index.js +4 -5
- package/dist/controls/index.d.ts +92 -5
- package/dist/controls/index.js +5 -4
- package/dist/dragdrop/index.d.ts +9 -6
- package/dist/dragdrop/index.js +3 -2
- package/dist/index.d.ts +108 -154
- package/dist/index.js +456 -600
- package/dist/index.js.map +1 -1
- package/dist/layers/index.d.ts +5 -35
- package/dist/layers/index.js +3 -3
- package/dist/loupe/index.d.ts +21 -17
- package/dist/loupe/index.js +5 -3
- package/dist/overview/index.d.ts +56 -0
- package/dist/overview/index.js +288 -0
- package/dist/overview/index.js.map +1 -0
- package/dist/passthrough/weasel-ui.d.ts +434 -231
- package/dist/passthrough/weasel-ui.js +2 -1
- package/dist/primitives/index.d.ts +19 -54
- package/dist/primitives/index.js +6 -4
- package/dist/state/index.d.ts +8 -7
- package/dist/state/index.js +4 -5
- package/dist/state/index.js.map +1 -1
- package/dist/styles.css +360 -174
- package/dist/undo/index.d.ts +5 -4
- package/package.json +13 -13
- package/src/annotations/frac.test.ts +7 -0
- package/src/annotations/index.ts +1 -0
- package/src/annotations/paint.test.ts +21 -0
- package/src/annotations/paint.ts +12 -0
- package/src/annotations/store.test.ts +57 -0
- package/src/annotations/store.ts +25 -6
- package/src/annotations/toolMap.test.ts +37 -2
- package/src/annotations/toolMap.ts +13 -0
- package/src/annotations/types.ts +30 -2
- package/src/canvas/AGENTS.md +8 -10
- package/src/canvas/CameraInput.test.tsx +136 -0
- package/src/canvas/CameraInput.tsx +311 -0
- package/src/canvas/CanvasStack.tsx +42 -58
- package/src/canvas/LinkedCursor.less +16 -0
- package/src/canvas/LinkedCursor.tsx +54 -0
- package/src/canvas/Stage.test.tsx +18 -0
- package/src/canvas/Stage.tsx +42 -37
- package/src/canvas/cameraView.test.ts +60 -0
- package/src/canvas/cameraView.ts +61 -0
- package/src/canvas/index.ts +11 -2
- package/src/chrome/regions/SidebarRegion.less +197 -0
- package/src/chrome/regions/SidebarRegion.test.tsx +27 -0
- package/src/chrome/regions/SidebarRegion.tsx +7 -2
- package/src/chrome/types.ts +5 -2
- package/src/config/autoConfig.ts +2 -2
- package/src/config/builder.ts +21 -2
- package/src/config/entry.test.ts +10 -0
- package/src/config/fromConfigField.ts +1 -0
- package/src/config/index.ts +5 -0
- package/src/config/resolve.ts +11 -1
- package/src/config/rules.ts +2 -0
- package/src/config/sectionTree.test.ts +152 -0
- package/src/config/sectionTree.ts +134 -0
- package/src/config/types.ts +21 -0
- package/src/controls/ControlPanel.less +4 -0
- package/src/controls/ControlPanel.stories.tsx +55 -2
- package/src/controls/ControlPanel.test.tsx +40 -3
- package/src/controls/ControlPanel.tsx +134 -336
- package/src/controls/inDialog.test.tsx +100 -0
- package/src/controls/inDialog.tsx +38 -0
- package/src/controls/index.ts +1 -0
- package/src/index.test.ts +1 -1
- package/src/index.ts +18 -29
- package/src/instrument/SineWave.smoke.test.tsx +2 -0
- package/src/instrument/types.ts +15 -3
- package/src/lab/Lab.nebula.test.tsx +40 -0
- package/src/lab/Lab.test.tsx +3 -1
- package/src/lab/Lab.tsx +42 -19
- package/src/lab/LabBoundary.test.tsx +66 -0
- package/src/lab/LabBoundary.tsx +56 -0
- package/src/lab/LabHeader.tsx +4 -23
- package/src/lab/LabRoot.tsx +2 -3
- package/src/lab/LabShell.less +0 -1
- package/src/lab/LabShell.stories.tsx +15 -0
- package/src/lab/LabShell.test.tsx +18 -0
- package/src/lab/LabShell.tsx +14 -1
- package/src/lab/LabSwitcher.less +1 -1
- package/src/lab/LabSwitcher.test.tsx +41 -0
- package/src/lab/LabSwitcher.tsx +30 -8
- package/src/lab/index.ts +1 -0
- package/src/layers/AGENTS.md +18 -74
- package/src/layers/index.ts +4 -2
- package/src/loupe/AGENTS.md +7 -6
- package/src/loupe/LoupeGestures.tsx +14 -9
- package/src/loupe/TrialLoupe.tsx +5 -5
- package/src/loupe/index.ts +1 -0
- package/src/loupe/loupeActions.ts +6 -2
- package/src/overview/AGENTS.md +38 -0
- package/src/overview/OverviewMarks.test.tsx +100 -0
- package/src/overview/OverviewMarks.tsx +93 -0
- package/src/overview/TrialOverview.less +53 -0
- package/src/overview/TrialOverview.stories.tsx +50 -0
- package/src/overview/TrialOverview.test.tsx +116 -0
- package/src/overview/TrialOverview.tsx +374 -0
- package/src/overview/index.ts +2 -0
- package/src/passthrough/weasel-ui.ts +32 -10
- package/src/primitives/index.ts +0 -2
- package/src/specimen/Specimen.tsx +59 -56
- package/src/state/labRecords.test.ts +4 -1
- package/src/state/store.test.ts +4 -4
- package/src/state/types.ts +2 -1
- package/src/state/useTrialState.test.tsx +1 -1
- package/src/styles.less +6 -2
- package/src/theme/Interstellar.stories.tsx +8 -13
- package/src/theme/base.less +3 -20
- package/src/theme/interstellar.test.ts +6 -3
- package/src/theme/interstellar.theme.json +0 -56
- package/src/theme/interstellar.ts +2 -2
- package/src/tools/labTool.ts +6 -2
- package/src/trial/Trial.annotations.test.tsx +16 -0
- package/src/trial/Trial.canvas.test.tsx +1 -1
- package/src/trial/Trial.loupe.test.tsx +73 -1
- package/src/trial/Trial.stories.tsx +2 -0
- package/src/trial/Trial.tsx +47 -9
- package/src/trial/trialOps.test.ts +4 -4
- package/dist/_dts/index-8LiKe3A-.d.ts +0 -410
- package/dist/chunk-2AYGEN57.js +0 -32
- package/dist/chunk-2AYGEN57.js.map +0 -1
- package/dist/chunk-6BUQ77ZV.js +0 -18
- package/dist/chunk-6BUQ77ZV.js.map +0 -1
- package/dist/chunk-753Y6HE6.js.map +0 -1
- package/dist/chunk-A2ACAGW6.js.map +0 -1
- package/dist/chunk-CKUKF72R.js.map +0 -1
- package/dist/chunk-D35PFTS7.js +0 -170
- package/dist/chunk-D35PFTS7.js.map +0 -1
- package/dist/chunk-EMXAIRUJ.js.map +0 -1
- package/dist/chunk-ERFMC6WU.js.map +0 -1
- package/dist/chunk-FKLT3ITC.js +0 -590
- package/dist/chunk-FKLT3ITC.js.map +0 -1
- package/dist/chunk-KJCUERQF.js.map +0 -1
- package/dist/chunk-LASCBWW7.js +0 -13456
- package/dist/chunk-LASCBWW7.js.map +0 -1
- package/dist/chunk-REOBGYMC.js.map +0 -1
- package/dist/chunk-RS3HRQWU.js.map +0 -1
- package/dist/chunk-TT5C5S6R.js +0 -368
- package/dist/chunk-TT5C5S6R.js.map +0 -1
- package/dist/ui/layers/index.d.ts +0 -66
- package/dist/ui/layers/index.js +0 -3
- package/dist/ui/layers/index.js.map +0 -1
- package/src/canvas/usePanZoom.test.ts +0 -244
- package/src/canvas/usePanZoom.ts +0 -147
- package/src/lab/useSystemMode.ts +0 -30
- package/src/layers/LayerList.less +0 -93
- package/src/layers/LayerList.stories.tsx +0 -66
- package/src/layers/LayerList.test.tsx +0 -232
- package/src/layers/LayerList.tsx +0 -268
- package/src/primitives/Sidebar.less +0 -112
- package/src/primitives/Sidebar.stories.tsx +0 -38
- package/src/primitives/Sidebar.test.tsx +0 -74
- package/src/primitives/Sidebar.tsx +0 -35
- package/src/ui/layers/index.ts +0 -5
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { ThemeContext } from '@weasel-js/theme/react';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
import { AnnotationsContext } from '../annotations/AnnotationsContext';
|
|
4
|
+
import { AnnotationPreloadContext } from '../annotations/preload';
|
|
5
|
+
import { CameraWheelContext } from '../canvas/CameraWheelContext';
|
|
6
|
+
import { CanvasStackContext } from '../canvas/CanvasStackContext';
|
|
7
|
+
import { LabStoreContext, TrialIdContext } from '../state/context';
|
|
8
|
+
import { PersistenceContext } from '../state/Persistence';
|
|
9
|
+
import { SurfaceCanvasContext, SurfaceContext } from '../surface/SurfaceContext';
|
|
10
|
+
import { TrialDragContext } from '../trial/TrialDragContext';
|
|
11
|
+
import { LabContext } from './LabContext';
|
|
12
|
+
import { PanelHostContext } from './panelHost';
|
|
13
|
+
|
|
14
|
+
/** Props for `<LabBoundary>`. */
|
|
15
|
+
export interface LabBoundaryProps {
|
|
16
|
+
children: ReactNode;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Renders its children as though no lab, trial or theme were above them: every context a `<Lab>` or a
|
|
21
|
+
* `<Trial>` publishes reads as absent below it, so a labkit piece mounted inside a trial behaves the way it
|
|
22
|
+
* does on a bare page. A `<LabRoot>` under it applies its own theme instead of deferring to the lab's, and a
|
|
23
|
+
* `usePersistedState` under it keeps local state instead of writing into the enclosing lab's records.
|
|
24
|
+
*
|
|
25
|
+
* For a host that shows arbitrary content inside a trial, such as a component workshop; a lab's own chrome
|
|
26
|
+
* never needs it.
|
|
27
|
+
*/
|
|
28
|
+
export function LabBoundary({ children }: LabBoundaryProps) {
|
|
29
|
+
return (
|
|
30
|
+
<LabStoreContext.Provider value={null}>
|
|
31
|
+
<PersistenceContext.Provider value={null}>
|
|
32
|
+
<AnnotationPreloadContext.Provider value={null}>
|
|
33
|
+
<LabContext.Provider value={null}>
|
|
34
|
+
<PanelHostContext.Provider value={null}>
|
|
35
|
+
<SurfaceContext.Provider value={null}>
|
|
36
|
+
<SurfaceCanvasContext.Provider value={{ over: null, under: null }}>
|
|
37
|
+
<TrialIdContext.Provider value={null}>
|
|
38
|
+
<AnnotationsContext.Provider value={null}>
|
|
39
|
+
<CameraWheelContext.Provider value={null}>
|
|
40
|
+
<CanvasStackContext.Provider value={null}>
|
|
41
|
+
<TrialDragContext.Provider value={null}>
|
|
42
|
+
<ThemeContext.Provider value={null}>{children}</ThemeContext.Provider>
|
|
43
|
+
</TrialDragContext.Provider>
|
|
44
|
+
</CanvasStackContext.Provider>
|
|
45
|
+
</CameraWheelContext.Provider>
|
|
46
|
+
</AnnotationsContext.Provider>
|
|
47
|
+
</TrialIdContext.Provider>
|
|
48
|
+
</SurfaceCanvasContext.Provider>
|
|
49
|
+
</SurfaceContext.Provider>
|
|
50
|
+
</PanelHostContext.Provider>
|
|
51
|
+
</LabContext.Provider>
|
|
52
|
+
</AnnotationPreloadContext.Provider>
|
|
53
|
+
</PersistenceContext.Provider>
|
|
54
|
+
</LabStoreContext.Provider>
|
|
55
|
+
);
|
|
56
|
+
}
|
package/src/lab/LabHeader.tsx
CHANGED
|
@@ -1,22 +1,7 @@
|
|
|
1
|
-
import { AddIcon
|
|
2
|
-
import {
|
|
3
|
-
import type { LabMode } from '../state/types';
|
|
1
|
+
import { AddIcon } from '@weasel-js/ui';
|
|
2
|
+
import { ColorModeControl, MenuButton } from '../passthrough/weasel-ui';
|
|
4
3
|
import { useLabContext } from './LabContext';
|
|
5
4
|
|
|
6
|
-
// The glyph is the segment's content and the word is its accessible name, so
|
|
7
|
-
// the bar stays a three-way radiogroup announcing Auto / Light / Dark.
|
|
8
|
-
const MODES = [
|
|
9
|
-
{ value: 'auto' as LabMode, label: 'Auto', glyph: <ModeAutoIcon size={14} /> },
|
|
10
|
-
{ value: 'light' as LabMode, label: 'Light', glyph: <ModeLightIcon size={14} /> },
|
|
11
|
-
{ value: 'dark' as LabMode, label: 'Dark', glyph: <ModeDarkIcon size={14} /> },
|
|
12
|
-
];
|
|
13
|
-
|
|
14
|
-
const MODE_ITEMS = MODES.map(({ value, label, glyph }) => ({
|
|
15
|
-
value,
|
|
16
|
-
label: glyph,
|
|
17
|
-
ariaLabel: label,
|
|
18
|
-
}));
|
|
19
|
-
|
|
20
5
|
/** The controls `<Lab>` puts in its header: add a trial, and choose the color
|
|
21
6
|
* mode. Both drive `LabContext`, which carried them with no UI at all — so
|
|
22
7
|
* every consumer rebuilt these two. Rendered before a consumer's own header
|
|
@@ -45,15 +30,11 @@ export function LabHeader({ addTrial = true }: { addTrial?: boolean }) {
|
|
|
45
30
|
/>
|
|
46
31
|
)}
|
|
47
32
|
|
|
48
|
-
<
|
|
33
|
+
<ColorModeControl
|
|
49
34
|
className="lk-lab-header__mode"
|
|
50
|
-
ariaLabel="Color mode"
|
|
51
35
|
variant="flat"
|
|
52
|
-
items={MODE_ITEMS}
|
|
53
36
|
value={lab.mode}
|
|
54
|
-
onChange={
|
|
55
|
-
if (next) lab.setMode(next);
|
|
56
|
-
}}
|
|
37
|
+
onChange={lab.setMode}
|
|
57
38
|
/>
|
|
58
39
|
</>
|
|
59
40
|
);
|
package/src/lab/LabRoot.tsx
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { ThemeProvider, useThemeOptional } from '@weasel-js/theme/react';
|
|
1
|
+
import { ThemeProvider, useResolvedColorMode, useThemeOptional } from '@weasel-js/theme/react';
|
|
2
2
|
import type { ReactNode } from 'react';
|
|
3
3
|
import type { LabMode } from '../state/types';
|
|
4
4
|
import { interstellarTheme } from '../theme/interstellar';
|
|
5
|
-
import { useResolvedMode } from './useSystemMode';
|
|
6
5
|
|
|
7
6
|
/** Props for `<LabRoot>`. */
|
|
8
7
|
export interface LabRootProps {
|
|
@@ -24,7 +23,7 @@ export interface LabRootProps {
|
|
|
24
23
|
* Pair it with `import '@weasel-js/labkit/styles.css'`, which is what defines
|
|
25
24
|
* the rules this element scopes. */
|
|
26
25
|
export function LabRoot({ children, mode = 'auto', className }: LabRootProps) {
|
|
27
|
-
const resolved =
|
|
26
|
+
const resolved = useResolvedColorMode(mode);
|
|
28
27
|
const outer = useThemeOptional();
|
|
29
28
|
|
|
30
29
|
// Overlays portal here rather than to the themed wrapper above: `.lk-root`
|
package/src/lab/LabShell.less
CHANGED
|
@@ -86,3 +86,18 @@ export const HeaderControlsLineUp: Story = {
|
|
|
86
86
|
await expect(middles).toEqual(middles.map(() => middles[0]));
|
|
87
87
|
},
|
|
88
88
|
};
|
|
89
|
+
|
|
90
|
+
// One document serving its pages from the hash: the open route is the one the
|
|
91
|
+
// switcher marks, and each entry is a link to its fragment.
|
|
92
|
+
export const HashRoutedPages: Story = {
|
|
93
|
+
args: {
|
|
94
|
+
title: 'Registry',
|
|
95
|
+
pages: [
|
|
96
|
+
{ href: '/app/', label: 'App' },
|
|
97
|
+
{ href: '#/dev/tools', label: 'Tools' },
|
|
98
|
+
{ href: '#/dev/registry', label: 'Registry' },
|
|
99
|
+
],
|
|
100
|
+
path: '/app/#/dev/registry?entry=rect',
|
|
101
|
+
children: <p>Open the title to see the current route marked.</p>,
|
|
102
|
+
},
|
|
103
|
+
};
|
|
@@ -47,6 +47,24 @@ describe('LabShell', () => {
|
|
|
47
47
|
expect(container.querySelector('.lk-root')?.hasAttribute('data-wzl-portal-host')).toBe(true);
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
+
test('holds the document title while mounted when asked to', () => {
|
|
51
|
+
document.title = 'before';
|
|
52
|
+
const { unmount } = render(
|
|
53
|
+
<LabShell title="t" documentTitle="Page title">
|
|
54
|
+
x
|
|
55
|
+
</LabShell>,
|
|
56
|
+
);
|
|
57
|
+
expect(document.title).toBe('Page title');
|
|
58
|
+
unmount();
|
|
59
|
+
expect(document.title).toBe('before');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('leaves the document title alone by default', () => {
|
|
63
|
+
document.title = 'before';
|
|
64
|
+
render(<LabShell title="t">x</LabShell>);
|
|
65
|
+
expect(document.title).toBe('before');
|
|
66
|
+
});
|
|
67
|
+
|
|
50
68
|
test('always applies lk-root class', () => {
|
|
51
69
|
const { container } = render(<LabShell title="t">x</LabShell>);
|
|
52
70
|
expect(container.querySelector('.lk-root')).not.toBeNull();
|
package/src/lab/LabShell.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ReactNode, useEffect } from 'react';
|
|
2
2
|
import type { LabMode } from '../state/types';
|
|
3
3
|
import { LabRoot } from './LabRoot';
|
|
4
4
|
import type { LabPage } from './LabSwitcher';
|
|
@@ -20,6 +20,9 @@ export interface LabShellProps {
|
|
|
20
20
|
/** The path the switcher reads to mark the open page. Defaults to the
|
|
21
21
|
* current location. */
|
|
22
22
|
path?: string;
|
|
23
|
+
/** Set as `document.title` while the shell is mounted, and the previous
|
|
24
|
+
* title restored after. Omitted, the document's title is left alone. */
|
|
25
|
+
documentTitle?: string;
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
/** Page frame for a lab: a titled header, a body, and an optional footer,
|
|
@@ -33,7 +36,17 @@ export function LabShell({
|
|
|
33
36
|
mode = 'auto',
|
|
34
37
|
pages,
|
|
35
38
|
path,
|
|
39
|
+
documentTitle,
|
|
36
40
|
}: LabShellProps) {
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (documentTitle === undefined) return;
|
|
43
|
+
const previous = document.title;
|
|
44
|
+
document.title = documentTitle;
|
|
45
|
+
return () => {
|
|
46
|
+
document.title = previous;
|
|
47
|
+
};
|
|
48
|
+
}, [documentTitle]);
|
|
49
|
+
|
|
37
50
|
return (
|
|
38
51
|
<LabRoot mode={mode} className="lk-shell">
|
|
39
52
|
<header className="lk-shell-header">
|
package/src/lab/LabSwitcher.less
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
// hyphen and the control becomes three ragged lines with a caret adrift.
|
|
27
27
|
white-space: nowrap;
|
|
28
28
|
|
|
29
|
-
&:hover { color: var(--wzl-accent); }
|
|
29
|
+
&:hover { color: var(--wzl-accent-fg); }
|
|
30
30
|
|
|
31
31
|
&:focus-visible {
|
|
32
32
|
outline: 2px solid var(--wzl-focus-ring, currentColor);
|
|
@@ -9,6 +9,8 @@ const PAGES = [
|
|
|
9
9
|
{ href: '/ingest', label: 'Ingestion' },
|
|
10
10
|
];
|
|
11
11
|
|
|
12
|
+
const open2 = (name: string) =>
|
|
13
|
+
fireEvent.click(screen.getByRole('button', { name: new RegExp(name) }));
|
|
12
14
|
const open = () => fireEvent.click(screen.getByRole('button', { name: /corpus stats/ }));
|
|
13
15
|
|
|
14
16
|
describe('currentPage', () => {
|
|
@@ -27,6 +29,45 @@ describe('currentPage', () => {
|
|
|
27
29
|
it('reports -1 for a page that is not one of them', () => {
|
|
28
30
|
expect(currentPage('/bench', PAGES)).toBe(-1);
|
|
29
31
|
});
|
|
32
|
+
|
|
33
|
+
// One document serving its pages from the hash: every href shares a path, so
|
|
34
|
+
// only the fragment can tell them apart.
|
|
35
|
+
const ROUTES = [
|
|
36
|
+
{ href: '/app/', label: 'App' },
|
|
37
|
+
{ href: '#/dev/tools', label: 'Tools' },
|
|
38
|
+
{ href: '#/dev/registry', label: 'Registry' },
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
it('tells hash routes on one document apart', () => {
|
|
42
|
+
expect(currentPage('/app/#/dev/registry', ROUTES)).toBe(2);
|
|
43
|
+
expect(currentPage('/app/#/dev/tools', ROUTES)).toBe(1);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('matches a hash route through its own query and sub-routes', () => {
|
|
47
|
+
expect(currentPage('/app/#/dev/tools?bundle=minimal', ROUTES)).toBe(1);
|
|
48
|
+
expect(currentPage('/app/#/dev/tools/', ROUTES)).toBe(1);
|
|
49
|
+
expect(currentPage('/app/#/dev/registry/tool/rect', ROUTES)).toBe(2);
|
|
50
|
+
expect(currentPage('/app/#/dev/toolsets', ROUTES)).toBe(0);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('falls back to the page without a hash when no route matches', () => {
|
|
54
|
+
expect(currentPage('/app/', ROUTES)).toBe(0);
|
|
55
|
+
expect(currentPage('/app/#/elsewhere', ROUTES)).toBe(0);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('reads the hash off the current location by default', () => {
|
|
59
|
+
window.history.replaceState(null, '', '/app/#/dev/registry');
|
|
60
|
+
try {
|
|
61
|
+
render(<LabSwitcher title="Registry" pages={ROUTES} />);
|
|
62
|
+
open2('Registry');
|
|
63
|
+
expect(screen.getByRole('menuitem', { name: 'Registry' })).toHaveAttribute(
|
|
64
|
+
'aria-current',
|
|
65
|
+
'page',
|
|
66
|
+
);
|
|
67
|
+
} finally {
|
|
68
|
+
window.history.replaceState(null, '', '/');
|
|
69
|
+
}
|
|
70
|
+
});
|
|
30
71
|
});
|
|
31
72
|
|
|
32
73
|
describe('<LabSwitcher>', () => {
|
package/src/lab/LabSwitcher.tsx
CHANGED
|
@@ -2,7 +2,8 @@ import { useCallback, useEffect, useId, useRef, useState } from 'react';
|
|
|
2
2
|
|
|
3
3
|
/** One page in a project's set of labs. */
|
|
4
4
|
export interface LabPage {
|
|
5
|
-
/** The page's own URL, and how the switcher knows which page is open.
|
|
5
|
+
/** The page's own URL, and how the switcher knows which page is open. A
|
|
6
|
+
* hash route (`#/dev/tools`) names a page on the current document. */
|
|
6
7
|
href: string;
|
|
7
8
|
label: string;
|
|
8
9
|
}
|
|
@@ -18,18 +19,39 @@ export interface LabSwitcherProps {
|
|
|
18
19
|
className?: string;
|
|
19
20
|
}
|
|
20
21
|
|
|
22
|
+
/** A URL split into the document it names and the hash route on it, each
|
|
23
|
+
* without its query, trailing slash or `.html`. */
|
|
24
|
+
function locate(url: string): { doc: string; route: string } {
|
|
25
|
+
const hash = url.indexOf('#');
|
|
26
|
+
const trim = (part: string) => part.replace(/\?.*$/, '').replace(/\/$/, '');
|
|
27
|
+
return {
|
|
28
|
+
doc: trim(hash < 0 ? url : url.slice(0, hash)).replace(/\.html$/, ''),
|
|
29
|
+
route: hash < 0 ? '' : trim(url.slice(hash + 1)),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
21
33
|
/** Which of `pages` a path is on, or -1.
|
|
22
34
|
*
|
|
23
35
|
* Matched on the end of the path so a query string, a trailing slash or a
|
|
24
36
|
* leftover `.html` cannot lose it: a project whose dev server maps `/stats`
|
|
25
37
|
* to `stats.html` serves both spellings, and a bookmark from before the URLs
|
|
26
|
-
* lost their extension still resolves to the same page.
|
|
38
|
+
* lost their extension still resolves to the same page.
|
|
39
|
+
*
|
|
40
|
+
* A page whose `href` carries a hash is a route on a single document and wins
|
|
41
|
+
* when the path's hash is that route or one under it. A page without one
|
|
42
|
+
* ignores the hash, so an in-page anchor does not lose it. */
|
|
27
43
|
export function currentPage(path: string, pages: readonly LabPage[]): number {
|
|
28
|
-
const here = path
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
.
|
|
32
|
-
|
|
44
|
+
const here = locate(path);
|
|
45
|
+
const onDoc = (doc: string) => here.doc.endsWith(doc);
|
|
46
|
+
const routed = pages.findIndex((p) => {
|
|
47
|
+
if (!p.href.includes('#')) return false;
|
|
48
|
+
const page = locate(p.href);
|
|
49
|
+
return (
|
|
50
|
+
onDoc(page.doc) && (here.route === page.route || here.route.startsWith(`${page.route}/`))
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
if (routed >= 0) return routed;
|
|
54
|
+
return pages.findIndex((p) => !p.href.includes('#') && onDoc(locate(p.href).doc));
|
|
33
55
|
}
|
|
34
56
|
|
|
35
57
|
/** A project's lab title, doubling as the way to reach its other labs.
|
|
@@ -47,7 +69,7 @@ export function LabSwitcher({ title, pages, path, className }: LabSwitcherProps)
|
|
|
47
69
|
const root = useRef<HTMLDivElement>(null);
|
|
48
70
|
const menuId = useId();
|
|
49
71
|
const here = currentPage(
|
|
50
|
-
path ?? (typeof window === 'undefined' ? '' : window.location.pathname),
|
|
72
|
+
path ?? (typeof window === 'undefined' ? '' : window.location.pathname + window.location.hash),
|
|
51
73
|
pages,
|
|
52
74
|
);
|
|
53
75
|
|
package/src/lab/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export type { LabProps } from './Lab';
|
|
2
2
|
export { Lab } from './Lab';
|
|
3
|
+
export { LabBoundary, type LabBoundaryProps } from './LabBoundary';
|
|
3
4
|
export type { LabContextValue } from './LabContext';
|
|
4
5
|
export { LabContext, useLabContext } from './LabContext';
|
|
5
6
|
export type { LabPaletteProps } from './LabPalette';
|
package/src/layers/AGENTS.md
CHANGED
|
@@ -1,33 +1,11 @@
|
|
|
1
1
|
# Layers — Agent Guide
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`src/layers/` re-exports `<LayerList>` from `@weasel-js/ui`, along with the
|
|
4
|
+
`layers` capability types. The component itself — rows, cards, nesting,
|
|
5
|
+
selection, visibility, reorder — is documented at its source in
|
|
6
|
+
`packages/ui/src/components/LayerList/`.
|
|
4
7
|
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
| File | Role |
|
|
8
|
-
|---|---|
|
|
9
|
-
| `LayerList.tsx` | Layer toggle/reorder list, flat or nested (sidebar widget) |
|
|
10
|
-
| `LayerList.less` | Row styling, drag handle, pinned-row variant, subtree indent |
|
|
11
|
-
| `LayerList.stories.tsx` | Flat and nested renderings |
|
|
12
|
-
|
|
13
|
-
## Props
|
|
14
|
-
|
|
15
|
-
```ts
|
|
16
|
-
interface LayerListProps {
|
|
17
|
-
layers: LayerTreeNode[]; // LayerDescriptor + { children?, defaultCollapsed? }
|
|
18
|
-
visibility: Record<string, boolean>; // missing key → visible
|
|
19
|
-
onReorder: (newOrder: LayerTreeNode[]) => void;
|
|
20
|
-
onToggle: (id: string, visible: boolean) => void;
|
|
21
|
-
collapsedIds?: readonly string[]; // supplying it makes collapse controlled
|
|
22
|
-
onCollapsedChange?: (ids: string[]) => void;
|
|
23
|
-
className?: string;
|
|
24
|
-
}
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
`LayerDescriptor` lives in `src/instrument/types.ts`; `LayerTreeNode` extends it
|
|
28
|
-
in `LayerList.tsx`, so a plain `LayerDescriptor[]` is still a valid `layers`.
|
|
29
|
-
|
|
30
|
-
## How it integrates
|
|
8
|
+
## How the capability uses it
|
|
31
9
|
|
|
32
10
|
The instrument declares layer ids:
|
|
33
11
|
|
|
@@ -35,53 +13,19 @@ The instrument declares layer ids:
|
|
|
35
13
|
layers: { ids: ['grid', 'plants'] }
|
|
36
14
|
```
|
|
37
15
|
|
|
38
|
-
`Trial.tsx`
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
Setting `alwaysOn: true` on a `LayerDescriptor` does two things:
|
|
45
|
-
|
|
46
|
-
1. The row renders with a 🔒 badge instead of a drag handle and visibility checkbox.
|
|
47
|
-
2. The row is excluded from reorder operations — pinned rows always sort to the bottom of the rendered list.
|
|
48
|
-
|
|
49
|
-
This is intended for legend/HUD layers that should never be toggled off. Visibility for pinned rows is **not** read from the `visibility` prop; they always render.
|
|
50
|
-
|
|
51
|
-
## Nesting
|
|
52
|
-
|
|
53
|
-
A node with `children` renders an expandable subtree. The expand control is
|
|
54
|
-
`<Disclosure>` from `@weasel-js/ui` — the same twisty every other collapsible
|
|
55
|
-
surface uses.
|
|
56
|
-
|
|
57
|
-
Reordering is scoped to siblings: a drag moves a row within its own parent's
|
|
58
|
-
child list and never reparents it. `onReorder` still receives the whole tree,
|
|
59
|
-
with only that sibling group's order changed.
|
|
60
|
-
|
|
61
|
-
The twisty column appears only when some node in the tree has children — a flat
|
|
62
|
-
list renders exactly the rows it always did. Within a tree, childless rows get
|
|
63
|
-
`.lk-layer-list__twisty-gap` so labels stay aligned down a level.
|
|
64
|
-
|
|
65
|
-
Collapse is uncontrolled by default, seeded once from each node's
|
|
66
|
-
`defaultCollapsed`. Pass `collapsedIds` to own it; `onCollapsedChange` fires
|
|
67
|
-
either way.
|
|
68
|
-
|
|
69
|
-
## Drag handle customization
|
|
70
|
-
|
|
71
|
-
The drag handle is a button with class `lk-layer-list__handle` holding `<DragHandleGlyph>` (from `@weasel-js/ui`), the same grip `LayerStack` uses. Its padding is transparent and cancelled by an equal negative margin, so the grab target is larger than the drawn dots without widening the row. Row pitch — height plus row gap — is measured from the DOM when a drag starts, so restyling the row does not skew drag distance. It used to be a hardcoded `28`, which had already drifted from the rendered height by the time it was found.
|
|
72
|
-
|
|
73
|
-
Pointer capture is acquired on `pointerdown` and released on `pointerup`, so dragging works across the document without requiring window-level listeners.
|
|
74
|
-
|
|
75
|
-
## Empty state
|
|
76
|
-
|
|
77
|
-
If `layers.length === 0`, renders `.lk-layer-list__empty` with the text "No layers". Trial already guards this case (`layerDescriptors.length > 0`) so the empty state is rare in practice.
|
|
16
|
+
`Trial.tsx` turns each id into a `LayerDescriptor` (`label === id` unless the
|
|
17
|
+
entry is a descriptor), keeps `visibility` and `layerOrder` in local state, and
|
|
18
|
+
builds the list's items from them in the order the canvas draws. The order a
|
|
19
|
+
drag produces is applied to `instrument.canvas.layers` before they reach
|
|
20
|
+
`<CanvasStack>`, and each toggle and reorder emits `layers.toggle` /
|
|
21
|
+
`layers.reorder` on the bus.
|
|
78
22
|
|
|
79
|
-
|
|
23
|
+
`layers.ids` and `canvas.layers[].id` are assumed to match. A canvas layer not
|
|
24
|
+
in `layers.ids` keeps its default order and is always visible.
|
|
80
25
|
|
|
81
|
-
|
|
82
|
-
- Multi-select reorder (current implementation is single-row)
|
|
83
|
-
- Right-click context menu
|
|
84
|
-
- Reparenting by drag (nesting renders, but a drag stays within one parent)
|
|
85
|
-
- Solo/mute UI common in DAW-style apps
|
|
26
|
+
## `alwaysOn`
|
|
86
27
|
|
|
87
|
-
|
|
28
|
+
A descriptor with `alwaysOn: true` becomes a `locked` item, pinned below the
|
|
29
|
+
rest: it shows a lock in place of the grip and has no visibility checkbox,
|
|
30
|
+
cannot be dragged, and no drag can cross it. It always renders, whatever
|
|
31
|
+
`visibility` says. Intended for legend and HUD layers.
|
package/src/layers/index.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// LayerList lives in `@weasel-js/ui`. Named, not `export *` — a star re-export
|
|
2
|
+
// of an external package emits no binding in the bundle.
|
|
3
|
+
export type { LayerListItem, LayerListProps, LayerMove } from '@weasel-js/ui';
|
|
4
|
+
export { LayerList, moveLayers } from '@weasel-js/ui';
|
|
1
5
|
export type { LayerCapability, LayerDescriptor } from '../instrument/types';
|
|
2
|
-
export type { LayerListProps, LayerTreeNode } from './LayerList';
|
|
3
|
-
export { LayerList } from './LayerList';
|
package/src/loupe/AGENTS.md
CHANGED
|
@@ -14,7 +14,7 @@ This directory binds that model to a labkit trial and draws it.
|
|
|
14
14
|
| `types.ts` | `LoupeCapability`, and `resolveLoupe` filling in every default |
|
|
15
15
|
| `useLoupe.ts` | The model over a host element, and pointer aiming |
|
|
16
16
|
| `loupeActions.ts` | `loupe.peek` and `loupe.magnify`, as `Action` descriptors |
|
|
17
|
-
| `LoupeGestures.tsx` | Registers those and mounts
|
|
17
|
+
| `LoupeGestures.tsx` | Registers those, and mounts a dispatcher on the host when no camera has one there |
|
|
18
18
|
| `TrialLoupe.tsx` | Picks the painter and mounts the lens |
|
|
19
19
|
| `LoupeBubble.tsx` | The circular clip, positioned on the aim |
|
|
20
20
|
| `CanvasLoupe.tsx` | Painter for a `<CanvasStack>` |
|
|
@@ -46,11 +46,12 @@ StrictMode mounts / unmounts / mounts every effect — so disposing in the
|
|
|
46
46
|
cleanup leaves a magnifier that draws but silently ignores every aim. It owns
|
|
47
47
|
no resources; unmounting only reports the lens gone.
|
|
48
48
|
|
|
49
|
-
**The wheel is taken from
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
the
|
|
53
|
-
dispatcher
|
|
49
|
+
**The wheel is taken from the camera by rank, and handed back by declining.**
|
|
50
|
+
In a trial the loupe's actions join the camera's dispatcher (`CameraInput`).
|
|
51
|
+
`loupe.magnify` sits in the hotkey tier, so while the lens is up it outranks
|
|
52
|
+
the camera's wheel zoom; while it is down its `enabled` returns a disabled
|
|
53
|
+
reason and the dispatcher falls through to the zoom. Outside a camera — the DOM
|
|
54
|
+
`.lk-trial__loupe-host` — `<LoupeGestures>` mounts a dispatcher of its own.
|
|
54
55
|
|
|
55
56
|
**Aiming is a plain listener because a hover is not a gesture.**
|
|
56
57
|
`GESTURE_DESCRIPTORS` names no continuous-motion gesture, so `pointermove` /
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* weasel binding does.
|
|
2
|
+
* Registers the loupe's actions, and mounts the gesture dispatcher on the
|
|
3
|
+
* loupe's host when no camera already has one there, so the peek key and the
|
|
4
|
+
* wheel route the way every other weasel binding does.
|
|
5
5
|
*
|
|
6
6
|
* Three of the dispatcher's four element channels are off. Only `wheel` is
|
|
7
7
|
* wanted here; `contextMenu` suppresses the native menu unconditionally and
|
|
@@ -14,7 +14,8 @@ import {
|
|
|
14
14
|
useActionsRegistry,
|
|
15
15
|
useGestureDispatcher,
|
|
16
16
|
} from '@weasel-js/core';
|
|
17
|
-
import { type RefObject, useEffect, useMemo } from 'react';
|
|
17
|
+
import { type RefObject, useContext, useEffect, useMemo } from 'react';
|
|
18
|
+
import { CameraContext } from '../canvas/CameraInput';
|
|
18
19
|
import { createLoupeActions, type LoupeInputApi } from './loupeActions';
|
|
19
20
|
|
|
20
21
|
/** Props for `<LoupeGestures>`. */
|
|
@@ -35,7 +36,8 @@ function LoupeDispatch({
|
|
|
35
36
|
input,
|
|
36
37
|
peekKey,
|
|
37
38
|
registry,
|
|
38
|
-
|
|
39
|
+
ownDispatcher,
|
|
40
|
+
}: LoupeGesturesProps & { registry: ActionsRegistry; ownDispatcher: boolean }) {
|
|
39
41
|
const actions = useMemo(() => createLoupeActions(input, peekKey), [input, peekKey]);
|
|
40
42
|
|
|
41
43
|
useEffect(() => {
|
|
@@ -50,18 +52,21 @@ function LoupeDispatch({
|
|
|
50
52
|
actions: registry,
|
|
51
53
|
toolsById: NO_TOOLS,
|
|
52
54
|
channels: CHANNELS,
|
|
55
|
+
enabled: ownDispatcher,
|
|
53
56
|
});
|
|
54
57
|
|
|
55
58
|
return null;
|
|
56
59
|
}
|
|
57
60
|
|
|
58
61
|
/**
|
|
59
|
-
* The loupe's input, routed.
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
+
* The loupe's input, routed. Inside a camera (`<CanvasStack>`, `<Stage>`) its
|
|
63
|
+
* actions join the camera's dispatcher, which already listens on the host;
|
|
64
|
+
* elsewhere it mounts a dispatcher of its own. With no registry in scope it
|
|
65
|
+
* renders nothing rather than registering actions that could never fire.
|
|
62
66
|
*/
|
|
63
67
|
export function LoupeGestures(props: LoupeGesturesProps) {
|
|
64
68
|
const registry = useActionsRegistry();
|
|
69
|
+
const camera = useContext(CameraContext);
|
|
65
70
|
if (!registry) return null;
|
|
66
|
-
return <LoupeDispatch {...props} registry={registry} />;
|
|
71
|
+
return <LoupeDispatch {...props} registry={registry} ownDispatcher={camera === null} />;
|
|
67
72
|
}
|
package/src/loupe/TrialLoupe.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { WeaselProvider } from '@weasel-js/core';
|
|
2
1
|
import { type RefObject, useContext, useMemo } from 'react';
|
|
2
|
+
import { CameraScope } from '../canvas/CameraInput';
|
|
3
3
|
import { CanvasStackContext } from '../canvas/CanvasStackContext';
|
|
4
4
|
import type { WorldSpec } from '../canvas/worldSpec';
|
|
5
5
|
import type { ViewTransform } from '../instrument/types';
|
|
@@ -63,12 +63,12 @@ export function TrialLoupe({
|
|
|
63
63
|
|
|
64
64
|
// The gestures mount outside the visibility gate: hold-to-peek is what
|
|
65
65
|
// raises a lens that is down, so its binding has to be live while it is.
|
|
66
|
-
//
|
|
67
|
-
//
|
|
66
|
+
// In a trial they join the trial's scope, and with it the camera's
|
|
67
|
+
// dispatcher; outside one, `<CameraScope>` gives them an isolated scope.
|
|
68
68
|
const gestures = (
|
|
69
|
-
<
|
|
69
|
+
<CameraScope>
|
|
70
70
|
<LoupeGestures hostRef={host} input={loupe.input} peekKey={capability.peekKey ?? null} />
|
|
71
|
-
</
|
|
71
|
+
</CameraScope>
|
|
72
72
|
);
|
|
73
73
|
|
|
74
74
|
if (!loupe.visible) return gestures;
|
package/src/loupe/index.ts
CHANGED
|
@@ -21,8 +21,8 @@ export const WHEEL_RATE = 0.002;
|
|
|
21
21
|
* registration. */
|
|
22
22
|
export interface LoupeInputApi {
|
|
23
23
|
/** Whether the lens is up. The wheel belongs to the loupe only while it is;
|
|
24
|
-
* otherwise `loupe.magnify` declines and the dispatcher
|
|
25
|
-
*
|
|
24
|
+
* otherwise `loupe.magnify` declines and the dispatcher falls through to
|
|
25
|
+
* the trial camera's zoom. */
|
|
26
26
|
shown: () => boolean;
|
|
27
27
|
/** Raise or drop the hold-to-peek flag. */
|
|
28
28
|
setPeeking: (on: boolean) => void;
|
|
@@ -43,6 +43,10 @@ export function createLoupeActions(
|
|
|
43
43
|
id: LOUPE_MAGNIFY_ID,
|
|
44
44
|
label: 'Magnify (loupe)',
|
|
45
45
|
group: 'loupe',
|
|
46
|
+
// Hotkey scope, so while the lens is up it outranks the trial camera's
|
|
47
|
+
// wheel zoom on the same dispatcher; while it is down `enabled` declines
|
|
48
|
+
// and the zoom runs.
|
|
49
|
+
scope: 'hotkey',
|
|
46
50
|
// Every modifier optional: the lens claims the wheel whenever it is up,
|
|
47
51
|
// which is what the hand-rolled capture-phase listener did. A bare spec
|
|
48
52
|
// would forbid modifiers and hand Cmd+wheel back to the lab mid-peek.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Overview — Agent Guide
|
|
2
|
+
|
|
3
|
+
`src/overview/` is `@weasel-js/labkit/overview`: `<TrialOverview>`, a trial's
|
|
4
|
+
whole content in a floating panel. Its own bundle entry; nothing in the main
|
|
5
|
+
bundle imports it.
|
|
6
|
+
|
|
7
|
+
| File | Role |
|
|
8
|
+
|---|---|
|
|
9
|
+
| `TrialOverview.tsx` | The panel: content, chrome canvas, and its own dispatcher |
|
|
10
|
+
| `OverviewMarks.tsx` | Every annotation target's marks, read-only, as SVG |
|
|
11
|
+
|
|
12
|
+
## How it reads the trial
|
|
13
|
+
|
|
14
|
+
Only through `CameraContext`, which `<CanvasStack>` and `<Stage>` set: the
|
|
15
|
+
camera as a weasel `ViewApi` over frame-local coordinates, the frame, the
|
|
16
|
+
camera's element, a stage's content rect. A canvas instrument's layers come
|
|
17
|
+
from `CanvasStackContext`, marks from `AnnotationsContext`. So it works
|
|
18
|
+
anywhere inside a camera — `stage.overlay`, or a canvas instrument's `render`.
|
|
19
|
+
|
|
20
|
+
Input is core's `minimap.center` / `minimap.pan` on a dispatcher of its own,
|
|
21
|
+
with the camera as `rootView`; the pointer store is the trial's, shared.
|
|
22
|
+
|
|
23
|
+
## Traps
|
|
24
|
+
|
|
25
|
+
**It sits inside the stage's element.** Every event that bubbles out of the box
|
|
26
|
+
reaches the stage's dispatcher, the loupe's aim and the stage's pointer
|
|
27
|
+
publisher. The box stops them; drop that and a drag in the overview also pans
|
|
28
|
+
the stage, and the pointer flips between `'overview'` and `'stage'` each move.
|
|
29
|
+
|
|
30
|
+
**Marks are placed by measuring.** A target's rect on the overview is its
|
|
31
|
+
element's rect against the camera's element, through the camera — never the
|
|
32
|
+
target's content size, which is the mark's world and not where it is drawn.
|
|
33
|
+
jsdom measures zero; a test mocks `getBoundingClientRect`, and the picture is a
|
|
34
|
+
screenshot.
|
|
35
|
+
|
|
36
|
+
**Never render the instrument's own `render` in it.** Its effects — a pane
|
|
37
|
+
ref, an annotation target — would run twice. `render` is a separate, lighter
|
|
38
|
+
copy the instrument supplies.
|