@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
package/src/job/useJob.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { useLatest } from '@weasel-js/core';
|
|
1
2
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
3
|
import type { JobCapability, JobFailure, JobHandle, JobStatus } from './types';
|
|
3
4
|
|
|
5
|
+
/** Inputs to `useJob`. `setState` receives each item folded in by the
|
|
6
|
+
* capability's `onItem`. */
|
|
4
7
|
export interface UseJobOptions<TS, TC, TItem> {
|
|
5
8
|
capability: JobCapability<TS, TC, TItem>;
|
|
6
9
|
config: TC;
|
|
@@ -23,6 +26,12 @@ function sameKey(a: readonly unknown[] | null, b: readonly unknown[] | null): bo
|
|
|
23
26
|
return a.length === b.length && a.every((v, i) => Object.is(v, b[i]));
|
|
24
27
|
}
|
|
25
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Drive a `JobCapability` and report its progress. `start` cancels any run in
|
|
31
|
+
* flight and begins again; results from a superseded or canceled run are
|
|
32
|
+
* dropped. With `auto`, it starts on mount and again whenever `key` changes
|
|
33
|
+
* element-wise. Unmounting aborts the run.
|
|
34
|
+
*/
|
|
26
35
|
export function useJob<TS, TC, TItem>({
|
|
27
36
|
capability,
|
|
28
37
|
config,
|
|
@@ -38,14 +47,10 @@ export function useJob<TS, TC, TItem>({
|
|
|
38
47
|
|
|
39
48
|
// Read through refs: `run` is called once per run and must see the values as of
|
|
40
49
|
// that moment rather than re-subscribing on every render.
|
|
41
|
-
const capRef =
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
const stateRef = useRef(state);
|
|
46
|
-
stateRef.current = state;
|
|
47
|
-
const setStateRef = useRef(setState);
|
|
48
|
-
setStateRef.current = setState;
|
|
50
|
+
const capRef = useLatest(capability);
|
|
51
|
+
const configRef = useLatest(config);
|
|
52
|
+
const stateRef = useLatest(state);
|
|
53
|
+
const setStateRef = useLatest(setState);
|
|
49
54
|
|
|
50
55
|
const cancel = useCallback(() => {
|
|
51
56
|
token.current += 1;
|
|
@@ -96,7 +101,7 @@ export function useJob<TS, TC, TItem>({
|
|
|
96
101
|
}));
|
|
97
102
|
}
|
|
98
103
|
})();
|
|
99
|
-
}, []);
|
|
104
|
+
}, [capRef, configRef, setStateRef, stateRef]);
|
|
100
105
|
|
|
101
106
|
// Re-run when the declared key changes. `auto` covers the first mount too.
|
|
102
107
|
const lastKey = useRef<readonly unknown[] | null>(null);
|
package/src/lab/Lab.tsx
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
type ReactNode,
|
|
6
6
|
useCallback,
|
|
7
7
|
useEffect,
|
|
8
|
+
useInsertionEffect,
|
|
8
9
|
useLayoutEffect,
|
|
9
10
|
useMemo,
|
|
10
11
|
useRef,
|
|
@@ -418,7 +419,10 @@ function LabRuntime({
|
|
|
418
419
|
}, []);
|
|
419
420
|
|
|
420
421
|
const ownSurface = useTiledSurface({ onFrame });
|
|
421
|
-
|
|
422
|
+
// Not `useLatest`: `onFrame` reads it, and is needed before the surface exists.
|
|
423
|
+
useInsertionEffect(() => {
|
|
424
|
+
surfaceRef.current = ownSurface;
|
|
425
|
+
}, [ownSurface]);
|
|
422
426
|
const [labBody, setLabBody] = useState<HTMLDivElement | null>(null);
|
|
423
427
|
const attachOwnSurface = ownSurface.containerRef;
|
|
424
428
|
const labBodyRef = useCallback(
|
package/src/lab/LabShell.less
CHANGED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import '@weasel-js/theme/tokens.css';
|
|
2
|
+
import '../styles.less';
|
|
3
|
+
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
|
|
4
|
+
import { afterEach, expect, test } from 'vitest';
|
|
5
|
+
import { LabShell } from './LabShell';
|
|
6
|
+
|
|
7
|
+
// The menu sits under the shell's title, so what it inherits from the title is
|
|
8
|
+
// cascade, which only a real browser computes.
|
|
9
|
+
|
|
10
|
+
afterEach(cleanup);
|
|
11
|
+
|
|
12
|
+
const PAGES = [
|
|
13
|
+
{ href: '/corpus', label: 'Wall' },
|
|
14
|
+
{ href: '/stats', label: 'Dashboard' },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
test('only the open page is bold in the shell title menu', () => {
|
|
18
|
+
render(
|
|
19
|
+
<LabShell title="corpus" pages={PAGES} path="/corpus">
|
|
20
|
+
<p>body</p>
|
|
21
|
+
</LabShell>,
|
|
22
|
+
);
|
|
23
|
+
fireEvent.click(screen.getByRole('button', { name: /corpus/ }));
|
|
24
|
+
const weight = (name: string) =>
|
|
25
|
+
Number(getComputedStyle(screen.getByRole('menuitem', { name })).fontWeight);
|
|
26
|
+
expect(weight('Wall')).toBeGreaterThan(weight('Dashboard'));
|
|
27
|
+
});
|
package/src/lab/LabSwitcher.less
CHANGED
|
@@ -59,6 +59,8 @@
|
|
|
59
59
|
display: flex;
|
|
60
60
|
flex-direction: column;
|
|
61
61
|
padding: var(--wzl-space-xs);
|
|
62
|
+
// A host's title class lands on the wrapper, so the menu would inherit its weight.
|
|
63
|
+
font-weight: var(--wzl-font-weight-normal);
|
|
62
64
|
// `--wzl-surface-raised` is translucent by design -- it is for panels that
|
|
63
65
|
// blur what sits behind them. A menu has to be read, so it takes the opaque
|
|
64
66
|
// surface, the way `FloatingPanel` does.
|
package/src/lab/LabZoom.tsx
CHANGED
|
@@ -10,10 +10,11 @@ import {
|
|
|
10
10
|
useActionsRegistry,
|
|
11
11
|
useDepSource,
|
|
12
12
|
useGestureDispatcher,
|
|
13
|
+
useLatest,
|
|
13
14
|
type ViewApi,
|
|
14
15
|
} from '@weasel-js/core';
|
|
15
16
|
import { ZoomInIcon, ZoomOutIcon } from '@weasel-js/ui';
|
|
16
|
-
import { useContext, useEffect, useMemo,
|
|
17
|
+
import { useContext, useEffect, useMemo, useSyncExternalStore } from 'react';
|
|
17
18
|
import { useStore } from 'zustand/react';
|
|
18
19
|
import type { CameraView } from '../canvas/CameraInput';
|
|
19
20
|
import { CameraRegistryContext } from '../canvas/cameraRegistry';
|
|
@@ -96,8 +97,7 @@ function LabZoomControls() {
|
|
|
96
97
|
|
|
97
98
|
const live = zoom !== null && camera !== null;
|
|
98
99
|
const view = useMemo(() => (camera ? forZoom(camera) : null), [camera]);
|
|
99
|
-
const viewRef =
|
|
100
|
-
viewRef.current = live ? view : null;
|
|
100
|
+
const viewRef = useLatest(live ? view : null);
|
|
101
101
|
|
|
102
102
|
useDepSource('view', () => viewRef.current as ViewApi);
|
|
103
103
|
|
|
@@ -108,7 +108,7 @@ function LabZoomControls() {
|
|
|
108
108
|
enabled: () => (viewRef.current ? true : ActionDisabledReason.NotApplicable),
|
|
109
109
|
};
|
|
110
110
|
return registry.register(action);
|
|
111
|
-
}, [registry]);
|
|
111
|
+
}, [registry, viewRef]);
|
|
112
112
|
|
|
113
113
|
// First, so a story's own zoom keys do not also fire over a trial with a
|
|
114
114
|
// camera; over one without, the action is disabled and the keys pass on.
|
package/src/lab/Workspace.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useLatest } from '@weasel-js/core';
|
|
1
2
|
import {
|
|
2
3
|
Children,
|
|
3
4
|
type ReactNode,
|
|
@@ -33,6 +34,7 @@ export type TrialLayout = Record<
|
|
|
33
34
|
{ size?: { w?: number; h?: number }; span?: { cols?: number; rows?: number } }
|
|
34
35
|
>;
|
|
35
36
|
|
|
37
|
+
/** Props for `Workspace`. */
|
|
36
38
|
export interface WorkspaceProps {
|
|
37
39
|
children: ReactNode;
|
|
38
40
|
/**
|
|
@@ -61,7 +63,7 @@ export interface WorkspaceProps {
|
|
|
61
63
|
padding?: number;
|
|
62
64
|
/** Undocked sidebar panels to render alongside the trials. A `'tile'` panel
|
|
63
65
|
* joins the grid as a peer of the trials; a `'floating'` one goes into the
|
|
64
|
-
* floating zone above it. The body is
|
|
66
|
+
* floating zone above it. The body is portaled in by the trial that owns
|
|
65
67
|
* it, so all this renders is the frame and the host. */
|
|
66
68
|
panels?: readonly PanelDescriptor[];
|
|
67
69
|
/**
|
|
@@ -72,6 +74,26 @@ export interface WorkspaceProps {
|
|
|
72
74
|
viewport?: { w: number; h: number };
|
|
73
75
|
}
|
|
74
76
|
|
|
77
|
+
/** The layout store a workspace keeps for its lifetime: the tile grid, and the zone floating panels live in. */
|
|
78
|
+
function createWorkspaceStore(grid: { resizable: boolean; gap: number; padding: number }): Store {
|
|
79
|
+
const store = new Store();
|
|
80
|
+
store.registerNode(
|
|
81
|
+
createNode({
|
|
82
|
+
kind: 'zone',
|
|
83
|
+
id: ZONE_ID,
|
|
84
|
+
container: { strategyId: 'grid', config: grid },
|
|
85
|
+
}),
|
|
86
|
+
);
|
|
87
|
+
store.registerNode(
|
|
88
|
+
createNode({
|
|
89
|
+
kind: 'zone',
|
|
90
|
+
id: FLOAT_ZONE_ID,
|
|
91
|
+
container: { strategyId: 'floating', config: {} },
|
|
92
|
+
}),
|
|
93
|
+
);
|
|
94
|
+
return store;
|
|
95
|
+
}
|
|
96
|
+
|
|
75
97
|
function extentOf(store: Store, id: NodeId): TrialLayout[string] | null {
|
|
76
98
|
const p = store.getNode(id)?.membership?.placement as TrialLayout[string] | undefined;
|
|
77
99
|
if (!p) return null;
|
|
@@ -81,21 +103,6 @@ function extentOf(store: Store, id: NodeId): TrialLayout[string] | null {
|
|
|
81
103
|
return Object.keys(out).length > 0 ? out : null;
|
|
82
104
|
}
|
|
83
105
|
|
|
84
|
-
/**
|
|
85
|
-
* Auto-balanced tiling of trials, `ceil(sqrt(n))` columns wide.
|
|
86
|
-
*
|
|
87
|
-
* Tiles are absolutely positioned at the rects `gridStrategy` computes, not
|
|
88
|
-
* laid out by CSS — `windease/styles.css` (folded into
|
|
89
|
-
* `@weasel-js/labkit/styles.css`) carries the rules that positioning depends on.
|
|
90
|
-
*
|
|
91
|
-
* `panels` are undocked sidebar sections. A `'tile'` panel is registered as a
|
|
92
|
-
* peer of the trials under `PANEL_KIND`, so the grid places and resizes it like
|
|
93
|
-
* one; a `'floating'` panel goes in the overlay above. Either way this renders
|
|
94
|
-
* only the frame and an empty host: the section's content is portalled in by
|
|
95
|
-
* the trial that owns it, which is what keeps a torn-out section inside its
|
|
96
|
-
* trial's React tree instead of rebuilding it as a sibling.
|
|
97
|
-
*/
|
|
98
|
-
|
|
99
106
|
/** The frame an undocked panel gets: a box, and the host element its trial
|
|
100
107
|
* portals into. The title and the dock control come through the portal with
|
|
101
108
|
* the body — the workspace knows a panel's key, not what is in it. */
|
|
@@ -116,6 +123,20 @@ export interface PanelDescriptor {
|
|
|
116
123
|
as: 'tile' | 'floating';
|
|
117
124
|
}
|
|
118
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Auto-balanced tiling of trials, `ceil(sqrt(n))` columns wide.
|
|
128
|
+
*
|
|
129
|
+
* Tiles are absolutely positioned at the rects `gridStrategy` computes, not
|
|
130
|
+
* laid out by CSS — `windease/styles.css` (folded into
|
|
131
|
+
* `@weasel-js/labkit/styles.css`) carries the rules that positioning depends on.
|
|
132
|
+
*
|
|
133
|
+
* `panels` are undocked sidebar sections. A `'tile'` panel is registered as a
|
|
134
|
+
* peer of the trials under `PANEL_KIND`, so the grid places and resizes it like
|
|
135
|
+
* one; a `'floating'` panel goes in the overlay above. Either way this renders
|
|
136
|
+
* only the frame and an empty host: the section's content is portaled in by
|
|
137
|
+
* the trial that owns it, which is what keeps a torn-out section inside its
|
|
138
|
+
* trial's React tree instead of rebuilding it as a sibling.
|
|
139
|
+
*/
|
|
119
140
|
export function Workspace({
|
|
120
141
|
children,
|
|
121
142
|
ids,
|
|
@@ -134,50 +155,25 @@ export function Workspace({
|
|
|
134
155
|
const floatPanels = useMemo(() => (panels ?? []).filter((p) => p.as === 'floating'), [panels]);
|
|
135
156
|
// Identity has to be stable while the id *contents* are unchanged: the sync
|
|
136
157
|
// effect below keys off it, and a fresh array every render would re-register
|
|
137
|
-
// every tile forever.
|
|
138
|
-
//
|
|
139
|
-
const
|
|
158
|
+
// every tile forever. So the ids are memoized on a serialized key, and read
|
|
159
|
+
// back from it.
|
|
160
|
+
const idKey = JSON.stringify([
|
|
140
161
|
...items.map((_, i) => ids?.[i] ?? `lk-ws-${i}`),
|
|
141
162
|
...tilePanels.map((p) => `lk-panel-${p.key}`),
|
|
142
|
-
];
|
|
143
|
-
const
|
|
144
|
-
const idKeyRef = useRef<string | null>(null);
|
|
145
|
-
const nodeIdsRef = useRef<NodeId[]>([]);
|
|
146
|
-
if (idKeyRef.current !== idKey) {
|
|
147
|
-
idKeyRef.current = idKey;
|
|
148
|
-
nodeIdsRef.current = wantedIds.map(asNodeId);
|
|
149
|
-
}
|
|
150
|
-
const nodeIds = nodeIdsRef.current;
|
|
163
|
+
]);
|
|
164
|
+
const nodeIds = useMemo(() => (JSON.parse(idKey) as string[]).map(asNodeId), [idKey]);
|
|
151
165
|
|
|
152
166
|
// Held in refs rather than depended on: a fresh object each render would
|
|
153
167
|
// re-run the sync effect, and only a newly registered tile reads `layout`.
|
|
154
|
-
const layoutRef =
|
|
155
|
-
|
|
156
|
-
const onLayoutChangeRef = useRef(onLayoutChange);
|
|
157
|
-
onLayoutChangeRef.current = onLayoutChange;
|
|
168
|
+
const layoutRef = useLatest(layout);
|
|
169
|
+
const onLayoutChangeRef = useLatest(onLayoutChange);
|
|
158
170
|
|
|
159
171
|
// One store for the component's lifetime: a tile's dragged extent lives in
|
|
160
172
|
// its node, so rebuilding the store on every add or close would silently
|
|
161
173
|
// reset every pane.
|
|
162
174
|
const storeRef = useRef<Store | null>(null);
|
|
163
|
-
if (storeRef.current === null)
|
|
164
|
-
|
|
165
|
-
store.registerNode(
|
|
166
|
-
createNode({
|
|
167
|
-
kind: 'zone',
|
|
168
|
-
id: ZONE_ID,
|
|
169
|
-
container: { strategyId: 'grid', config: { resizable, gap, padding } },
|
|
170
|
-
}),
|
|
171
|
-
);
|
|
172
|
-
store.registerNode(
|
|
173
|
-
createNode({
|
|
174
|
-
kind: 'zone',
|
|
175
|
-
id: FLOAT_ZONE_ID,
|
|
176
|
-
container: { strategyId: 'floating', config: {} },
|
|
177
|
-
}),
|
|
178
|
-
);
|
|
179
|
-
storeRef.current = store;
|
|
180
|
-
}
|
|
175
|
+
if (storeRef.current === null)
|
|
176
|
+
storeRef.current = createWorkspaceStore({ resizable, gap, padding });
|
|
181
177
|
const store = storeRef.current;
|
|
182
178
|
|
|
183
179
|
// A tile that only moves reports nothing to a ResizeObserver, and only this
|
|
@@ -203,7 +199,7 @@ export function Workspace({
|
|
|
203
199
|
if (saved) store.patchPlacement(id, saved);
|
|
204
200
|
}
|
|
205
201
|
store.setChildOrder(ZONE_ID, [...nodeIds]);
|
|
206
|
-
}, [store, nodeIds]);
|
|
202
|
+
}, [store, nodeIds, layoutRef]);
|
|
207
203
|
|
|
208
204
|
// biome-ignore lint/correctness/useExhaustiveDependencies: nodeIds is the signal that the tile set changed — a re-tile moves tiles without resizing any — not a value this reads
|
|
209
205
|
useEffect(() => {
|
|
@@ -222,7 +218,7 @@ export function Workspace({
|
|
|
222
218
|
}
|
|
223
219
|
onLayoutChangeRef.current?.(next);
|
|
224
220
|
});
|
|
225
|
-
}, [store, onLayoutChange]);
|
|
221
|
+
}, [store, onLayoutChange, onLayoutChangeRef]);
|
|
226
222
|
|
|
227
223
|
const commitOrder = useCallback(
|
|
228
224
|
(nextIds: NodeId[]) => onReorder?.(nextIds.map(String)),
|
package/src/loupe/AGENTS.md
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# Loupe — Agent Guide
|
|
2
2
|
|
|
3
|
-
`src/loupe/` is
|
|
4
|
-
|
|
3
|
+
`src/loupe/` is `@weasel-js/labkit/loupe`: `<TrialLoupe>`, a magnifier the
|
|
4
|
+
instrument mounts in its own content, painted by whichever painter suits it.
|
|
5
|
+
Its own bundle entry; nothing in the main bundle imports it, so a lab that
|
|
6
|
+
mounts no lens never loads it.
|
|
5
7
|
|
|
6
8
|
The magnifier itself is not here. `@weasel-js/loupe` holds the model — aim,
|
|
7
9
|
factor, mode, color, picking — over a `LoupeSurface` it asks five questions.
|
|
@@ -11,29 +13,40 @@ This directory binds that model to a labkit trial and draws it.
|
|
|
11
13
|
|
|
12
14
|
| File | Role |
|
|
13
15
|
|---|---|
|
|
14
|
-
| `types.ts` | `
|
|
16
|
+
| `types.ts` | `LoupeOptions`, and `resolveLoupe` filling in every default |
|
|
15
17
|
| `useLoupe.ts` | The model over a host element, and pointer aiming |
|
|
16
18
|
| `loupeActions.ts` | `loupe.peek` and `loupe.magnify`, as `Action` descriptors |
|
|
17
19
|
| `LoupeGestures.tsx` | Registers those, and mounts a dispatcher on the host when no camera has one there |
|
|
18
|
-
| `TrialLoupe.tsx` |
|
|
20
|
+
| `TrialLoupe.tsx` | Finds its host and the trial's switch, picks the painter, mounts the lens |
|
|
19
21
|
| `LoupeBubble.tsx` | The circular clip, positioned on the aim |
|
|
20
22
|
| `CanvasLoupe.tsx` | Painter for a `<CanvasStack>` |
|
|
21
23
|
| `canvasLens.ts` | That painter's geometry and drawing, with no React in it |
|
|
22
24
|
| `DomLoupe.tsx` | Painter for DOM content |
|
|
23
25
|
| `useHostSize.ts` | The host's measured box, for the DOM stage |
|
|
24
26
|
|
|
25
|
-
##
|
|
27
|
+
## How it reads the trial
|
|
28
|
+
|
|
29
|
+
Only through context, the way `<TrialOverview>` does. `CanvasStackContext`'s
|
|
30
|
+
`surface` gives the canvas painter the stack's layers, pixels and `worldSpec`;
|
|
31
|
+
`CameraContext` gives a lens in a `<Stage>` its element and camera. Outside
|
|
32
|
+
both, it tracks `hostRef`, or failing that wraps its `children` in a
|
|
33
|
+
`.lk-loupe-host` box of its own.
|
|
26
34
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
`
|
|
35
|
+
The toolbar toggle is `LoupeSwitchContext` (`src/trial/loupeSwitch.ts`, main
|
|
36
|
+
bundle). A lens with no `enabled` prop calls `mount` and follows `on`; the
|
|
37
|
+
trial offers its Loupe toggle while any lens is mounted. `enabled` opts a lens
|
|
38
|
+
out of the switch entirely. Because the lens mounts after the trial's first
|
|
39
|
+
render, `loupe` is in `TRANSIENT_BUILTINS`, so suppressing it before then is
|
|
40
|
+
not a typo.
|
|
41
|
+
|
|
42
|
+
## Which painter
|
|
32
43
|
|
|
33
|
-
|
|
34
|
-
`
|
|
35
|
-
|
|
36
|
-
|
|
44
|
+
`render` decides. Absent, the lens re-runs the stack's canvas layers through
|
|
45
|
+
`lensCamera` — sharp at any factor, and `mode: 'pixel'` enlarges the presented
|
|
46
|
+
pixels with smoothing off instead. Present, it is handed a camera and draws the
|
|
47
|
+
content again; a DOM loupe is always `vector`, since DOM has no framebuffer to
|
|
48
|
+
enlarge. The canvas painter needs the stack's own pixels, which is why a
|
|
49
|
+
drawing instrument mounts the lens in its `render`, inside `<CanvasStack>`.
|
|
37
50
|
|
|
38
51
|
## Traps
|
|
39
52
|
|
|
@@ -51,7 +64,7 @@ In a trial the loupe's actions join the camera's dispatcher (`CameraInput`).
|
|
|
51
64
|
`loupe.magnify` sits in the hotkey tier, so while the lens is up it outranks
|
|
52
65
|
the camera's wheel zoom; while it is down its `enabled` returns a disabled
|
|
53
66
|
reason and the dispatcher falls through to the zoom. Outside a camera — the DOM
|
|
54
|
-
`.lk-
|
|
67
|
+
`.lk-loupe-host` — `<LoupeGestures>` mounts a dispatcher of its own.
|
|
55
68
|
|
|
56
69
|
**Aiming is a plain listener because a hover is not a gesture.**
|
|
57
70
|
`GESTURE_DESCRIPTORS` names no continuous-motion gesture, so `pointermove` /
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useVisibleRaf } from '@weasel-js/core';
|
|
1
|
+
import { useLatest, useVisibleRaf } from '@weasel-js/core';
|
|
2
2
|
import type { LoupeMode, LoupePoint } from '@weasel-js/loupe';
|
|
3
3
|
import { useEffect, useRef } from 'react';
|
|
4
4
|
import type { CanvasStackSurface } from '../canvas/CanvasStackContext';
|
|
@@ -40,8 +40,7 @@ export function CanvasLoupe({
|
|
|
40
40
|
// Whatever the stack redraws every frame, the lens has to redraw too — an
|
|
41
41
|
// instrument that animates from its own loop never re-renders this component.
|
|
42
42
|
const args = { aim, factor, mode, diameter, dpr, view, frame, worldSpec, surface };
|
|
43
|
-
const argsRef =
|
|
44
|
-
argsRef.current = args;
|
|
43
|
+
const argsRef = useLatest(args);
|
|
45
44
|
|
|
46
45
|
const loop = useVisibleRaf(
|
|
47
46
|
() => {
|
package/src/loupe/DomLoupe.tsx
CHANGED
|
@@ -16,13 +16,11 @@ export interface DomLoupeProps {
|
|
|
16
16
|
/** The trial's own camera. */
|
|
17
17
|
view: ViewTransform;
|
|
18
18
|
frame?: WorldFrame;
|
|
19
|
-
state: unknown;
|
|
20
|
-
config: unknown;
|
|
21
19
|
render: (args: LoupeRenderArgs) => React.ReactNode;
|
|
22
20
|
}
|
|
23
21
|
|
|
24
22
|
/**
|
|
25
|
-
* Paints a lens over DOM content by asking
|
|
23
|
+
* Paints a lens over DOM content by asking its `render` to draw it again
|
|
26
24
|
* at a magnified camera.
|
|
27
25
|
*
|
|
28
26
|
* The stage is a full copy of the host, so the instrument's own layout still
|
|
@@ -38,8 +36,6 @@ export function DomLoupe({
|
|
|
38
36
|
size,
|
|
39
37
|
view,
|
|
40
38
|
frame,
|
|
41
|
-
state,
|
|
42
|
-
config,
|
|
43
39
|
render,
|
|
44
40
|
}: DomLoupeProps) {
|
|
45
41
|
const style: CSSProperties = {
|
|
@@ -50,8 +46,6 @@ export function DomLoupe({
|
|
|
50
46
|
return (
|
|
51
47
|
<div className="lk-loupe__stage" style={style}>
|
|
52
48
|
{render({
|
|
53
|
-
state,
|
|
54
|
-
config,
|
|
55
49
|
view: zoomAt(view, factor, aim, { frame }),
|
|
56
50
|
factor,
|
|
57
51
|
mode,
|
package/src/loupe/Loupe.less
CHANGED
|
@@ -36,3 +36,19 @@
|
|
|
36
36
|
left: 0;
|
|
37
37
|
transform-origin: 0 0;
|
|
38
38
|
}
|
|
39
|
+
|
|
40
|
+
// The box a lens wraps around DOM content when nothing around it has one to
|
|
41
|
+
// track. Reproduces the content well's column so the content still stretches
|
|
42
|
+
// to it rather than collapsing to its intrinsic height.
|
|
43
|
+
.lk-loupe-host {
|
|
44
|
+
position: relative;
|
|
45
|
+
display: flex;
|
|
46
|
+
flex-direction: column;
|
|
47
|
+
min-height: 0;
|
|
48
|
+
height: 100%;
|
|
49
|
+
|
|
50
|
+
> * {
|
|
51
|
+
flex: 1 1 auto;
|
|
52
|
+
min-height: 0;
|
|
53
|
+
}
|
|
54
|
+
}
|