@weasel-js/labkit 1.0.4 → 1.1.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/README.md +6 -6
- package/dist/_dts/{index-CFlDPeZh.d.ts → index-iAP6XbH3.d.ts} +17 -7
- package/dist/_dts/{useExperimentState-D7EQnnwJ.d.ts → useTrialState-DYe2vUwN.d.ts} +60 -36
- package/dist/canvas/index.d.ts +1 -1
- package/dist/chunk-3TYUJR7Z.js +488 -0
- package/dist/chunk-3TYUJR7Z.js.map +1 -0
- package/dist/{chunk-BAPZPDDA.js → chunk-C6GJKPUI.js} +365 -340
- package/dist/chunk-C6GJKPUI.js.map +1 -0
- package/dist/{chunk-DWV7SFKR.js → chunk-DJLDIRFN.js} +28 -12
- package/dist/chunk-DJLDIRFN.js.map +1 -0
- package/dist/{chunk-DEWXYFEU.js → chunk-NRKWVTVT.js} +3 -3
- package/dist/{chunk-DEWXYFEU.js.map → chunk-NRKWVTVT.js.map} +1 -1
- package/dist/dragdrop/index.d.ts +1 -1
- package/dist/index.d.ts +149 -149
- package/dist/index.js +164 -159
- package/dist/index.js.map +1 -1
- package/dist/layers/index.d.ts +2 -2
- package/dist/passthrough/weasel-canvas.js +1 -1
- package/dist/passthrough/weasel-ui.d.ts +9 -2
- package/dist/passthrough/weasel-ui.js +2 -2
- package/dist/state/index.d.ts +12 -12
- package/dist/state/index.js +5 -5
- package/dist/state/index.js.map +1 -1
- package/dist/styles.css +21 -13
- package/dist/ui/layers/index.js +3 -3
- package/dist/undo/index.d.ts +1 -1
- package/package.json +1 -1
- package/src/canvas/AGENTS.md +6 -2
- package/src/dragdrop/dragDrop.test.tsx +4 -7
- package/src/index.test.ts +52 -0
- package/src/index.ts +11 -7
- package/src/instrument/SineWave.smoke.test.tsx +1 -1
- package/src/instrument/capabilityDetector.ts +1 -1
- package/src/instrument/types.ts +18 -6
- package/src/lab/Lab.less +16 -0
- package/src/lab/Lab.stories.tsx +5 -5
- package/src/lab/Lab.test.tsx +25 -25
- package/src/lab/Lab.tsx +50 -50
- package/src/lab/LabContext.ts +12 -12
- package/src/lab/LabShell.tsx +1 -1
- package/src/lab/{WorkspaceGrid.less → Workspace.less} +2 -2
- package/src/lab/{WorkspaceGrid.stories.tsx → Workspace.stories.tsx} +13 -13
- package/src/lab/{WorkspaceGrid.test.tsx → Workspace.test.tsx} +24 -24
- package/src/lab/{WorkspaceGrid.tsx → Workspace.tsx} +18 -18
- package/src/lab/index.ts +2 -2
- package/src/layers/AGENTS.md +3 -3
- package/src/primitives/Toolbar.stories.tsx +2 -2
- package/src/state/SingletonExperiment.test.tsx +8 -8
- package/src/state/SingletonExperiment.tsx +9 -9
- package/src/state/context.tsx +12 -12
- package/src/state/document.test.ts +369 -0
- package/src/state/document.ts +194 -0
- package/src/state/helpers.test.ts +74 -46
- package/src/state/helpers.ts +21 -29
- package/src/state/index.ts +16 -8
- package/src/state/store.test.ts +440 -41
- package/src/state/store.ts +159 -102
- package/src/state/types.ts +29 -12
- package/src/state/{useExperimentState.test.tsx → useTrialState.test.tsx} +18 -18
- package/src/state/useTrialState.ts +29 -0
- package/src/styles.less +2 -2
- package/src/theme/base.less +1 -1
- package/src/{workspace → trial}/DefaultSidebar.tsx +3 -3
- package/src/{workspace → trial}/DefaultStatusBar.tsx +5 -4
- package/src/{workspace → trial}/DefaultToolbar.tsx +9 -8
- package/src/trial/Trial.canvas.test.tsx +80 -0
- package/src/{workspace/Workspace.less → trial/Trial.less} +1 -1
- package/src/{workspace/Workspace.stories.tsx → trial/Trial.stories.tsx} +13 -18
- package/src/{workspace/Workspace.test.tsx → trial/Trial.test.tsx} +26 -26
- package/src/{workspace/Workspace.tsx → trial/Trial.tsx} +48 -43
- package/src/{workspace/WorkspaceChrome.tsx → trial/TrialChrome.tsx} +42 -42
- package/src/{workspace → trial}/index.ts +13 -13
- package/src/{workspace → trial}/slotTypes.ts +16 -16
- package/src/{workspace/workspaceOps.test.ts → trial/trialOps.test.ts} +38 -44
- package/src/trial/trialOps.ts +99 -0
- package/src/ui/format.test.ts +33 -0
- package/src/ui/format.ts +11 -0
- package/src/ui/properties/CurveField.tsx +6 -6
- package/src/ui/properties/SpeechBalloonPanels.stories.tsx +3 -3
- package/dist/chunk-BAPZPDDA.js.map +0 -1
- package/dist/chunk-DWV7SFKR.js.map +0 -1
- package/dist/chunk-KSTEW2AF.js +0 -367
- package/dist/chunk-KSTEW2AF.js.map +0 -1
- package/src/state/useExperimentState.ts +0 -31
- package/src/workspace/workspaceOps.ts +0 -102
package/dist/layers/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { L as LayerCapability } from '../_dts/index-
|
|
1
|
+
import { d as LayerDescriptor } from '../_dts/index-iAP6XbH3.js';
|
|
2
|
+
export { L as LayerCapability } from '../_dts/index-iAP6XbH3.js';
|
|
3
3
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
4
4
|
import 'react';
|
|
5
5
|
import '../_dts/types-x92Kfeme.js';
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { FALLBACK_FIT_VIEW, MinimapCanvas, SceneViewCanvas, buildSceneViewCommands, computeFitView2 as computeFitView, computeIndicatorCommand, renderSceneToCanvas } from '../chunk-
|
|
1
|
+
export { FALLBACK_FIT_VIEW, MinimapCanvas, SceneViewCanvas, buildSceneViewCommands, computeFitView2 as computeFitView, computeIndicatorCommand, renderSceneToCanvas } from '../chunk-DJLDIRFN.js';
|
|
2
2
|
//# sourceMappingURL=weasel-canvas.js.map
|
|
3
3
|
//# sourceMappingURL=weasel-canvas.js.map
|
|
@@ -1082,6 +1082,10 @@ type TrackCtx = {
|
|
|
1082
1082
|
* `onInput` fires continuously through a drag; `onChange` fires once when it
|
|
1083
1083
|
* ends and is the one to write to history.
|
|
1084
1084
|
*
|
|
1085
|
+
* `stops` are attractors: a drag that passes within a few pixels of one lands
|
|
1086
|
+
* on it, and the arrow keys move stop to stop. `step` still quantizes the
|
|
1087
|
+
* values between them.
|
|
1088
|
+
*
|
|
1085
1089
|
* `constraint: 'ordered'` keeps thumbs from crossing each other. Supplying
|
|
1086
1090
|
* `onAddThumb` makes a click on empty track create a thumb, and supplying
|
|
1087
1091
|
* `onRemoveThumb` lets a right-click or a drag off the track remove one —
|
|
@@ -1095,6 +1099,7 @@ type SliderProps<T extends Thumb = Thumb> = {
|
|
|
1095
1099
|
min: number;
|
|
1096
1100
|
max: number;
|
|
1097
1101
|
step?: number;
|
|
1102
|
+
stops?: number[];
|
|
1098
1103
|
constraint?: 'free' | 'ordered';
|
|
1099
1104
|
onAddThumb?: (atValue: number) => T | null;
|
|
1100
1105
|
onRemoveThumb?: (index: number) => boolean;
|
|
@@ -1676,8 +1681,10 @@ interface InsertAdapter<TNode extends {
|
|
|
1676
1681
|
dx: number;
|
|
1677
1682
|
dy: number;
|
|
1678
1683
|
};
|
|
1679
|
-
/** Mutator wired by `insertNode`-using ops (kit-side InsertOp).
|
|
1680
|
-
|
|
1684
|
+
/** Mutator wired by `insertNode`-using ops (kit-side InsertOp). `index`
|
|
1685
|
+
* carries the z-position a delete captured, so undo restores paint order;
|
|
1686
|
+
* see `SceneAdapter.insertNode`. */
|
|
1687
|
+
insertNode(node: TNode, index?: number): void;
|
|
1681
1688
|
/** Mutator wired by `setSelection` ops batched alongside paste. */
|
|
1682
1689
|
setSelection(ids: string[]): void;
|
|
1683
1690
|
/** Optional: see SceneAdapter.applyOps. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { m as ActionBar, r3 as ActionsBar, Ft as Badge, a as Button, o3 as Checkbox, p4 as ComboBox,
|
|
2
|
-
import '../chunk-
|
|
1
|
+
export { m as ActionBar, r3 as ActionsBar, Ft as Badge, a as Button, o3 as Checkbox, p4 as ComboBox, m2 as ComboBoxItem, F as CurveEditor, u as DataGrid, c3 as Dialog, qe as EDGE_PROFILES, n as Field, u3 as Input, s as KeyCap, c2 as KeySequence, o2 as MINUS_SIGN, f2 as NumberField, r2 as OptionsBar, _ as Plot2D, n4 as PointPlotter, r4 as Powerline, u4 as Radio, l as RadioGroup, l2 as RangeSlider, p3 as Select, h as SelectItem, n2 as Sidebar, o4 as SidebarPanel, v as Slider, a3 as Switch, c4 as Tab, s3 as TabList, l3 as TabPanel, o6 as Tabs, r as ToggleBar, o5 as ToolButton, n3 as ToolGroup, _2 as ToolPalette, He as chromaAt, u2 as detectPlatform, p as dlog, r5 as fieldClasses, s2 as formatNumber, d2 as formatShortcut, u5 as formatShortcutParts, o as inferKeycapKind, f as isDebugEnabled, e as keyGlyph, p2 as keySpecFromKey, d as keySpecsFromMods, Ve as oklchToHex, p5 as paintGradientTrack, c as useReorderDragList, a2 as useRovingTabIndex } from '../chunk-C6GJKPUI.js';
|
|
2
|
+
import '../chunk-DJLDIRFN.js';
|
|
3
3
|
//# sourceMappingURL=weasel-ui.js.map
|
|
4
4
|
//# sourceMappingURL=weasel-ui.js.map
|
package/dist/state/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { I as InstrumentSerializers,
|
|
2
|
-
export { C as
|
|
1
|
+
import { S as SerializedTrial, I as InstrumentSerializers, T as TrialRecord, U as UndoStack, a as StorageAdapter } from '../_dts/useTrialState-DYe2vUwN.js';
|
|
2
|
+
export { C as CURRENT_DOCUMENT_VERSION, b as CreateLabStoreOptions, L as LabDocument, c as LabStore, d as LabStoreActions, e as LabStoreContext, f as LabStoreProvider, g as LabStoreState, M as Migration, h as SavedSnapshot, i as TrialIdContext, j as TrialIdProvider, k as TrialStateHandle, l as createLabStore, m as createMemoryAdapter, n as labDocumentKey, o as localStorageAdapter, p as noneAdapter, q as quarantineKey, s as sessionStorageAdapter, u as urlHashAdapter, r as useLabStore, t as useTrialId, v as useTrialState } from '../_dts/useTrialState-DYe2vUwN.js';
|
|
3
3
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
4
4
|
import { ReactNode } from 'react';
|
|
5
5
|
import 'zustand/vanilla';
|
|
@@ -14,19 +14,19 @@ declare function encodeUrlHash(value: string): string;
|
|
|
14
14
|
declare function decodeUrlHash(hash: string): string | null;
|
|
15
15
|
/** A fresh, empty undo history. */
|
|
16
16
|
declare function emptyUndoStack(): UndoStack;
|
|
17
|
-
/** Serialize
|
|
17
|
+
/** Serialize trials for storage, running each instrument's own serializer
|
|
18
18
|
* over its state. Undo history is deliberately dropped — it does not survive
|
|
19
19
|
* a reload. */
|
|
20
|
-
declare function
|
|
21
|
-
/** Rebuild
|
|
20
|
+
declare function serializeTrials(trials: TrialRecord[], serializers: InstrumentSerializers): SerializedTrial[];
|
|
21
|
+
/** Rebuild trials from storage, running each instrument's deserializer
|
|
22
22
|
* over its state and starting each with an empty undo history. Returns an
|
|
23
23
|
* empty list rather than throwing on malformed input. */
|
|
24
|
-
declare function
|
|
24
|
+
declare function deserializeTrials(records: SerializedTrial[], deserializers: InstrumentSerializers): TrialRecord[];
|
|
25
25
|
|
|
26
26
|
/** Props for `<SingletonExperimentProvider>`. */
|
|
27
27
|
interface SingletonExperimentProviderProps<TS, TC> {
|
|
28
|
-
/** Stable id for the synthetic
|
|
29
|
-
*
|
|
28
|
+
/** Stable id for the synthetic trial; also doubles as the
|
|
29
|
+
* TrialIdContext value. */
|
|
30
30
|
id: string;
|
|
31
31
|
initialConfig: TC;
|
|
32
32
|
initialState: TS;
|
|
@@ -35,12 +35,12 @@ interface SingletonExperimentProviderProps<TS, TC> {
|
|
|
35
35
|
children: ReactNode;
|
|
36
36
|
}
|
|
37
37
|
/**
|
|
38
|
-
* One-
|
|
39
|
-
* a `LabStoreProvider` + `
|
|
40
|
-
*
|
|
38
|
+
* One-trial `<Lab>` substitute for single-screen experiments. Mounts
|
|
39
|
+
* a `LabStoreProvider` + `TrialIdProvider` with one synthetic
|
|
40
|
+
* trial, so `useTrialState` works without going through the
|
|
41
41
|
* full `<Lab instruments={...}>` runtime.
|
|
42
42
|
*/
|
|
43
43
|
declare function SingletonExperimentProvider<TS, TC>({ id, initialConfig, initialState, storage, storageKey, children, }: SingletonExperimentProviderProps<TS, TC>): react_jsx_runtime.JSX.Element;
|
|
44
44
|
|
|
45
|
-
export { InstrumentSerializers, SingletonExperimentProvider, StorageAdapter,
|
|
45
|
+
export { InstrumentSerializers, SerializedTrial, SingletonExperimentProvider, StorageAdapter, TrialRecord, UndoStack, decodeUrlHash, deserializeTrials, emptyUndoStack, encodeUrlHash, labStorageKey, serializeTrials };
|
|
46
46
|
export type { SingletonExperimentProviderProps };
|
package/dist/state/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { createLabStore, LabStoreProvider,
|
|
2
|
-
export { LabStoreContext, LabStoreProvider,
|
|
1
|
+
import { createLabStore, LabStoreProvider, TrialIdProvider } from '../chunk-3TYUJR7Z.js';
|
|
2
|
+
export { CURRENT_DOCUMENT_VERSION, LabStoreContext, LabStoreProvider, TrialIdContext, TrialIdProvider, createLabStore, createMemoryAdapter, decodeUrlHash, deserializeTrials, emptyUndoStack, encodeUrlHash, labDocumentKey, labStorageKey, localStorageAdapter, noneAdapter, quarantineKey, serializeTrials, sessionStorageAdapter, urlHashAdapter, useLabStore, useTrialId, useTrialState } from '../chunk-3TYUJR7Z.js';
|
|
3
3
|
import { useRef } from 'react';
|
|
4
4
|
import { jsx } from 'react/jsx-runtime';
|
|
5
5
|
|
|
@@ -15,8 +15,8 @@ function SingletonExperimentProvider({
|
|
|
15
15
|
const storeRef = useRef(null);
|
|
16
16
|
if (storeRef.current === null) {
|
|
17
17
|
const store = createLabStore({ storageKey, storage });
|
|
18
|
-
if (!store.getState().
|
|
19
|
-
store.getState().
|
|
18
|
+
if (!store.getState().trials.some((w) => w.id === id)) {
|
|
19
|
+
store.getState().addTrial({
|
|
20
20
|
id,
|
|
21
21
|
instrumentName: SINGLETON_INSTRUMENT,
|
|
22
22
|
config: initialConfig,
|
|
@@ -26,7 +26,7 @@ function SingletonExperimentProvider({
|
|
|
26
26
|
}
|
|
27
27
|
storeRef.current = store;
|
|
28
28
|
}
|
|
29
|
-
return /* @__PURE__ */ jsx(LabStoreProvider, { store: storeRef.current, children: /* @__PURE__ */ jsx(
|
|
29
|
+
return /* @__PURE__ */ jsx(LabStoreProvider, { store: storeRef.current, children: /* @__PURE__ */ jsx(TrialIdProvider, { trialId: id, children }) });
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
export { SingletonExperimentProvider };
|
package/dist/state/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/state/SingletonExperiment.tsx"],"names":[],"mappings":";;;;;AAKA,IAAM,oBAAA,GAAuB,eAAA;AAoBtB,SAAS,2BAAA,CAAoC;AAAA,EAClD,EAAA;AAAA,EACA,aAAA;AAAA,EACA,YAAA;AAAA,EACA,OAAA;AAAA,EACA,UAAA;AAAA,EACA;AACF,CAAA,EAA6C;AAC3C,EAAA,MAAM,QAAA,GAAW,OAAwB,IAAI,CAAA;AAC7C,EAAA,IAAI,QAAA,CAAS,YAAY,IAAA,EAAM;AAC7B,IAAA,MAAM,KAAA,GAAQ,cAAA,CAAe,EAAE,UAAA,EAAY,SAAS,CAAA;AACpD,IAAA,IAAI,CAAC,KAAA,CAAM,QAAA,EAAS,CAAE,
|
|
1
|
+
{"version":3,"sources":["../../src/state/SingletonExperiment.tsx"],"names":[],"mappings":";;;;;AAKA,IAAM,oBAAA,GAAuB,eAAA;AAoBtB,SAAS,2BAAA,CAAoC;AAAA,EAClD,EAAA;AAAA,EACA,aAAA;AAAA,EACA,YAAA;AAAA,EACA,OAAA;AAAA,EACA,UAAA;AAAA,EACA;AACF,CAAA,EAA6C;AAC3C,EAAA,MAAM,QAAA,GAAW,OAAwB,IAAI,CAAA;AAC7C,EAAA,IAAI,QAAA,CAAS,YAAY,IAAA,EAAM;AAC7B,IAAA,MAAM,KAAA,GAAQ,cAAA,CAAe,EAAE,UAAA,EAAY,SAAS,CAAA;AACpD,IAAA,IAAI,CAAC,KAAA,CAAM,QAAA,EAAS,CAAE,MAAA,CAAO,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,EAAA,KAAO,EAAE,CAAA,EAAG;AACrD,MAAA,KAAA,CAAM,QAAA,GAAW,QAAA,CAAS;AAAA,QACxB,EAAA;AAAA,QACA,cAAA,EAAgB,oBAAA;AAAA,QAChB,MAAA,EAAQ,aAAA;AAAA,QACR,KAAA,EAAO,YAAA;AAAA,QACP,IAAA,EAAM,EAAE,IAAA,EAAM,CAAA,EAAG,GAAA,EAAK,EAAE,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAE;AAAE,OACtC,CAAA;AAAA,IACH;AACA,IAAA,QAAA,CAAS,OAAA,GAAU,KAAA;AAAA,EACrB;AACA,EAAA,uBACE,GAAA,CAAC,gBAAA,EAAA,EAAiB,KAAA,EAAO,QAAA,CAAS,OAAA,EAChC,8BAAC,eAAA,EAAA,EAAgB,OAAA,EAAS,EAAA,EAAK,QAAA,EAAS,CAAA,EAC1C,CAAA;AAEJ","file":"index.js","sourcesContent":["import { type ReactNode, useRef } from 'react';\nimport { LabStoreProvider, TrialIdProvider } from './context';\nimport { createLabStore, type LabStore } from './store';\nimport type { StorageAdapter } from './types';\n\nconst SINGLETON_INSTRUMENT = '__singleton__';\n\n/** Props for `<SingletonExperimentProvider>`. */\nexport interface SingletonExperimentProviderProps<TS, TC> {\n /** Stable id for the synthetic trial; also doubles as the\n * TrialIdContext value. */\n id: string;\n initialConfig: TC;\n initialState: TS;\n storage: StorageAdapter;\n storageKey: string;\n children: ReactNode;\n}\n\n/**\n * One-trial `<Lab>` substitute for single-screen experiments. Mounts\n * a `LabStoreProvider` + `TrialIdProvider` with one synthetic\n * trial, so `useTrialState` works without going through the\n * full `<Lab instruments={...}>` runtime.\n */\nexport function SingletonExperimentProvider<TS, TC>({\n id,\n initialConfig,\n initialState,\n storage,\n storageKey,\n children,\n}: SingletonExperimentProviderProps<TS, TC>) {\n const storeRef = useRef<LabStore | null>(null);\n if (storeRef.current === null) {\n const store = createLabStore({ storageKey, storage });\n if (!store.getState().trials.some((w) => w.id === id)) {\n store.getState().addTrial({\n id,\n instrumentName: SINGLETON_INSTRUMENT,\n config: initialConfig,\n state: initialState,\n view: { zoom: 1, pan: { x: 0, y: 0 } },\n });\n }\n storeRef.current = store;\n }\n return (\n <LabStoreProvider store={storeRef.current}>\n <TrialIdProvider trialId={id}>{children}</TrialIdProvider>\n </LabStoreProvider>\n );\n}\n"]}
|
package/dist/styles.css
CHANGED
|
@@ -356,15 +356,15 @@
|
|
|
356
356
|
letter-spacing: 0.01em;
|
|
357
357
|
line-height: 1.2;
|
|
358
358
|
}
|
|
359
|
-
.lk-workspace
|
|
359
|
+
.lk-workspace {
|
|
360
360
|
min-height: 0;
|
|
361
361
|
}
|
|
362
|
-
.lk-
|
|
362
|
+
.lk-trial-tile {
|
|
363
363
|
display: flex;
|
|
364
364
|
flex-direction: column;
|
|
365
365
|
height: 100%;
|
|
366
366
|
}
|
|
367
|
-
.lk-
|
|
367
|
+
.lk-trial-tile__grip {
|
|
368
368
|
flex: 0 0 auto;
|
|
369
369
|
display: flex;
|
|
370
370
|
align-items: center;
|
|
@@ -374,17 +374,17 @@
|
|
|
374
374
|
border-radius: var(--wzl-radius-sm) var(--wzl-radius-sm) 0 0;
|
|
375
375
|
background: var(--wzl-surface-sunken);
|
|
376
376
|
}
|
|
377
|
-
.lk-
|
|
377
|
+
.lk-trial-tile__grip:active {
|
|
378
378
|
cursor: grabbing;
|
|
379
379
|
}
|
|
380
|
-
.lk-
|
|
380
|
+
.lk-trial-tile__grip-dots {
|
|
381
381
|
width: 28px;
|
|
382
382
|
height: 3px;
|
|
383
383
|
border-radius: 999px;
|
|
384
384
|
background: var(--wzl-fg-muted);
|
|
385
385
|
opacity: 0.5;
|
|
386
386
|
}
|
|
387
|
-
.lk-
|
|
387
|
+
.lk-trial-tile__body {
|
|
388
388
|
flex: 1;
|
|
389
389
|
min-height: 0;
|
|
390
390
|
}
|
|
@@ -567,10 +567,18 @@
|
|
|
567
567
|
.lk-scale-indicator-label {
|
|
568
568
|
white-space: nowrap;
|
|
569
569
|
}
|
|
570
|
+
html:has(.lk-lab),
|
|
571
|
+
body:has(.lk-lab) {
|
|
572
|
+
margin: 0;
|
|
573
|
+
height: 100%;
|
|
574
|
+
}
|
|
575
|
+
body:has(.lk-lab) > :has(> .lk-lab) {
|
|
576
|
+
height: 100%;
|
|
577
|
+
}
|
|
570
578
|
.lk-lab {
|
|
571
579
|
height: 100%;
|
|
572
580
|
}
|
|
573
|
-
.lk-
|
|
581
|
+
.lk-trial {
|
|
574
582
|
display: flex;
|
|
575
583
|
flex-direction: column;
|
|
576
584
|
height: 100%;
|
|
@@ -579,27 +587,27 @@
|
|
|
579
587
|
border-radius: var(--wzl-radius-md);
|
|
580
588
|
overflow: hidden;
|
|
581
589
|
}
|
|
582
|
-
.lk-
|
|
590
|
+
.lk-trial__body {
|
|
583
591
|
display: flex;
|
|
584
592
|
flex: 1;
|
|
585
593
|
overflow: hidden;
|
|
586
594
|
}
|
|
587
|
-
.lk-
|
|
595
|
+
.lk-trial__sidebar {
|
|
588
596
|
flex-shrink: 0;
|
|
589
597
|
}
|
|
590
|
-
.lk-
|
|
598
|
+
.lk-trial__content {
|
|
591
599
|
flex: 1;
|
|
592
600
|
overflow: auto;
|
|
593
601
|
}
|
|
594
|
-
.lk-
|
|
602
|
+
.lk-trial__toolbar {
|
|
595
603
|
flex-shrink: 0;
|
|
596
604
|
border-bottom: 1px solid var(--wzl-line-subtle);
|
|
597
605
|
}
|
|
598
|
-
.lk-
|
|
606
|
+
.lk-trial__status {
|
|
599
607
|
flex-shrink: 0;
|
|
600
608
|
border-top: 1px solid var(--wzl-line-subtle);
|
|
601
609
|
}
|
|
602
|
-
.lk-
|
|
610
|
+
.lk-trial--unknown {
|
|
603
611
|
display: flex;
|
|
604
612
|
align-items: center;
|
|
605
613
|
justify-content: center;
|
package/dist/ui/layers/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { LayerStack } from '../../chunk-
|
|
2
|
-
import '../../chunk-
|
|
3
|
-
import '../../chunk-
|
|
1
|
+
export { LayerStack } from '../../chunk-NRKWVTVT.js';
|
|
2
|
+
import '../../chunk-C6GJKPUI.js';
|
|
3
|
+
import '../../chunk-DJLDIRFN.js';
|
|
4
4
|
//# sourceMappingURL=index.js.map
|
|
5
5
|
//# sourceMappingURL=index.js.map
|
package/dist/undo/index.d.ts
CHANGED
package/package.json
CHANGED
package/src/canvas/AGENTS.md
CHANGED
|
@@ -32,12 +32,12 @@ interface CanvasStackProps {
|
|
|
32
32
|
|
|
33
33
|
1. The container measures itself with `ResizeObserver` and reports `{width, height, dpr}`.
|
|
34
34
|
2. `useLayerScheduler` watches `layers`, `view`, and `size`, and on any change schedules a single `requestAnimationFrame` redraw.
|
|
35
|
-
3. Each layer's `render(ctx, view)` is called with a fresh transform: the canvas is sized to `width * dpr × height * dpr` so coordinates are in CSS pixels.
|
|
35
|
+
3. Each layer's `render(ctx, view)` is called with a fresh transform: the canvas is sized to `width * dpr × height * dpr` so coordinates are in CSS pixels. A `CanvasLayerDescriptor` gets the whole `view` and places its own geometry; an instrument's `CanvasLayer.draw` is called with the camera already applied — see below.
|
|
36
36
|
4. Layers with `visible: false` are skipped and their canvas is `display: none` (DOM is preserved to avoid remounts).
|
|
37
37
|
|
|
38
38
|
## Adding a new layer type
|
|
39
39
|
|
|
40
|
-
A layer is defined by the instrument's `canvas.layers[]` (type `CanvasLayer`), then translated to a `CanvasLayerDescriptor` inside `
|
|
40
|
+
A layer is defined by the instrument's `canvas.layers[]` (type `CanvasLayer`), then translated to a `CanvasLayerDescriptor` inside `Trial.tsx` before being passed to `<CanvasStack>`. To add a layer, push it into `instrument.canvas.layers`:
|
|
41
41
|
|
|
42
42
|
```ts
|
|
43
43
|
canvas: {
|
|
@@ -50,6 +50,10 @@ canvas: {
|
|
|
50
50
|
}
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
+
`draw` receives the context with the camera already applied, so its coordinates are world coordinates — `Trial.tsx` translates by `view.pan` and scales by `view.zoom` before calling it. `zoom` is still passed so a layer can keep line widths and handle sizes from growing: divide by it (`ctx.lineWidth = 1 / zoom`).
|
|
54
|
+
|
|
55
|
+
That is the difference between the two layer types. The lower-level `CanvasLayerDescriptor.render(ctx, view)` gets the raw view and an untransformed context, which is what screen-space chrome wants.
|
|
56
|
+
|
|
53
57
|
Layer order in the array = paint order (first drawn = bottom).
|
|
54
58
|
|
|
55
59
|
## Overlay (children)
|
|
@@ -34,19 +34,16 @@ const originalGetBoundingClientRect = HTMLElement.prototype.getBoundingClientRec
|
|
|
34
34
|
function StateProbe({ onState }: { onState: (state: DropState) => void }) {
|
|
35
35
|
const ctx = useContext(LabStoreContext);
|
|
36
36
|
const ws = useStore(
|
|
37
|
-
ctx?.store ?? ({ getState: () => ({
|
|
38
|
-
(s) => (s as {
|
|
37
|
+
ctx?.store ?? ({ getState: () => ({ trials: [] }) } as never),
|
|
38
|
+
(s) => (s as { trials: { state: unknown }[] }).trials[0],
|
|
39
39
|
);
|
|
40
40
|
if (ws) onState(ws.state as DropState);
|
|
41
41
|
return null;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
// biome-ignore lint/suspicious/noExplicitAny: cross-generic instrument array
|
|
45
|
-
const instruments: any[] = [testInstrument];
|
|
46
|
-
|
|
47
44
|
function renderLab(probe?: (state: DropState) => void) {
|
|
48
45
|
return render(
|
|
49
|
-
<Lab instruments={
|
|
46
|
+
<Lab instruments={[testInstrument]} defaultInstrument="TestDrop">
|
|
50
47
|
{probe ? <StateProbe onState={probe} /> : null}
|
|
51
48
|
</Lab>,
|
|
52
49
|
);
|
|
@@ -54,7 +51,7 @@ function renderLab(probe?: (state: DropState) => void) {
|
|
|
54
51
|
|
|
55
52
|
beforeEach(() => {
|
|
56
53
|
HTMLElement.prototype.getBoundingClientRect = function () {
|
|
57
|
-
if (this.classList.contains('lk-
|
|
54
|
+
if (this.classList.contains('lk-trial__canvas-host')) {
|
|
58
55
|
return {
|
|
59
56
|
x: 100,
|
|
60
57
|
y: 100,
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import * as labkit from './index';
|
|
5
|
+
|
|
6
|
+
/** The barrels the public surface is assembled from. Type-only exports are
|
|
7
|
+
* erased at runtime, so the names are read out of the source rather than off
|
|
8
|
+
* the module object. */
|
|
9
|
+
const BARRELS = ['./index.ts', './lab/index.ts', './trial/index.ts', './state/index.ts'];
|
|
10
|
+
|
|
11
|
+
function exportedNames(): string[] {
|
|
12
|
+
const names: string[] = [];
|
|
13
|
+
for (const barrel of BARRELS) {
|
|
14
|
+
const src = readFileSync(fileURLToPath(new URL(barrel, import.meta.url)), 'utf8');
|
|
15
|
+
for (const [, body] of src.matchAll(/export\s+(?:type\s+)?\{([^}]*)\}/g)) {
|
|
16
|
+
for (const clause of body.split(',')) {
|
|
17
|
+
const name = clause
|
|
18
|
+
.trim()
|
|
19
|
+
.split(/\s+as\s+/)
|
|
20
|
+
.pop()
|
|
21
|
+
?.replace(/^type\s+/, '')
|
|
22
|
+
.trim();
|
|
23
|
+
if (name) names.push(name);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return names;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('public export surface', () => {
|
|
31
|
+
it('names the grid, not a tile, when it says Workspace', () => {
|
|
32
|
+
const offenders = exportedNames().filter(
|
|
33
|
+
(n) => n.startsWith('Workspace') && n !== 'Workspace' && n !== 'WorkspaceProps',
|
|
34
|
+
);
|
|
35
|
+
expect(offenders).toEqual([]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('exports no Experiment symbol that means anything per-tile', () => {
|
|
39
|
+
const offenders = exportedNames().filter(
|
|
40
|
+
(n) => n.includes('Experiment') && !n.startsWith('SingletonExperiment'),
|
|
41
|
+
);
|
|
42
|
+
expect(offenders).toEqual([]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('exports the tile as Trial and the area it sits in as Workspace', () => {
|
|
46
|
+
expect(labkit).toHaveProperty('Trial');
|
|
47
|
+
expect(labkit).toHaveProperty('TrialChrome');
|
|
48
|
+
expect(labkit).toHaveProperty('Workspace');
|
|
49
|
+
expect(labkit).toHaveProperty('useTrialState');
|
|
50
|
+
expect(labkit).toHaveProperty('useTrialId');
|
|
51
|
+
});
|
|
52
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -23,6 +23,7 @@ export type {
|
|
|
23
23
|
DragFeedback,
|
|
24
24
|
HitResult,
|
|
25
25
|
Instrument,
|
|
26
|
+
InstrumentList,
|
|
26
27
|
LayerCapability,
|
|
27
28
|
LayerDescriptor,
|
|
28
29
|
PaletteItem,
|
|
@@ -48,25 +49,28 @@ export {
|
|
|
48
49
|
export {
|
|
49
50
|
LabStoreContext,
|
|
50
51
|
LabStoreProvider,
|
|
52
|
+
TrialIdContext,
|
|
53
|
+
TrialIdProvider,
|
|
51
54
|
useLabStore,
|
|
52
|
-
|
|
53
|
-
WorkspaceIdContext,
|
|
54
|
-
WorkspaceIdProvider,
|
|
55
|
+
useTrialId,
|
|
55
56
|
} from './state/context';
|
|
57
|
+
export { CURRENT_DOCUMENT_VERSION, labDocumentKey, quarantineKey } from './state/document';
|
|
56
58
|
export type {
|
|
57
59
|
CreateLabStoreOptions,
|
|
58
|
-
|
|
60
|
+
LabDocument,
|
|
59
61
|
LabMode,
|
|
60
62
|
LabStoreState,
|
|
61
63
|
SavedSnapshot,
|
|
64
|
+
SerializedTrial,
|
|
62
65
|
StorageAdapter,
|
|
66
|
+
TrialRecord,
|
|
67
|
+
TrialStateHandle,
|
|
63
68
|
UndoStack,
|
|
64
|
-
WorkspaceRecord,
|
|
65
69
|
} from './state/types';
|
|
66
|
-
export {
|
|
70
|
+
export { useTrialState } from './state/useTrialState';
|
|
67
71
|
export { interstellarTheme } from './theme/interstellar';
|
|
72
|
+
export * from './trial';
|
|
68
73
|
export * from './ui/layers';
|
|
69
74
|
export * from './ui/properties';
|
|
70
75
|
export type { EventBus, EventListener } from './undo';
|
|
71
76
|
export { clearUndo, createEventBus, emptyStack, pushSnapshot, redo, undo } from './undo';
|
|
72
|
-
export * from './workspace';
|
|
@@ -85,7 +85,7 @@ describe('SineWave instrument smoke test', () => {
|
|
|
85
85
|
config: SineWaveInstrument.defaultConfig(),
|
|
86
86
|
setState: vi.fn(),
|
|
87
87
|
setConfig: vi.fn(),
|
|
88
|
-
|
|
88
|
+
trial: { id: 'w', zoom: 1, setZoom: vi.fn() },
|
|
89
89
|
emit: vi.fn(),
|
|
90
90
|
};
|
|
91
91
|
const { container } = render(SineWaveInstrument.render(ctx) as ReactElement);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Instrument } from './types';
|
|
2
2
|
|
|
3
|
-
/** Which optional capabilities an instrument declared — what the
|
|
3
|
+
/** Which optional capabilities an instrument declared — what the trial
|
|
4
4
|
* consults to decide which chrome to show. */
|
|
5
5
|
export interface CapabilityFlags {
|
|
6
6
|
hasCanvas: boolean;
|
package/src/instrument/types.ts
CHANGED
|
@@ -2,13 +2,13 @@ import type { ReactNode } from 'react';
|
|
|
2
2
|
import type { ConfigField } from '../controls/types';
|
|
3
3
|
|
|
4
4
|
/** What an instrument's `render` is handed: its state and config, the setters
|
|
5
|
-
* for both, the
|
|
5
|
+
* for both, the trial it is mounted in, and a way to emit named events. */
|
|
6
6
|
export interface RenderContext<TS = unknown, TC = unknown> {
|
|
7
7
|
state: TS;
|
|
8
8
|
config: TC;
|
|
9
9
|
setState: (next: TS | ((prev: TS) => TS)) => void;
|
|
10
10
|
setConfig: (key: keyof TC, value: unknown) => void;
|
|
11
|
-
|
|
11
|
+
trial: {
|
|
12
12
|
id: string;
|
|
13
13
|
zoom: number;
|
|
14
14
|
setZoom: (z: number) => void;
|
|
@@ -16,7 +16,11 @@ export interface RenderContext<TS = unknown, TC = unknown> {
|
|
|
16
16
|
emit: (event: string) => void;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
/** One 2D canvas layer of an instrument, drawn in declaration order.
|
|
19
|
+
/** One 2D canvas layer of an instrument, drawn in declaration order.
|
|
20
|
+
*
|
|
21
|
+
* `draw` is called with the camera already applied, so it works in world
|
|
22
|
+
* coordinates. `zoom` is passed for the things that must not scale with it —
|
|
23
|
+
* set `ctx.lineWidth = 1 / zoom` to keep a hairline hairline. */
|
|
20
24
|
export interface CanvasLayer<TS = unknown, TC = unknown> {
|
|
21
25
|
id: string;
|
|
22
26
|
draw: (ctx: CanvasRenderingContext2D, args: { state: TS; config: TC; zoom: number }) => void;
|
|
@@ -29,7 +33,7 @@ export interface CanvasCapability<TS = unknown, TC = unknown> {
|
|
|
29
33
|
initialView?: { zoom: number; pan: { x: number; y: number } };
|
|
30
34
|
}
|
|
31
35
|
|
|
32
|
-
/** Declares which of an instrument's layers the
|
|
36
|
+
/** Declares which of an instrument's layers the trial should offer
|
|
33
37
|
* show/hide controls for. */
|
|
34
38
|
export interface LayerCapability {
|
|
35
39
|
ids: string[];
|
|
@@ -59,7 +63,7 @@ export type SystemEvent = string;
|
|
|
59
63
|
export type Point = { x: number; y: number };
|
|
60
64
|
/** What a hit-test found, and where. */
|
|
61
65
|
export type HitResult = { hit: boolean; layerId?: string; pointId?: string };
|
|
62
|
-
/** A
|
|
66
|
+
/** A trial's camera. */
|
|
63
67
|
export type ViewTransform = { zoom: number; pan: Point };
|
|
64
68
|
/** A layer as the layer list shows it. `alwaysOn` layers cannot be hidden. */
|
|
65
69
|
export type LayerDescriptor = { id: string; label: string; alwaysOn?: boolean };
|
|
@@ -76,13 +80,15 @@ export type DragFeedback = { ok: boolean; reason?: string };
|
|
|
76
80
|
* and `state`, what the experiment is currently doing — and renders from both.
|
|
77
81
|
* The optional capability fields declare what else it wants from the runtime:
|
|
78
82
|
* a canvas, a layer list, palette drag-and-drop, undo. Declaring a capability
|
|
79
|
-
* is what makes the
|
|
83
|
+
* is what makes the trial provide the corresponding chrome.
|
|
80
84
|
*/
|
|
81
85
|
export interface Instrument<TS = unknown, TC = unknown> {
|
|
82
86
|
name: string;
|
|
83
87
|
defaultConfig: () => TC;
|
|
84
88
|
initialState: (config: TC) => TS;
|
|
85
89
|
configSchema?: () => ConfigField[];
|
|
90
|
+
/** The instrument's DOM. With `canvas`, this renders as an overlay above the
|
|
91
|
+
* layers rather than instead of them; return `null` for canvas only. */
|
|
86
92
|
render: (ctx: RenderContext<TS, TC>) => ReactNode;
|
|
87
93
|
onConfigChange?: (config: TC, prev: TC, state: TS) => TS;
|
|
88
94
|
serialize?: (state: TS) => unknown;
|
|
@@ -92,3 +98,9 @@ export interface Instrument<TS = unknown, TC = unknown> {
|
|
|
92
98
|
dragDrop?: DragDropCapability<TS, TC>;
|
|
93
99
|
undo?: UndoCapability;
|
|
94
100
|
}
|
|
101
|
+
|
|
102
|
+
/** Instruments as a lab receives them. `any` rather than `unknown` because
|
|
103
|
+
* parameter contravariance keeps a `defineInstrument<TS, TC>` result out of
|
|
104
|
+
* an `Instrument<unknown, unknown>[]`; it is contained to this alias. */
|
|
105
|
+
// biome-ignore lint/suspicious/noExplicitAny: see above
|
|
106
|
+
export type InstrumentList = readonly Instrument<any, any>[];
|
package/src/lab/Lab.less
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
// `height: 100%` resolves against the containing block, so the lab needs an
|
|
2
|
+
// unbroken chain of sized ancestors — and a host that supplies the height but
|
|
3
|
+
// not the margin reset gets a page 16px taller than the viewport, which reads
|
|
4
|
+
// as a stuck canvas rather than as overflow. Scoped with `:has` so a page that
|
|
5
|
+
// mounts no lab is untouched, and stopping at the lab's own parent so an
|
|
6
|
+
// embedded lab cannot resize its host's layout.
|
|
7
|
+
html:has(.lk-lab),
|
|
8
|
+
body:has(.lk-lab) {
|
|
9
|
+
margin: 0;
|
|
10
|
+
height: 100%;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
body:has(.lk-lab) > :has(> .lk-lab) {
|
|
14
|
+
height: 100%;
|
|
15
|
+
}
|
|
16
|
+
|
|
1
17
|
.lk-lab {
|
|
2
18
|
height: 100%;
|
|
3
19
|
}
|
package/src/lab/Lab.stories.tsx
CHANGED
|
@@ -31,21 +31,21 @@ export const Default: Story = {
|
|
|
31
31
|
},
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
-
function
|
|
34
|
+
function AddSecondTrial() {
|
|
35
35
|
const ctx = useLabContext();
|
|
36
36
|
// biome-ignore lint/correctness/useExhaustiveDependencies: run once on mount
|
|
37
37
|
useEffect(() => {
|
|
38
|
-
if (ctx.
|
|
38
|
+
if (ctx.trials.length < 2) ctx.addTrial('Stub');
|
|
39
39
|
}, []);
|
|
40
40
|
return null;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
export const
|
|
43
|
+
export const TwoTrials: Story = {
|
|
44
44
|
args: {
|
|
45
45
|
instruments: [StubInstrument],
|
|
46
46
|
defaultInstrument: 'Stub',
|
|
47
|
-
title: 'Two
|
|
47
|
+
title: 'Two Trials',
|
|
48
48
|
storage: null,
|
|
49
|
-
children: <
|
|
49
|
+
children: <AddSecondTrial />,
|
|
50
50
|
},
|
|
51
51
|
};
|