@weasel-js/labkit 1.3.0-pre.0 → 1.4.0-pre.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_dts/CanvasStackContext-DtAn_3O-.d.ts +42 -0
- package/dist/_dts/DrawCommand-B3bskUsC.d.ts +564 -0
- package/dist/_dts/{PrefsForm-BYa6cWnO.d.ts → PrefsForm-BkUJZx0A.d.ts} +4 -1
- package/dist/_dts/fitViewToBounds-dZ2UDB6e.d.ts +21 -0
- package/dist/_dts/frac-Cp3NivlC.d.ts +728 -0
- package/dist/_dts/index-BDVzvRzQ.d.ts +236 -0
- package/dist/_dts/shapeKinds-Cx_rxwsa.d.ts +87 -0
- package/dist/_dts/{DrawCommand-DBB45NfN.d.ts → types-C-gh9Ap-.d.ts} +1 -582
- package/dist/_dts/{types-AHlQxBNN.d.ts → types-D6s4b7if.d.ts} +2 -2
- package/dist/_dts/{types-1Sdxy_Pv.d.ts → types-DYMaEvM5.d.ts} +26 -1
- package/dist/_dts/{useTrialState-CsGMjhu9.d.ts → useTrialState-gmMvZqPc.d.ts} +7 -2
- package/dist/canvas/index.d.ts +43 -25
- package/dist/canvas/index.js +5 -3
- package/dist/chrome/index.d.ts +9 -6
- package/dist/chrome/index.js +7 -6
- package/dist/chunk-2AYGEN57.js +32 -0
- package/dist/chunk-2AYGEN57.js.map +1 -0
- package/dist/{chunk-RVNN4CHQ.js → chunk-3P5TTTA7.js} +16 -8
- package/dist/chunk-3P5TTTA7.js.map +1 -0
- package/dist/{chunk-NS54R52R.js → chunk-3ZELRRBV.js} +45 -40
- package/dist/chunk-3ZELRRBV.js.map +1 -0
- package/dist/{chunk-HFDVGF4X.js → chunk-4EY67BKJ.js} +32 -7
- package/dist/chunk-4EY67BKJ.js.map +1 -0
- package/dist/chunk-5TCEVMPI.js +140 -0
- package/dist/chunk-5TCEVMPI.js.map +1 -0
- package/dist/{chunk-O7NVSCLN.js → chunk-6JMU56CU.js} +133 -134
- package/dist/chunk-6JMU56CU.js.map +1 -0
- package/dist/{chunk-CPUJ3QXL.js → chunk-CQLQPQ4P.js} +2 -2
- package/dist/chunk-CQLQPQ4P.js.map +1 -0
- package/dist/chunk-CTRKTLYZ.js +33 -0
- package/dist/chunk-CTRKTLYZ.js.map +1 -0
- package/dist/chunk-EQIB2PGC.js +458 -0
- package/dist/chunk-EQIB2PGC.js.map +1 -0
- package/dist/chunk-HEORBLVT.js +7128 -0
- package/dist/chunk-HEORBLVT.js.map +1 -0
- package/dist/{chunk-V7PRSIRQ.js → chunk-JDDIZSLL.js} +4 -4
- package/dist/chunk-JDDIZSLL.js.map +1 -0
- package/dist/chunk-LITE5YFB.js +669 -0
- package/dist/chunk-LITE5YFB.js.map +1 -0
- package/dist/{chunk-Z6HJ5FEM.js → chunk-ROQDRXKW.js} +732 -315
- package/dist/chunk-ROQDRXKW.js.map +1 -0
- package/dist/chunk-SBXNQW4G.js +3 -0
- package/dist/chunk-SBXNQW4G.js.map +1 -0
- package/dist/{chunk-4TMMVIDM.js → chunk-XJ6N32QP.js} +50 -6
- package/dist/chunk-XJ6N32QP.js.map +1 -0
- package/dist/chunk-YQJNT4XQ.js +293 -0
- package/dist/chunk-YQJNT4XQ.js.map +1 -0
- package/dist/controls/index.d.ts +4 -28
- package/dist/controls/index.js +3 -3
- package/dist/dragdrop/index.d.ts +50 -5
- package/dist/dragdrop/index.js +3 -1
- package/dist/index.d.ts +660 -302
- package/dist/index.js +1793 -587
- package/dist/index.js.map +1 -1
- package/dist/layers/index.d.ts +6 -5
- package/dist/layers/index.js +4 -2
- package/dist/loupe/index.d.ts +187 -0
- package/dist/loupe/index.js +7 -0
- package/dist/loupe/index.js.map +1 -0
- package/dist/passthrough/weasel-canvas.d.ts +3 -1
- package/dist/passthrough/weasel-canvas.js +1 -1
- package/dist/passthrough/weasel-ui.d.ts +151 -91
- package/dist/passthrough/weasel-ui.js +3 -2
- package/dist/primitives/index.d.ts +3 -1
- package/dist/primitives/index.js +6 -5
- package/dist/state/index.d.ts +3 -3
- package/dist/state/index.js +2 -2
- package/dist/styles.css +229 -703
- package/dist/surface/index.d.ts +18 -2
- package/dist/surface/index.js +2 -2
- package/dist/ui/layers/index.d.ts +8 -2
- package/dist/ui/layers/index.js +2 -4
- package/dist/undo/index.d.ts +5 -4
- package/package.json +14 -7
- package/src/annotations/AnnotationOverlay.tsx +205 -0
- package/src/annotations/AnnotationTargets.tsx +37 -0
- package/src/annotations/Annotations.less +129 -0
- package/src/annotations/Annotations.meaning.test.tsx +111 -0
- package/src/annotations/Annotations.overlay.test.tsx +142 -0
- package/src/annotations/AnnotationsContext.ts +49 -0
- package/src/annotations/ExportMenu.test.tsx +86 -0
- package/src/annotations/ExportMenu.tsx +182 -0
- package/src/annotations/MarkList.tsx +75 -0
- package/src/annotations/capture.test.ts +140 -0
- package/src/annotations/capture.ts +237 -0
- package/src/annotations/drawOne.test.ts +56 -0
- package/src/annotations/drawOne.ts +47 -0
- package/src/annotations/frac.test.ts +59 -0
- package/src/annotations/frac.ts +66 -0
- package/src/annotations/history.test.ts +82 -0
- package/src/annotations/history.ts +118 -0
- package/src/annotations/index.ts +37 -0
- package/src/annotations/paint.test.ts +125 -0
- package/src/annotations/paint.ts +130 -0
- package/src/annotations/staleness.test.ts +56 -0
- package/src/annotations/staleness.ts +41 -0
- package/src/annotations/store.test.ts +175 -0
- package/src/annotations/store.ts +317 -0
- package/src/annotations/svgNodes.test.ts +77 -0
- package/src/annotations/svgNodes.ts +74 -0
- package/src/annotations/toolMap.test.ts +36 -0
- package/src/annotations/toolMap.ts +54 -0
- package/src/annotations/types.ts +222 -0
- package/src/annotations/view.test.ts +37 -0
- package/src/annotations/view.ts +40 -0
- package/src/canvas/AGENTS.md +55 -5
- package/src/canvas/CanvasStack.test.tsx +27 -1
- package/src/canvas/CanvasStack.tsx +29 -4
- package/src/canvas/CanvasStackContext.ts +23 -4
- package/src/canvas/camera.test.ts +78 -0
- package/src/canvas/camera.ts +62 -0
- package/src/canvas/canvasCoords.test.ts +26 -0
- package/src/canvas/canvasCoords.ts +20 -7
- package/src/canvas/index.ts +5 -1
- package/src/canvas/useLayerScheduler.ts +7 -3
- package/src/canvas/usePanZoom.test.ts +36 -5
- package/src/canvas/usePanZoom.ts +20 -15
- package/src/canvas/worldSpec.test.ts +63 -0
- package/src/canvas/worldSpec.ts +48 -0
- package/src/chrome/builtins.test.ts +31 -1
- package/src/chrome/builtins.tsx +63 -19
- package/src/chrome/regions/PaletteRegion.test.tsx +22 -1
- package/src/chrome/regions/PaletteRegion.tsx +4 -0
- package/src/chrome/regions/SidebarRegion.tsx +37 -13
- package/src/chrome/regions/ToolbarRegion.tsx +2 -1
- package/src/chrome/regions/ViewportRegion.tsx +9 -1
- package/src/chrome/regions/regions.test.tsx +6 -1
- package/src/chrome/types.ts +20 -0
- package/src/config/builder.test.ts +7 -1
- package/src/config/builder.ts +1 -5
- package/src/config/resolve.test.ts +1 -2
- package/src/controls/ControlPanel.stories.tsx +41 -2
- package/src/controls/ControlPanel.test.tsx +69 -9
- package/src/controls/ControlPanel.tsx +106 -25
- package/src/dragdrop/DragDropRuntime.tsx +8 -2
- package/src/dragdrop/dragDrop.test.tsx +29 -2
- package/src/dragdrop/index.ts +11 -0
- package/src/index.test.ts +22 -0
- package/src/index.ts +136 -4
- package/src/instrument/SineWave.smoke.test.tsx +1 -0
- package/src/instrument/types.ts +24 -1
- package/src/lab/Lab.surface.test.tsx +162 -0
- package/src/lab/Lab.tsx +102 -17
- package/src/lab/Lab.undock.test.tsx +85 -0
- package/src/lab/LabFullChrome.stories.tsx +20 -0
- package/src/lab/LabShell.less +11 -0
- package/src/lab/Workspace.less +73 -0
- package/src/lab/Workspace.surface.test.tsx +2 -0
- package/src/lab/Workspace.tsx +97 -8
- package/src/lab/index.ts +1 -1
- package/src/lab/panelHost.ts +37 -0
- package/src/layers/AGENTS.md +1 -1
- package/src/layers/LayerList.tsx +1 -1
- package/src/loupe/AGENTS.md +49 -0
- package/src/loupe/CanvasLoupe.tsx +83 -0
- package/src/loupe/DomLoupe.tsx +62 -0
- package/src/loupe/Loupe.less +38 -0
- package/src/loupe/LoupeBubble.tsx +32 -0
- package/src/loupe/TrialLoupe.tsx +93 -0
- package/src/loupe/canvasLens.test.ts +205 -0
- package/src/loupe/canvasLens.ts +141 -0
- package/src/loupe/index.ts +19 -0
- package/src/loupe/types.test.ts +29 -0
- package/src/loupe/types.ts +90 -0
- package/src/loupe/useHostSize.ts +30 -0
- package/src/loupe/useLoupe.test.tsx +197 -0
- package/src/loupe/useLoupe.ts +189 -0
- package/src/passthrough/weasel-ui.ts +25 -17
- package/src/primitives/FloatingPanel.test.tsx +76 -1
- package/src/primitives/FloatingPanel.tsx +19 -3
- package/src/primitives/ScaleIndicator.test.tsx +7 -2
- package/src/primitives/Sidebar.less +18 -0
- package/src/primitives/Toolbar.less +8 -0
- package/src/primitives/Toolbar.tsx +5 -0
- package/src/primitives/useRovingTabIndex.test.ts +21 -0
- package/src/primitives/useRovingTabIndex.ts +26 -17
- package/src/state/document.test.ts +23 -0
- package/src/state/document.ts +14 -2
- package/src/state/index.ts +2 -0
- package/src/state/store.test.ts +24 -0
- package/src/state/store.ts +35 -1
- package/src/state/types.ts +11 -0
- package/src/state/undock.test.ts +40 -0
- package/src/state/undock.ts +39 -0
- package/src/styles.less +2 -4
- package/src/surface/SurfaceContext.ts +4 -0
- package/src/surface/index.ts +8 -3
- package/src/surface/useSurfaceTile.test.tsx +2 -0
- package/src/surface/useSurfaceTile.ts +7 -1
- package/src/surface/useTiledSurface.test.tsx +58 -0
- package/src/surface/useTiledSurface.ts +38 -3
- package/src/theme/Interstellar.stories.tsx +1 -1
- package/src/trial/Trial.annotations.persist.test.tsx +166 -0
- package/src/trial/Trial.annotations.test.tsx +121 -0
- package/src/trial/Trial.canvas.test.tsx +76 -1
- package/src/trial/Trial.config.test.tsx +3 -8
- package/src/trial/Trial.less +19 -0
- package/src/trial/Trial.loupe.test.tsx +128 -0
- package/src/trial/Trial.stories.tsx +1 -0
- package/src/trial/Trial.test.tsx +12 -0
- package/src/trial/Trial.trialId.test.tsx +36 -0
- package/src/trial/Trial.tsx +215 -40
- package/src/trial/TrialChrome.tsx +35 -0
- package/src/trial/UndockedSections.tsx +65 -0
- package/src/trial/trialOps.test.ts +21 -0
- package/src/trial/trialOps.ts +5 -3
- package/src/ui/layers/index.ts +5 -1
- package/dist/_dts/index-DgcNdEdh.d.ts +0 -314
- package/dist/chunk-4TMMVIDM.js.map +0 -1
- package/dist/chunk-CPUJ3QXL.js.map +0 -1
- package/dist/chunk-HFDVGF4X.js.map +0 -1
- package/dist/chunk-NS54R52R.js.map +0 -1
- package/dist/chunk-O2BULFHX.js +0 -152
- package/dist/chunk-O2BULFHX.js.map +0 -1
- package/dist/chunk-O7NVSCLN.js.map +0 -1
- package/dist/chunk-PO6L3NSH.js +0 -366
- package/dist/chunk-PO6L3NSH.js.map +0 -1
- package/dist/chunk-PQJ5B2U2.js +0 -27
- package/dist/chunk-PQJ5B2U2.js.map +0 -1
- package/dist/chunk-RVNN4CHQ.js.map +0 -1
- package/dist/chunk-TLGRYALP.js +0 -6151
- package/dist/chunk-TLGRYALP.js.map +0 -1
- package/dist/chunk-V7PRSIRQ.js.map +0 -1
- package/dist/chunk-Z6HJ5FEM.js.map +0 -1
- package/dist/chunk-ZZAYVX4X.js +0 -505
- package/dist/chunk-ZZAYVX4X.js.map +0 -1
- package/src/primitives/DragHandleGlyph.tsx +0 -29
- package/src/ui/format.test.ts +0 -33
- package/src/ui/format.ts +0 -30
- package/src/ui/layers/LayerStack.less +0 -143
- package/src/ui/layers/LayerStack.stories.tsx +0 -45
- package/src/ui/layers/LayerStack.test.tsx +0 -170
- package/src/ui/layers/LayerStack.tsx +0 -208
- package/src/ui/properties/CheckboxRow.stories.tsx +0 -27
- package/src/ui/properties/ColorRow.stories.tsx +0 -106
- package/src/ui/properties/CurveField.less +0 -48
- package/src/ui/properties/CurveField.stories.tsx +0 -20
- package/src/ui/properties/CurveField.test.tsx +0 -127
- package/src/ui/properties/CurveField.tsx +0 -184
- package/src/ui/properties/EffectCard.tsx +0 -324
- package/src/ui/properties/Gallery.stories.tsx +0 -178
- package/src/ui/properties/NumberRow.stories.tsx +0 -65
- package/src/ui/properties/PropertyGroup.less +0 -50
- package/src/ui/properties/PropertyGroup.stories.tsx +0 -34
- package/src/ui/properties/PropertyGroup.test.tsx +0 -33
- package/src/ui/properties/PropertyGroup.tsx +0 -45
- package/src/ui/properties/PropertyList.stories.tsx +0 -83
- package/src/ui/properties/PropertyPanel.less +0 -606
- package/src/ui/properties/PropertyPanel.stories.tsx +0 -190
- package/src/ui/properties/PropertyPanel.test.tsx +0 -193
- package/src/ui/properties/PropertyPanel.tsx +0 -469
- package/src/ui/properties/PropertyRow.stories.tsx +0 -53
- package/src/ui/properties/SelectRow.stories.tsx +0 -58
- package/src/ui/properties/SliderRow.stories.tsx +0 -74
- package/src/ui/properties/SpeechBalloonPanels.stories.tsx +0 -398
- package/src/ui/properties/TextRow.stories.tsx +0 -62
- package/src/ui/properties/ToggleRow.stories.tsx +0 -57
- package/src/ui/properties/index.ts +0 -39
- package/src/ui/properties/storyLayouts.tsx +0 -36
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `useTiledSurface` was built, exported and tested, but nothing mounted a
|
|
3
|
+
* `SurfaceContext` in production — so `useSurfaceOptional()` always answered
|
|
4
|
+
* null and a tile registered from inside a trial reached nothing.
|
|
5
|
+
*
|
|
6
|
+
* The surface is anchored to `.lk-lab__body` rather than wrapped around the
|
|
7
|
+
* workspace: `.lk-lab__body > .lk-workspace { min-width: 0 }` is a direct-child
|
|
8
|
+
* selector, so an extra element between them silently drops that rule.
|
|
9
|
+
*/
|
|
10
|
+
import { act, render } from '@testing-library/react';
|
|
11
|
+
import type { ReactNode } from 'react';
|
|
12
|
+
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
13
|
+
import { defineInstrument } from '../instrument/defineInstrument';
|
|
14
|
+
import { SurfaceContext } from '../surface/SurfaceContext';
|
|
15
|
+
import { useSurfaceOptional, useSurfaceTile } from '../surface/useSurfaceTile';
|
|
16
|
+
import type { SurfaceFrame, SurfaceHandle } from '../surface/useTiledSurface';
|
|
17
|
+
import { useTiledSurface } from '../surface/useTiledSurface';
|
|
18
|
+
import { Lab } from './Lab';
|
|
19
|
+
|
|
20
|
+
/** jsdom measures everything as zero, so each element is given a box by hand. */
|
|
21
|
+
function stubBox(el: HTMLElement, left: number, top: number, width: number, height: number): void {
|
|
22
|
+
vi.spyOn(el, 'getBoundingClientRect').mockReturnValue({
|
|
23
|
+
left,
|
|
24
|
+
top,
|
|
25
|
+
width,
|
|
26
|
+
height,
|
|
27
|
+
right: left + width,
|
|
28
|
+
bottom: top + height,
|
|
29
|
+
x: left,
|
|
30
|
+
y: top,
|
|
31
|
+
toJSON: () => ({}),
|
|
32
|
+
} as DOMRect);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
beforeAll(() => {
|
|
36
|
+
HTMLCanvasElement.prototype.getContext = vi.fn(
|
|
37
|
+
() => null,
|
|
38
|
+
) as unknown as HTMLCanvasElement['getContext'];
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
let seen: SurfaceHandle | null | 'not-rendered' = 'not-rendered';
|
|
42
|
+
|
|
43
|
+
function Probe() {
|
|
44
|
+
seen = useSurfaceOptional();
|
|
45
|
+
const tile = useSurfaceTile('pane');
|
|
46
|
+
return <div data-testid="pane" ref={tile} />;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const probeInstrument = defineInstrument<Record<string, never>, Record<string, never>>({
|
|
50
|
+
name: 'Probe',
|
|
51
|
+
defaultConfig: () => ({}),
|
|
52
|
+
initialState: () => ({}),
|
|
53
|
+
render: () => <Probe />,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('<Lab> surface provider', () => {
|
|
57
|
+
it('puts a surface handle in reach of an instrument', () => {
|
|
58
|
+
seen = 'not-rendered';
|
|
59
|
+
render(<Lab instruments={[probeInstrument]} defaultInstrument="Probe" />);
|
|
60
|
+
expect(seen).not.toBe('not-rendered');
|
|
61
|
+
expect(seen).not.toBeNull();
|
|
62
|
+
expect(typeof (seen as unknown as SurfaceHandle).registerTile).toBe('function');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('mounts one inert buffer inside the lab body', () => {
|
|
66
|
+
const { container } = render(<Lab instruments={[probeInstrument]} defaultInstrument="Probe" />);
|
|
67
|
+
const canvases = container.querySelectorAll('.lk-lab__body > canvas.lk-lab__surface');
|
|
68
|
+
expect(canvases).toHaveLength(1);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('mounts no buffer of its own when a host already owns the surface', () => {
|
|
72
|
+
function Host({ children }: { children: ReactNode }) {
|
|
73
|
+
const surface = useTiledSurface({ onFrame: () => {} });
|
|
74
|
+
return (
|
|
75
|
+
<SurfaceContext.Provider value={surface}>
|
|
76
|
+
<div ref={surface.containerRef}>{children}</div>
|
|
77
|
+
</SurfaceContext.Provider>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
const { container } = render(
|
|
81
|
+
<Host>
|
|
82
|
+
<Lab instruments={[probeInstrument]} defaultInstrument="Probe" />
|
|
83
|
+
</Host>,
|
|
84
|
+
);
|
|
85
|
+
expect(container.querySelectorAll('canvas.lk-lab__surface')).toHaveLength(0);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('keeps the workspace a direct child of the lab body', () => {
|
|
89
|
+
// The min-width:0 rule that keeps the flex row from overflowing is
|
|
90
|
+
// `.lk-lab__body > .lk-workspace`. An element inserted between them drops
|
|
91
|
+
// it, and jsdom cannot see the overflow that follows.
|
|
92
|
+
const { container } = render(<Lab instruments={[probeInstrument]} defaultInstrument="Probe" />);
|
|
93
|
+
const body = container.querySelector('.lk-lab__body');
|
|
94
|
+
expect(body).not.toBeNull();
|
|
95
|
+
expect(body?.querySelector(':scope > .lk-workspace')).not.toBeNull();
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('<Lab> tile round trip', () => {
|
|
100
|
+
beforeEach(() => {
|
|
101
|
+
vi.useFakeTimers();
|
|
102
|
+
vi.stubGlobal('requestAnimationFrame', (cb: FrameRequestCallback) =>
|
|
103
|
+
setTimeout(() => cb(0), 16),
|
|
104
|
+
);
|
|
105
|
+
vi.stubGlobal('cancelAnimationFrame', (id: number) => clearTimeout(id));
|
|
106
|
+
});
|
|
107
|
+
afterEach(() => {
|
|
108
|
+
vi.useRealTimers();
|
|
109
|
+
vi.unstubAllGlobals();
|
|
110
|
+
vi.restoreAllMocks();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('publishes a rect for a tile an instrument registered', () => {
|
|
114
|
+
const frames: SurfaceFrame[] = [];
|
|
115
|
+
let handle: SurfaceHandle | null = null;
|
|
116
|
+
|
|
117
|
+
function Host({ children }: { children: ReactNode }) {
|
|
118
|
+
const surface = useTiledSurface({ onFrame: (f) => frames.push(f) });
|
|
119
|
+
handle = surface;
|
|
120
|
+
return (
|
|
121
|
+
<SurfaceContext.Provider value={surface}>
|
|
122
|
+
<div
|
|
123
|
+
ref={(el) => {
|
|
124
|
+
if (!el) return;
|
|
125
|
+
stubBox(el, 0, 0, 800, 600);
|
|
126
|
+
surface.containerRef(el);
|
|
127
|
+
}}
|
|
128
|
+
>
|
|
129
|
+
{children}
|
|
130
|
+
</div>
|
|
131
|
+
</SurfaceContext.Provider>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
render(
|
|
136
|
+
<Host>
|
|
137
|
+
<Lab instruments={[probeInstrument]} defaultInstrument="Probe" />
|
|
138
|
+
</Host>,
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
// The instrument's own div is the tile. Give it a box the container did
|
|
142
|
+
// not have, so a rect composed against the wrong origin is visible.
|
|
143
|
+
const pane = document.querySelector('[data-testid="pane"]') as HTMLElement;
|
|
144
|
+
expect(pane).not.toBeNull();
|
|
145
|
+
stubBox(pane, 240, 100, 320, 200);
|
|
146
|
+
|
|
147
|
+
act(() => {
|
|
148
|
+
handle?.invalidateRects();
|
|
149
|
+
handle?.invalidate('pane');
|
|
150
|
+
});
|
|
151
|
+
act(() => {
|
|
152
|
+
vi.advanceTimersByTime(64);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Assert the frame fired at all before asserting its contents — a test
|
|
156
|
+
// that passes because nothing ran is the failure mode this arc keeps
|
|
157
|
+
// hitting.
|
|
158
|
+
expect(frames.length).toBeGreaterThan(0);
|
|
159
|
+
const rect = frames[frames.length - 1]?.rects.get('pane');
|
|
160
|
+
expect(rect).toEqual({ x: 240, y: 100, w: 320, h: 200 });
|
|
161
|
+
});
|
|
162
|
+
});
|
package/src/lab/Lab.tsx
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { ThemeProvider } from '@weasel-js/theme/react';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
type CSSProperties,
|
|
4
|
+
type ReactNode,
|
|
5
|
+
useCallback,
|
|
6
|
+
useEffect,
|
|
7
|
+
useMemo,
|
|
8
|
+
useRef,
|
|
9
|
+
useState,
|
|
10
|
+
} from 'react';
|
|
3
11
|
import { useStore } from 'zustand/react';
|
|
4
12
|
import type { TrialContribution } from '../chrome/types';
|
|
5
13
|
import type { ConfigRule, ControlRenderer } from '../config/types';
|
|
@@ -8,6 +16,9 @@ import { noneAdapter } from '../state/adapters';
|
|
|
8
16
|
import { LabStoreContext } from '../state/context';
|
|
9
17
|
import { createLabStore, type LabStore } from '../state/store';
|
|
10
18
|
import type { LabMode, StorageAdapter, TrialRecord } from '../state/types';
|
|
19
|
+
import { SurfaceCanvasContext, SurfaceContext } from '../surface/SurfaceContext';
|
|
20
|
+
import { useSurfaceCanvas, useSurfaceOptional } from '../surface/useSurfaceTile';
|
|
21
|
+
import { useTiledSurface } from '../surface/useTiledSurface';
|
|
11
22
|
import { interstellarTheme } from '../theme/interstellar';
|
|
12
23
|
import type { TrialTool } from '../tools/types';
|
|
13
24
|
import { Trial } from '../trial/Trial';
|
|
@@ -22,8 +33,9 @@ import { LabContext, type LabContextValue } from './LabContext';
|
|
|
22
33
|
import { LabHeader } from './LabHeader';
|
|
23
34
|
import { LabPalette } from './LabPalette';
|
|
24
35
|
import { LabShell } from './LabShell';
|
|
36
|
+
import { createPanelHostRegistry, PanelHostContext } from './panelHost';
|
|
25
37
|
import { useResolvedMode } from './useSystemMode';
|
|
26
|
-
import { type TrialLayout, Workspace } from './Workspace';
|
|
38
|
+
import { type PanelDescriptor, type TrialLayout, Workspace } from './Workspace';
|
|
27
39
|
|
|
28
40
|
/** Props for `<Lab>`. */
|
|
29
41
|
export interface LabProps {
|
|
@@ -137,6 +149,57 @@ export function Lab({
|
|
|
137
149
|
const savedSnapshots = useStore(store, (s) => s.savedSnapshots);
|
|
138
150
|
const modeValue = useStore(store, (s) => s.mode);
|
|
139
151
|
const layout = useStore(store, (s) => s.layout);
|
|
152
|
+
const undockedPanels = useStore(store, (s) => s.undockedPanels);
|
|
153
|
+
|
|
154
|
+
// One registry for the lab's lifetime: a panel host is a DOM node the
|
|
155
|
+
// workspace owns and the trial portals into, so it must outlive both ends of
|
|
156
|
+
// a dock/undock without being rebuilt.
|
|
157
|
+
const panelHostsRef = useRef<ReturnType<typeof createPanelHostRegistry> | null>(null);
|
|
158
|
+
if (panelHostsRef.current === null) panelHostsRef.current = createPanelHostRegistry();
|
|
159
|
+
|
|
160
|
+
// One shared drawing surface for the whole lab, anchored to the body — tile
|
|
161
|
+
// rects compose against it, and every tile paints into the one buffer.
|
|
162
|
+
// `Workspace` invalidates rects when the grid moves something a
|
|
163
|
+
// ResizeObserver cannot see. A host that already owns a surface keeps it: a
|
|
164
|
+
// lab embedded in a larger shared-surface app must not open a second GL
|
|
165
|
+
// tenancy, and mounts no buffer of its own.
|
|
166
|
+
const outerSurface = useSurfaceOptional();
|
|
167
|
+
const outerCanvas = useSurfaceCanvas();
|
|
168
|
+
const [ownCanvas, setOwnCanvas] = useState<HTMLCanvasElement | null>(null);
|
|
169
|
+
const ownCanvasRef = useRef<HTMLCanvasElement | null>(null);
|
|
170
|
+
const bufferRef = useRef({ w: 0, h: 0 });
|
|
171
|
+
const surfaceRef = useRef<ReturnType<typeof useTiledSurface> | null>(null);
|
|
172
|
+
|
|
173
|
+
// Sizing the buffer clears all of it, so every tile has to repaint — not
|
|
174
|
+
// only the one whose move triggered the measurement.
|
|
175
|
+
const onFrame = useCallback((frame: { size: { width: number; height: number }; dpr: number }) => {
|
|
176
|
+
const c = ownCanvasRef.current;
|
|
177
|
+
if (!c) return;
|
|
178
|
+
const w = Math.round(frame.size.width * frame.dpr);
|
|
179
|
+
const h = Math.round(frame.size.height * frame.dpr);
|
|
180
|
+
if (bufferRef.current.w === w && bufferRef.current.h === h) return;
|
|
181
|
+
bufferRef.current = { w, h };
|
|
182
|
+
c.width = w;
|
|
183
|
+
c.height = h;
|
|
184
|
+
c.style.width = `${frame.size.width}px`;
|
|
185
|
+
c.style.height = `${frame.size.height}px`;
|
|
186
|
+
surfaceRef.current?.invalidateAll();
|
|
187
|
+
}, []);
|
|
188
|
+
|
|
189
|
+
const ownSurface = useTiledSurface({ onFrame });
|
|
190
|
+
surfaceRef.current = ownSurface;
|
|
191
|
+
const surface = outerSurface ?? ownSurface;
|
|
192
|
+
const surfaceCanvas = outerSurface ? outerCanvas : ownCanvas;
|
|
193
|
+
|
|
194
|
+
const workspacePanels = useMemo<PanelDescriptor[]>(
|
|
195
|
+
() =>
|
|
196
|
+
Object.entries(undockedPanels).map(([key, panel]) => ({
|
|
197
|
+
key,
|
|
198
|
+
title: panel.sectionId,
|
|
199
|
+
as: panel.as,
|
|
200
|
+
})),
|
|
201
|
+
[undockedPanels],
|
|
202
|
+
);
|
|
140
203
|
const resolvedMode = useResolvedMode(modeValue);
|
|
141
204
|
|
|
142
205
|
useEffect(() => {
|
|
@@ -232,21 +295,43 @@ export function Lab({
|
|
|
232
295
|
</>
|
|
233
296
|
}
|
|
234
297
|
>
|
|
235
|
-
<
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
298
|
+
<PanelHostContext.Provider value={panelHostsRef.current}>
|
|
299
|
+
<SurfaceContext.Provider value={surface}>
|
|
300
|
+
<SurfaceCanvasContext.Provider value={surfaceCanvas}>
|
|
301
|
+
<div
|
|
302
|
+
className="lk-lab__body"
|
|
303
|
+
ref={outerSurface ? undefined : ownSurface.containerRef}
|
|
304
|
+
>
|
|
305
|
+
{outerSurface ? null : (
|
|
306
|
+
// Above the trials and inert: the marks a tile paints have
|
|
307
|
+
// to sit over the instrument's own DOM, and nothing on this
|
|
308
|
+
// buffer takes input — each tile has its own input box.
|
|
309
|
+
<canvas
|
|
310
|
+
className="lk-lab__surface"
|
|
311
|
+
ref={(el) => {
|
|
312
|
+
ownCanvasRef.current = el;
|
|
313
|
+
setOwnCanvas(el);
|
|
314
|
+
}}
|
|
315
|
+
/>
|
|
316
|
+
)}
|
|
317
|
+
{tools ? <LabPalette tools={tools} /> : null}
|
|
318
|
+
<Workspace
|
|
319
|
+
panels={workspacePanels}
|
|
320
|
+
ids={trials.map((w) => w.id)}
|
|
321
|
+
resizable
|
|
322
|
+
reorderable
|
|
323
|
+
onReorder={(ids) => contextValue.reorderTrials(ids)}
|
|
324
|
+
layout={layout as TrialLayout}
|
|
325
|
+
onLayoutChange={(next) => store.getState().setLayout(next)}
|
|
326
|
+
>
|
|
327
|
+
{trials.map((w) => (
|
|
328
|
+
<Trial key={w.id} id={w.id} chrome={chrome} suppress={suppress} />
|
|
329
|
+
))}
|
|
330
|
+
</Workspace>
|
|
331
|
+
</div>
|
|
332
|
+
</SurfaceCanvasContext.Provider>
|
|
333
|
+
</SurfaceContext.Provider>
|
|
334
|
+
</PanelHostContext.Provider>
|
|
250
335
|
</LabShell>
|
|
251
336
|
</ThemeProvider>
|
|
252
337
|
</LabContext.Provider>
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import type { TrialContribution } from '../chrome/types';
|
|
5
|
+
import type { Instrument } from '../instrument/types';
|
|
6
|
+
import { Lab } from './Lab';
|
|
7
|
+
|
|
8
|
+
const bare: Instrument = {
|
|
9
|
+
name: 'Bare',
|
|
10
|
+
defaultConfig: () => ({}),
|
|
11
|
+
initialState: () => ({}),
|
|
12
|
+
render: () => null,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const notes: TrialContribution = {
|
|
16
|
+
id: 'notes',
|
|
17
|
+
region: 'sidebar',
|
|
18
|
+
item: { title: 'Notes', body: <p data-testid="notes-body">jot</p> },
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const floater: TrialContribution = {
|
|
22
|
+
id: 'floater',
|
|
23
|
+
region: 'sidebar',
|
|
24
|
+
item: {
|
|
25
|
+
title: 'Floater',
|
|
26
|
+
undockAs: 'floating',
|
|
27
|
+
body: <p data-testid="floater-body">hover</p>,
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const pinned: TrialContribution = {
|
|
32
|
+
id: 'pinned',
|
|
33
|
+
region: 'sidebar',
|
|
34
|
+
item: { title: 'Pinned', undockable: false, body: <p>stay</p> },
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function renderLab() {
|
|
38
|
+
return render(<Lab title="T" instruments={[bare]} defaultInstrument="Bare" chrome={[notes]} />);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe('undocking a sidebar section', () => {
|
|
42
|
+
it('starts docked inside the trial sidebar', () => {
|
|
43
|
+
const { container } = renderLab();
|
|
44
|
+
const sidebar = container.querySelector('.lk-trial__sidebar');
|
|
45
|
+
expect(sidebar).toContainElement(screen.getByTestId('notes-body'));
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('moves the section out of the sidebar and into a workspace panel', async () => {
|
|
49
|
+
const { container } = renderLab();
|
|
50
|
+
await userEvent.click(screen.getByRole('button', { name: 'Undock Notes' }));
|
|
51
|
+
|
|
52
|
+
expect(container.querySelector('.lk-trial__sidebar')).not.toContainElement(
|
|
53
|
+
screen.queryByTestId('notes-body'),
|
|
54
|
+
);
|
|
55
|
+
const panel = container.querySelector('.lk-panel-tile');
|
|
56
|
+
expect(panel).toContainElement(screen.getByTestId('notes-body'));
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('docks it back', async () => {
|
|
60
|
+
const { container } = renderLab();
|
|
61
|
+
await userEvent.click(screen.getByRole('button', { name: 'Undock Notes' }));
|
|
62
|
+
await userEvent.click(screen.getByRole('button', { name: 'Dock Notes' }));
|
|
63
|
+
|
|
64
|
+
expect(container.querySelector('.lk-panel-tile')).toBeNull();
|
|
65
|
+
expect(container.querySelector('.lk-trial__sidebar')).toContainElement(
|
|
66
|
+
screen.getByTestId('notes-body'),
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('sends a section declaring undockAs floating to the floating layer', async () => {
|
|
71
|
+
const { container } = render(
|
|
72
|
+
<Lab title="T" instruments={[bare]} defaultInstrument="Bare" chrome={[floater]} />,
|
|
73
|
+
);
|
|
74
|
+
await userEvent.click(screen.getByRole('button', { name: 'Undock Floater' }));
|
|
75
|
+
|
|
76
|
+
const floating = container.querySelector('.lk-workspace__floating');
|
|
77
|
+
expect(floating).toContainElement(screen.getByTestId('floater-body'));
|
|
78
|
+
expect(container.querySelector('.lk-panel-tile--floating')).not.toBeNull();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('offers no tear-out control on a section that opts out', () => {
|
|
82
|
+
render(<Lab title="T" instruments={[bare]} defaultInstrument="Bare" chrome={[pinned]} />);
|
|
83
|
+
expect(screen.queryByRole('button', { name: 'Undock Pinned' })).toBeNull();
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -280,6 +280,26 @@ export const AllChrome: Story = {
|
|
|
280
280
|
},
|
|
281
281
|
};
|
|
282
282
|
|
|
283
|
+
/** Undocking, in both targets. `Notes` declares `undockAs: 'floating'`, so its
|
|
284
|
+
* tear-out control sends it to the layer above the grid; every other section
|
|
285
|
+
* defaults to a tile and joins the tiling as a peer of the trials. */
|
|
286
|
+
export const UndockablePanels: Story = {
|
|
287
|
+
args: {
|
|
288
|
+
...AllChrome.args,
|
|
289
|
+
chrome: [
|
|
290
|
+
{
|
|
291
|
+
id: 'notes',
|
|
292
|
+
region: 'sidebar',
|
|
293
|
+
item: {
|
|
294
|
+
title: 'Notes',
|
|
295
|
+
undockAs: 'floating',
|
|
296
|
+
body: <p className="lk-run-tag">Undock me — I float.</p>,
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
],
|
|
300
|
+
},
|
|
301
|
+
};
|
|
302
|
+
|
|
283
303
|
/** The chrome the other story cannot show at the same time: one instrument, so
|
|
284
304
|
* the header is an Add trial button; one trial, so Close is disabled; no
|
|
285
305
|
* config schema, canvas, undo or job, so the sidebar falls back to its
|
package/src/lab/LabShell.less
CHANGED
|
@@ -48,12 +48,23 @@
|
|
|
48
48
|
display: flex;
|
|
49
49
|
height: 100%;
|
|
50
50
|
min-height: 0;
|
|
51
|
+
position: relative;
|
|
51
52
|
|
|
52
53
|
> .lk-workspace {
|
|
53
54
|
min-width: 0;
|
|
54
55
|
}
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
// The lab's one shared drawing buffer. Over the trials, because a tile's marks
|
|
59
|
+
// annotate the instrument's own DOM; under the viewport chrome, whose z-overlay
|
|
60
|
+
// outranks it; and inert, because every tile takes input from its own box.
|
|
61
|
+
.lk-lab__surface {
|
|
62
|
+
position: absolute;
|
|
63
|
+
inset: 0;
|
|
64
|
+
z-index: 1;
|
|
65
|
+
pointer-events: none;
|
|
66
|
+
}
|
|
67
|
+
|
|
57
68
|
.lk-shell-footer {
|
|
58
69
|
border-top: 1px solid var(--wzl-border);
|
|
59
70
|
padding: var(--wzl-space-xs) var(--wzl-space-md);
|
package/src/lab/Workspace.less
CHANGED
|
@@ -9,3 +9,76 @@
|
|
|
9
9
|
height: 100%;
|
|
10
10
|
min-height: 0;
|
|
11
11
|
}
|
|
12
|
+
|
|
13
|
+
// An undocked sidebar section. A tile version is placed by the grid like any
|
|
14
|
+
// trial; the floating version sits above the grid in its own layer.
|
|
15
|
+
.lk-panel-tile {
|
|
16
|
+
display: flex;
|
|
17
|
+
flex-direction: column;
|
|
18
|
+
height: 100%;
|
|
19
|
+
overflow: hidden;
|
|
20
|
+
border: var(--wzl-border-w) solid var(--wzl-line-subtle);
|
|
21
|
+
border-radius: var(--wzl-radius-sm);
|
|
22
|
+
background: var(--wzl-surface);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.lk-panel-tile__titlebar {
|
|
26
|
+
display: flex;
|
|
27
|
+
align-items: center;
|
|
28
|
+
gap: var(--wzl-space-sm);
|
|
29
|
+
flex-shrink: 0;
|
|
30
|
+
padding: 0 var(--wzl-space-sm);
|
|
31
|
+
border-bottom: var(--wzl-border-w) solid var(--wzl-line-subtle);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.lk-panel-tile__title {
|
|
35
|
+
flex: 1;
|
|
36
|
+
color: var(--wzl-fg-muted);
|
|
37
|
+
font-family: var(--wzl-font-display);
|
|
38
|
+
font-size: var(--wzl-font-size-sm);
|
|
39
|
+
letter-spacing: 0.08em;
|
|
40
|
+
text-transform: uppercase;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.lk-panel-tile__dock {
|
|
44
|
+
border: 0;
|
|
45
|
+
background: transparent;
|
|
46
|
+
color: var(--wzl-fg-subtle);
|
|
47
|
+
cursor: pointer;
|
|
48
|
+
|
|
49
|
+
&:hover {
|
|
50
|
+
color: var(--wzl-fg);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.lk-panel-tile__body {
|
|
55
|
+
flex: 1;
|
|
56
|
+
min-height: 0;
|
|
57
|
+
overflow: auto;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.lk-workspace-host {
|
|
61
|
+
position: relative;
|
|
62
|
+
width: 100%;
|
|
63
|
+
height: 100%;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.lk-workspace__floating {
|
|
67
|
+
position: absolute;
|
|
68
|
+
inset: 0;
|
|
69
|
+
pointer-events: none;
|
|
70
|
+
|
|
71
|
+
> * {
|
|
72
|
+
pointer-events: auto;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.lk-panel-tile--floating {
|
|
77
|
+
position: absolute;
|
|
78
|
+
top: var(--wzl-space-md);
|
|
79
|
+
right: var(--wzl-space-md);
|
|
80
|
+
width: 260px;
|
|
81
|
+
height: auto;
|
|
82
|
+
max-height: 60%;
|
|
83
|
+
box-shadow: var(--wzl-shadow-panel, 0 2px 8px rgb(0 0 0 / 0.3));
|
|
84
|
+
}
|