@weasel-js/labkit 1.3.0 → 1.4.0-pre.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 +226 -55
- package/dist/_dts/CanvasStackContext-LnCfqNBA.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-C-2c72Ij.d.ts +735 -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-DCGEcMO_.d.ts → types-D6s4b7if.d.ts} +1 -1
- 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 +110 -26
- package/dist/canvas/index.js +5 -4
- package/dist/chrome/index.d.ts +9 -6
- package/dist/chrome/index.js +6 -6
- package/dist/chunk-2AYGEN57.js +32 -0
- package/dist/chunk-2AYGEN57.js.map +1 -0
- package/dist/chunk-6OEYTYML.js +669 -0
- package/dist/chunk-6OEYTYML.js.map +1 -0
- package/dist/{chunk-BMW4TDP5.js → chunk-AE5CNVRU.js} +3 -3
- package/dist/{chunk-BMW4TDP5.js.map → chunk-AE5CNVRU.js.map} +1 -1
- 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-E2UQVZ44.js +458 -0
- package/dist/chunk-E2UQVZ44.js.map +1 -0
- package/dist/{chunk-LGKRIUSW.js → chunk-ESIQNQ6K.js} +32 -7
- package/dist/chunk-ESIQNQ6K.js.map +1 -0
- package/dist/{chunk-JDROYQM3.js → chunk-HHGVISVZ.js} +130 -131
- package/dist/chunk-HHGVISVZ.js.map +1 -0
- package/dist/{chunk-BDJXIBRZ.js → chunk-HMFODCOX.js} +126 -29
- package/dist/chunk-HMFODCOX.js.map +1 -0
- package/dist/{chunk-QCICIHEO.js → chunk-L5QJOOLV.js} +839 -320
- package/dist/chunk-L5QJOOLV.js.map +1 -0
- package/dist/{chunk-3XXPU73K.js → chunk-M75ZU6ZZ.js} +9 -7
- package/dist/chunk-M75ZU6ZZ.js.map +1 -0
- package/dist/{chunk-QRYPXSGP.js → chunk-QB5SM4BM.js} +16 -8
- package/dist/chunk-QB5SM4BM.js.map +1 -0
- package/dist/chunk-RBGKL7NF.js +7128 -0
- package/dist/chunk-RBGKL7NF.js.map +1 -0
- package/dist/{chunk-WTI26YTM.js → chunk-TO2FUOKF.js} +40 -13
- package/dist/chunk-TO2FUOKF.js.map +1 -0
- package/dist/{chunk-4TMMVIDM.js → chunk-XJ6N32QP.js} +50 -6
- package/dist/chunk-XJ6N32QP.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 +9 -5
- package/dist/dragdrop/index.js +3 -3
- package/dist/index.d.ts +517 -234
- package/dist/index.js +1769 -143
- package/dist/index.js.map +1 -1
- package/dist/layers/index.d.ts +6 -5
- package/dist/layers/index.js +3 -3
- 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 +138 -91
- package/dist/passthrough/weasel-ui.js +2 -2
- package/dist/primitives/index.d.ts +3 -1
- package/dist/primitives/index.js +5 -5
- package/dist/state/index.d.ts +3 -3
- package/dist/state/index.js +2 -2
- package/dist/styles.css +229 -1
- package/dist/surface/index.d.ts +18 -2
- package/dist/surface/index.js +2 -2
- package/dist/ui/layers/index.js +2 -2
- 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 +247 -0
- package/src/annotations/store.ts +347 -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 +229 -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 +16 -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/controls/ControlPanel.test.tsx +38 -0
- package/src/controls/ControlPanel.tsx +50 -3
- package/src/dragdrop/DragDropRuntime.tsx +8 -2
- package/src/dragdrop/dragDrop.test.tsx +29 -2
- package/src/index.test.ts +23 -0
- package/src/index.ts +93 -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/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 +3 -0
- 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 -0
- 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/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.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/dist/_dts/types-BttTIent.d.ts +0 -314
- package/dist/chunk-3XXPU73K.js.map +0 -1
- package/dist/chunk-4TMMVIDM.js.map +0 -1
- package/dist/chunk-6X5RBC6G.js +0 -366
- package/dist/chunk-6X5RBC6G.js.map +0 -1
- package/dist/chunk-BDJXIBRZ.js.map +0 -1
- package/dist/chunk-CPUJ3QXL.js.map +0 -1
- package/dist/chunk-JDROYQM3.js.map +0 -1
- package/dist/chunk-LGKRIUSW.js.map +0 -1
- package/dist/chunk-QCICIHEO.js.map +0 -1
- package/dist/chunk-QRYPXSGP.js.map +0 -1
- package/dist/chunk-RGT4EJIB.js +0 -6961
- package/dist/chunk-RGT4EJIB.js.map +0 -1
- package/dist/chunk-T66HU3HX.js +0 -17
- package/dist/chunk-T66HU3HX.js.map +0 -1
- package/dist/chunk-WTI26YTM.js.map +0 -1
package/src/index.ts
CHANGED
|
@@ -47,17 +47,103 @@ export {
|
|
|
47
47
|
TextRow,
|
|
48
48
|
ToggleRow,
|
|
49
49
|
} from '@weasel-js/ui';
|
|
50
|
-
export type {
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
export type {
|
|
51
|
+
Annotation,
|
|
52
|
+
AnnotationData,
|
|
53
|
+
AnnotationInit,
|
|
54
|
+
AnnotationKind,
|
|
55
|
+
AnnotationMeaning,
|
|
56
|
+
AnnotationPatch,
|
|
57
|
+
AnnotationQuery,
|
|
58
|
+
AnnotationStatus,
|
|
59
|
+
AnnotationStoreOptions,
|
|
60
|
+
AnnotationsApi,
|
|
61
|
+
AnnotationsCapability,
|
|
62
|
+
AnnotationTarget,
|
|
63
|
+
AnnotationTargetInfo,
|
|
64
|
+
CaptureDeps,
|
|
65
|
+
CaptureOptions,
|
|
66
|
+
CaptureResult,
|
|
67
|
+
CaptureSource,
|
|
68
|
+
FracPoint,
|
|
69
|
+
FracRect,
|
|
70
|
+
MarkScene,
|
|
71
|
+
SerializedAnnotations,
|
|
72
|
+
WorldRect,
|
|
73
|
+
} from './annotations';
|
|
74
|
+
export {
|
|
75
|
+
annotationsFromJSON,
|
|
76
|
+
createAnnotationScene,
|
|
77
|
+
createAnnotationStore,
|
|
78
|
+
fracContains,
|
|
79
|
+
fracIntersects,
|
|
80
|
+
fracToWorld,
|
|
81
|
+
isStale,
|
|
82
|
+
roundFrac,
|
|
83
|
+
seenFrom,
|
|
84
|
+
worldToFrac,
|
|
85
|
+
} from './annotations';
|
|
86
|
+
export type { AnnotationOverlayProps } from './annotations/AnnotationOverlay';
|
|
87
|
+
export { AnnotationOverlay } from './annotations/AnnotationOverlay';
|
|
88
|
+
export {
|
|
89
|
+
AnnotationsContext,
|
|
90
|
+
useAnnotations,
|
|
91
|
+
useAnnotationsOptional,
|
|
92
|
+
} from './annotations/AnnotationsContext';
|
|
93
|
+
export type { AnnotationTargetsProps } from './annotations/AnnotationTargets';
|
|
94
|
+
export { AnnotationTargets } from './annotations/AnnotationTargets';
|
|
95
|
+
export type { CaptureArgs, CapturePlan, ComposeSvgArgs } from './annotations/capture';
|
|
96
|
+
export { capturePlan, captureTarget, composeCaptureSvg } from './annotations/capture';
|
|
97
|
+
export type { MarkDrawOptions } from './annotations/drawOne';
|
|
98
|
+
export { createMarkDrawOne, resolveMarkStyle } from './annotations/drawOne';
|
|
99
|
+
export { ExportMenu } from './annotations/ExportMenu';
|
|
100
|
+
export type { MarkListProps } from './annotations/MarkList';
|
|
101
|
+
export { MarkList } from './annotations/MarkList';
|
|
102
|
+
export type { MarkStyle, PaintableMark } from './annotations/paint';
|
|
103
|
+
export { markCommands } from './annotations/paint';
|
|
104
|
+
export { markSvgNodes } from './annotations/svgNodes';
|
|
105
|
+
export type { AnnotationToolInfo } from './annotations/toolMap';
|
|
106
|
+
export {
|
|
107
|
+
ANNOTATION_TOOLS,
|
|
108
|
+
ANNOTATION_WEASEL_TOOLS,
|
|
109
|
+
annotationToolInfo,
|
|
110
|
+
} from './annotations/toolMap';
|
|
111
|
+
export type { ContentSize, PaneSize } from './annotations/view';
|
|
112
|
+
export { fitView, fromWeaselView, toWeaselView } from './annotations/view';
|
|
113
|
+
export type {
|
|
114
|
+
CanvasLayerDescriptor,
|
|
115
|
+
CanvasStackContextValue,
|
|
116
|
+
CanvasStackProps,
|
|
117
|
+
CanvasStackSurface,
|
|
118
|
+
OrbitHandlers,
|
|
119
|
+
OrbitView,
|
|
120
|
+
PanZoomHandlers,
|
|
121
|
+
UseOrbitOptions,
|
|
122
|
+
UsePanZoomOptions,
|
|
123
|
+
Vec3,
|
|
124
|
+
ViewportSize,
|
|
125
|
+
WorldFrame,
|
|
126
|
+
WorldSpec,
|
|
127
|
+
ZoomAtOptions,
|
|
128
|
+
} from './canvas';
|
|
53
129
|
export {
|
|
130
|
+
applyCamera,
|
|
131
|
+
CanvasStack,
|
|
132
|
+
CanvasStackContext,
|
|
133
|
+
centerOn,
|
|
54
134
|
clampPitch,
|
|
135
|
+
DEFAULT_FRAME,
|
|
55
136
|
orbitAfterDrag,
|
|
56
137
|
orbitAfterWheel,
|
|
57
138
|
PITCH_LIMIT,
|
|
139
|
+
resolveFrame,
|
|
140
|
+
screenToWorld,
|
|
58
141
|
useOrbit,
|
|
142
|
+
usePanZoom,
|
|
143
|
+
worldToScreen,
|
|
59
144
|
wrapYaw,
|
|
60
|
-
|
|
145
|
+
zoomAt,
|
|
146
|
+
} from './canvas';
|
|
61
147
|
export * from './chrome';
|
|
62
148
|
export { f } from './config/builder';
|
|
63
149
|
export { fromConfigFields } from './config/fromConfigField';
|
|
@@ -118,6 +204,7 @@ export * from './job';
|
|
|
118
204
|
export * from './lab';
|
|
119
205
|
export type { LayerListProps } from './layers';
|
|
120
206
|
export { LayerList } from './layers';
|
|
207
|
+
export * from './loupe';
|
|
121
208
|
export * from './primitives';
|
|
122
209
|
export {
|
|
123
210
|
createMemoryAdapter,
|
|
@@ -147,6 +234,8 @@ export type {
|
|
|
147
234
|
TrialStateHandle,
|
|
148
235
|
UndoStack,
|
|
149
236
|
} from './state/types';
|
|
237
|
+
export type { UndockedPanel, UndockedPanels } from './state/undock';
|
|
238
|
+
export { dockPanel, panelKey, undockPanel } from './state/undock';
|
|
150
239
|
export { useTrialState } from './state/useTrialState';
|
|
151
240
|
export type { ViewTransform2D } from './state/view';
|
|
152
241
|
export { as2DView, DEFAULT_VIEW } from './state/view';
|
package/src/instrument/types.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
|
+
import type { AnnotationsCapability } from '../annotations/types';
|
|
3
|
+
import type { ViewportSize, WorldSpec } from '../canvas/worldSpec';
|
|
2
4
|
import type { TrialContribution } from '../chrome/types';
|
|
3
5
|
import type { ConfigSchema } from '../config/types';
|
|
4
6
|
import type { ConfigField } from '../controls/types';
|
|
5
7
|
import type { JobCapability, JobHandle } from '../job/types';
|
|
8
|
+
import type { LoupeDeclaration } from '../loupe/types';
|
|
6
9
|
import type { ToolCapability } from '../tools/types';
|
|
7
10
|
|
|
8
11
|
/** What an instrument's `render` is handed: its state and config, the setters
|
|
@@ -25,6 +28,10 @@ export interface RenderContext<TS = unknown, TC = unknown> {
|
|
|
25
28
|
/** Resolved active tool: this trial's slot, or the lab's. Null when neither
|
|
26
29
|
* holds one. */
|
|
27
30
|
activeToolId: string | null;
|
|
31
|
+
/** The canvas layers currently shown, in declaration order. labkit skips a
|
|
32
|
+
* hidden layer's `draw`, so this is the only way to know what was
|
|
33
|
+
* painted — a legend listing rows the view did not draw needs it. */
|
|
34
|
+
visibleLayers: readonly string[];
|
|
28
35
|
};
|
|
29
36
|
emit: (event: string) => void;
|
|
30
37
|
/** Present only when the instrument declares a `job`. */
|
|
@@ -45,7 +52,14 @@ export interface CanvasLayer<TS = unknown, TC = unknown> {
|
|
|
45
52
|
* view starts. */
|
|
46
53
|
export interface CanvasCapability<TS = unknown, TC = unknown> {
|
|
47
54
|
layers: CanvasLayer<TS, TC>[];
|
|
48
|
-
|
|
55
|
+
/** The coordinate system this instrument's world is in. Omitted, world (0,0)
|
|
56
|
+
* sits at the canvas top-left with y running down — what labkit assumed
|
|
57
|
+
* before an instrument could say otherwise. */
|
|
58
|
+
worldSpec?: WorldSpec;
|
|
59
|
+
/** Where the view starts. A function is called once the canvas knows its
|
|
60
|
+
* size, so an instrument can frame content it can only place in terms of
|
|
61
|
+
* the viewport; until then the trial's view is `null`. */
|
|
62
|
+
initialView?: ViewTransform | ((size: ViewportSize) => ViewTransform);
|
|
49
63
|
/** Widens `usePanZoom`'s default clamp; the opening zoom stays reachable
|
|
50
64
|
* regardless of these. */
|
|
51
65
|
minZoom?: number;
|
|
@@ -127,6 +141,15 @@ export interface Instrument<TS = unknown, TC = unknown, TItem = unknown> {
|
|
|
127
141
|
/** Tools this instrument offers. Declaring them gives the trial a palette
|
|
128
142
|
* region and its own tool slot. */
|
|
129
143
|
tools?: ToolCapability;
|
|
144
|
+
/** Regions of this instrument that accept marks, and optionally what a mark
|
|
145
|
+
* is allowed to mean. Declaring it is what makes the trial provide the
|
|
146
|
+
* annotation overlay and its chrome. */
|
|
147
|
+
annotations?: AnnotationsCapability<TS, TC>;
|
|
148
|
+
/** Magnification. `true` takes every default and re-draws the instrument's
|
|
149
|
+
* canvas layers through a zoomed camera; an instrument whose content is DOM
|
|
150
|
+
* gives a `render` that draws it again at a camera it is handed. A function
|
|
151
|
+
* is re-read as the config changes, so a setting can drive the lens. */
|
|
152
|
+
loupe?: LoupeDeclaration<TS, TC> | ((config: TC) => LoupeDeclaration<TS, TC>);
|
|
130
153
|
/** Chrome this instrument contributes beyond what its capabilities imply. */
|
|
131
154
|
chrome?: TrialContribution[];
|
|
132
155
|
/** Work too slow to do during a render. The runtime starts it, aborts it on
|
|
@@ -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);
|