@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/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
React widgets for building self-contained interactive **lab** pages — pages with sliders, controls, and canvas-based experimentation.
|
|
4
4
|
|
|
5
|
-
This is the v0.x of the library. The Lab/
|
|
5
|
+
This is the v0.x of the library. The Lab/Trial/Instrument runtime arrives in later plans; v0.0.1 ships presentational primitives.
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
@@ -28,16 +28,16 @@ Then point your app at the local clone:
|
|
|
28
28
|
## Usage
|
|
29
29
|
|
|
30
30
|
```tsx
|
|
31
|
-
import { LabShell, Toolbar,
|
|
31
|
+
import { LabShell, Toolbar, Workspace, FpsMeter } from '@weasel-js/labkit';
|
|
32
32
|
import '@weasel-js/labkit/styles.css';
|
|
33
33
|
|
|
34
34
|
function MyLab() {
|
|
35
35
|
return (
|
|
36
36
|
<LabShell title="My Lab" header={<button>+ Add</button>}>
|
|
37
|
-
<
|
|
38
|
-
<div>
|
|
39
|
-
<div>
|
|
40
|
-
</
|
|
37
|
+
<Workspace>
|
|
38
|
+
<div>Trial 1</div>
|
|
39
|
+
<div>Trial 2</div>
|
|
40
|
+
</Workspace>
|
|
41
41
|
</LabShell>
|
|
42
42
|
);
|
|
43
43
|
}
|
|
@@ -2,20 +2,24 @@ import { ReactNode } from 'react';
|
|
|
2
2
|
import { C as ConfigField } from './types-x92Kfeme.js';
|
|
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
|
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;
|
|
15
15
|
};
|
|
16
16
|
emit: (event: string) => void;
|
|
17
17
|
}
|
|
18
|
-
/** One 2D canvas layer of an instrument, drawn in declaration order.
|
|
18
|
+
/** One 2D canvas layer of an instrument, drawn in declaration order.
|
|
19
|
+
*
|
|
20
|
+
* `draw` is called with the camera already applied, so it works in world
|
|
21
|
+
* coordinates. `zoom` is passed for the things that must not scale with it —
|
|
22
|
+
* set `ctx.lineWidth = 1 / zoom` to keep a hairline hairline. */
|
|
19
23
|
interface CanvasLayer<TS = unknown, TC = unknown> {
|
|
20
24
|
id: string;
|
|
21
25
|
draw: (ctx: CanvasRenderingContext2D, args: {
|
|
@@ -36,7 +40,7 @@ interface CanvasCapability<TS = unknown, TC = unknown> {
|
|
|
36
40
|
};
|
|
37
41
|
};
|
|
38
42
|
}
|
|
39
|
-
/** Declares which of an instrument's layers the
|
|
43
|
+
/** Declares which of an instrument's layers the trial should offer
|
|
40
44
|
* show/hide controls for. */
|
|
41
45
|
interface LayerCapability {
|
|
42
46
|
ids: string[];
|
|
@@ -72,7 +76,7 @@ type HitResult = {
|
|
|
72
76
|
layerId?: string;
|
|
73
77
|
pointId?: string;
|
|
74
78
|
};
|
|
75
|
-
/** A
|
|
79
|
+
/** A trial's camera. */
|
|
76
80
|
type ViewTransform = {
|
|
77
81
|
zoom: number;
|
|
78
82
|
pan: Point;
|
|
@@ -102,13 +106,15 @@ type DragFeedback = {
|
|
|
102
106
|
* and `state`, what the experiment is currently doing — and renders from both.
|
|
103
107
|
* The optional capability fields declare what else it wants from the runtime:
|
|
104
108
|
* a canvas, a layer list, palette drag-and-drop, undo. Declaring a capability
|
|
105
|
-
* is what makes the
|
|
109
|
+
* is what makes the trial provide the corresponding chrome.
|
|
106
110
|
*/
|
|
107
111
|
interface Instrument<TS = unknown, TC = unknown> {
|
|
108
112
|
name: string;
|
|
109
113
|
defaultConfig: () => TC;
|
|
110
114
|
initialState: (config: TC) => TS;
|
|
111
115
|
configSchema?: () => ConfigField[];
|
|
116
|
+
/** The instrument's DOM. With `canvas`, this renders as an overlay above the
|
|
117
|
+
* layers rather than instead of them; return `null` for canvas only. */
|
|
112
118
|
render: (ctx: RenderContext<TS, TC>) => ReactNode;
|
|
113
119
|
onConfigChange?: (config: TC, prev: TC, state: TS) => TS;
|
|
114
120
|
serialize?: (state: TS) => unknown;
|
|
@@ -118,5 +124,9 @@ interface Instrument<TS = unknown, TC = unknown> {
|
|
|
118
124
|
dragDrop?: DragDropCapability<TS, TC>;
|
|
119
125
|
undo?: UndoCapability;
|
|
120
126
|
}
|
|
127
|
+
/** Instruments as a lab receives them. `any` rather than `unknown` because
|
|
128
|
+
* parameter contravariance keeps a `defineInstrument<TS, TC>` result out of
|
|
129
|
+
* an `Instrument<unknown, unknown>[]`; it is contained to this alias. */
|
|
130
|
+
type InstrumentList = readonly Instrument<any, any>[];
|
|
121
131
|
|
|
122
|
-
export type { CanvasCapability as C, DragDropCapability as D, HitResult as H, Instrument as I, LayerCapability as L, PaletteItem as P, RenderContext as R, SystemEvent as S, UndoCapability as U, ViewTransform as V,
|
|
132
|
+
export type { CanvasCapability as C, DragDropCapability as D, HitResult as H, Instrument as I, LayerCapability as L, PaletteItem as P, RenderContext as R, SystemEvent as S, UndoCapability as U, ViewTransform as V, InstrumentList as a, CanvasLayer as b, DragFeedback as c, LayerDescriptor as d, Point as e };
|
|
@@ -2,15 +2,15 @@ import * as react from 'react';
|
|
|
2
2
|
import { ReactNode, ReactElement } from 'react';
|
|
3
3
|
import { StoreApi } from 'zustand/vanilla';
|
|
4
4
|
|
|
5
|
-
/** A
|
|
5
|
+
/** A trial's undo history, as snapshots of its state either side of the
|
|
6
6
|
* present. */
|
|
7
7
|
interface UndoStack {
|
|
8
8
|
past: unknown[];
|
|
9
9
|
future: unknown[];
|
|
10
10
|
}
|
|
11
|
-
/** One
|
|
11
|
+
/** One trial as the store holds it: which instrument it runs, that
|
|
12
12
|
* instrument's config and state, the camera, and the undo history. */
|
|
13
|
-
interface
|
|
13
|
+
interface TrialRecord<TS = unknown, TC = unknown> {
|
|
14
14
|
id: string;
|
|
15
15
|
instrumentName: string;
|
|
16
16
|
config: TC;
|
|
@@ -24,12 +24,12 @@ interface WorkspaceRecord<TS = unknown, TC = unknown> {
|
|
|
24
24
|
};
|
|
25
25
|
undoStack: UndoStack;
|
|
26
26
|
}
|
|
27
|
-
/** A named, saved copy of a
|
|
28
|
-
*
|
|
27
|
+
/** A named, saved copy of a trial's config and state, restorable into any
|
|
28
|
+
* trial running the same instrument. */
|
|
29
29
|
interface SavedSnapshot {
|
|
30
30
|
id: string;
|
|
31
31
|
name: string;
|
|
32
|
-
|
|
32
|
+
trialId: string;
|
|
33
33
|
instrumentName: string;
|
|
34
34
|
config: unknown;
|
|
35
35
|
state: unknown;
|
|
@@ -37,14 +37,14 @@ interface SavedSnapshot {
|
|
|
37
37
|
}
|
|
38
38
|
/** `auto` follows the OS; the other two are an explicit choice. */
|
|
39
39
|
type LabMode = 'auto' | 'light' | 'dark';
|
|
40
|
-
/** Everything a lab persists: its
|
|
40
|
+
/** Everything a lab persists: its trials, its saved snapshots, and the
|
|
41
41
|
* chosen color mode. */
|
|
42
42
|
interface LabStoreState {
|
|
43
|
-
|
|
43
|
+
trials: TrialRecord[];
|
|
44
44
|
savedSnapshots: SavedSnapshot[];
|
|
45
45
|
mode: LabMode;
|
|
46
|
-
/** Per-
|
|
47
|
-
* shape belongs to whatever lays the
|
|
46
|
+
/** Per-trial tile extents, keyed by trial id. Opaque here — the
|
|
47
|
+
* shape belongs to whatever lays the trials out. */
|
|
48
48
|
layout: Record<string, unknown>;
|
|
49
49
|
}
|
|
50
50
|
/** Where a lab persists itself. Implementations are keyed string storage and
|
|
@@ -55,9 +55,9 @@ interface StorageAdapter {
|
|
|
55
55
|
write(key: string, value: string): void;
|
|
56
56
|
delete?(key: string): void;
|
|
57
57
|
}
|
|
58
|
-
/** What `
|
|
58
|
+
/** What `useTrialState` hands an instrument: its state and config, with
|
|
59
59
|
* a setter for each. */
|
|
60
|
-
interface
|
|
60
|
+
interface TrialStateHandle<TS, TC> {
|
|
61
61
|
state: TS;
|
|
62
62
|
setState: (next: TS | ((prev: TS) => TS)) => void;
|
|
63
63
|
config: TC;
|
|
@@ -76,6 +76,20 @@ type InstrumentSerializers = Record<string, {
|
|
|
76
76
|
serialize?: (state: unknown) => unknown;
|
|
77
77
|
deserialize?: (data: unknown) => unknown;
|
|
78
78
|
} | undefined>;
|
|
79
|
+
/** A trial as it is persisted: everything but the undo history, which is
|
|
80
|
+
* session-only. */
|
|
81
|
+
type SerializedTrial = Omit<TrialRecord, 'undoStack'>;
|
|
82
|
+
/** Everything a lab persists, under one key, at a known version. */
|
|
83
|
+
interface LabDocument {
|
|
84
|
+
version: number;
|
|
85
|
+
trials: SerializedTrial[];
|
|
86
|
+
saves: SavedSnapshot[];
|
|
87
|
+
layout: Record<string, unknown>;
|
|
88
|
+
mode: LabMode;
|
|
89
|
+
}
|
|
90
|
+
/** Migrates a document one version forward. Index `i` in the chain takes a
|
|
91
|
+
* version-`i` document to version `i + 1`. */
|
|
92
|
+
type Migration = (doc: Record<string, unknown>) => Record<string, unknown>;
|
|
79
93
|
|
|
80
94
|
/** Persist to `localStorage` — state survives a reload and a new tab. Falls
|
|
81
95
|
* back to no-ops when storage is unavailable. */
|
|
@@ -91,20 +105,20 @@ declare function createMemoryAdapter(): StorageAdapter;
|
|
|
91
105
|
/** Persists nothing and reads back nothing. */
|
|
92
106
|
declare const noneAdapter: StorageAdapter;
|
|
93
107
|
|
|
94
|
-
/** Every mutation a lab store supports: managing
|
|
108
|
+
/** Every mutation a lab store supports: managing trials, saving and
|
|
95
109
|
* restoring snapshots, and setting the color mode. */
|
|
96
110
|
interface LabStoreActions {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
saveSnapshot: (
|
|
105
|
-
loadSnapshot: (snapshotId: string,
|
|
111
|
+
addTrial: (record: Omit<TrialRecord, 'undoStack'>) => void;
|
|
112
|
+
removeTrial: (id: string) => void;
|
|
113
|
+
updateTrialState: <TS>(id: string, next: TS | ((prev: TS) => TS)) => void;
|
|
114
|
+
updateTrialConfig: <TC>(id: string, key: keyof TC, value: TC[keyof TC]) => void;
|
|
115
|
+
updateTrialView: (id: string, view: TrialRecord['view']) => void;
|
|
116
|
+
updateTrialUndoStack: (id: string, next: TrialRecord['undoStack'] | ((prev: TrialRecord['undoStack']) => TrialRecord['undoStack'])) => void;
|
|
117
|
+
setTrialInstrument: (id: string, instrumentName: string) => void;
|
|
118
|
+
saveSnapshot: (trialId: string, name: string) => void;
|
|
119
|
+
loadSnapshot: (snapshotId: string, trialId: string) => void;
|
|
106
120
|
deleteSnapshot: (snapshotId: string) => void;
|
|
107
|
-
listSnapshots: (
|
|
121
|
+
listSnapshots: (trialId?: string) => SavedSnapshot[];
|
|
108
122
|
setMode: (mode: LabMode) => void;
|
|
109
123
|
setLayout: (layout: Record<string, unknown>) => void;
|
|
110
124
|
}
|
|
@@ -130,22 +144,32 @@ declare function LabStoreProvider({ store, children, }: {
|
|
|
130
144
|
}): ReactElement;
|
|
131
145
|
/** Subscribe to the whole lab store. Throws outside a `<LabStoreProvider>`. */
|
|
132
146
|
declare function useLabStore(): LabStoreState & ReturnType<LabStore['getState']>;
|
|
133
|
-
/** Context carrying which
|
|
134
|
-
declare const
|
|
135
|
-
/** Names the
|
|
147
|
+
/** Context carrying which trial the subtree belongs to. */
|
|
148
|
+
declare const TrialIdContext: react.Context<string | null>;
|
|
149
|
+
/** Names the trial its subtree belongs to, so an instrument's hooks can
|
|
136
150
|
* find their own record in the store without being passed an id. */
|
|
137
|
-
declare function
|
|
138
|
-
|
|
151
|
+
declare function TrialIdProvider({ trialId, children, }: {
|
|
152
|
+
trialId: string;
|
|
139
153
|
children: ReactNode;
|
|
140
154
|
}): ReactElement;
|
|
141
|
-
/** The id of the
|
|
142
|
-
* `<
|
|
143
|
-
declare function
|
|
155
|
+
/** The id of the trial this component is inside. Throws outside a
|
|
156
|
+
* `<TrialIdProvider>`. */
|
|
157
|
+
declare function useTrialId(): string;
|
|
158
|
+
|
|
159
|
+
/** Bumped whenever the persisted shape changes; every bump needs a migration. */
|
|
160
|
+
declare const CURRENT_DOCUMENT_VERSION = 2;
|
|
161
|
+
/** The one key a lab persists under. The `:doc` suffix keeps it out of the
|
|
162
|
+
* legacy bucket namespace, where a lab named `a:saves` would otherwise write
|
|
163
|
+
* its document over lab `a`'s saves bucket. */
|
|
164
|
+
declare function labDocumentKey(storageKey: string): string;
|
|
165
|
+
/** Where a document that failed to migrate is set aside, so a bad migration
|
|
166
|
+
* loses state loudly rather than silently. */
|
|
167
|
+
declare function quarantineKey(storageKey: string): string;
|
|
144
168
|
|
|
145
169
|
/** An instrument's own state and config, plus setters. Reads the surrounding
|
|
146
|
-
*
|
|
147
|
-
* running in. Throws outside a lab store and
|
|
148
|
-
declare function
|
|
170
|
+
* trial id, so an instrument never has to know which trial it is
|
|
171
|
+
* running in. Throws outside a lab store and trial. */
|
|
172
|
+
declare function useTrialState<TS = unknown, TC = unknown>(): TrialStateHandle<TS, TC>;
|
|
149
173
|
|
|
150
|
-
export { LabStoreContext as
|
|
151
|
-
export type {
|
|
174
|
+
export { CURRENT_DOCUMENT_VERSION as C, LabStoreContext as e, LabStoreProvider as f, TrialIdContext as i, TrialIdProvider as j, createLabStore as l, createMemoryAdapter as m, labDocumentKey as n, localStorageAdapter as o, noneAdapter as p, quarantineKey as q, useLabStore as r, sessionStorageAdapter as s, useTrialId as t, urlHashAdapter as u, useTrialState as v };
|
|
175
|
+
export type { InstrumentSerializers as I, LabDocument as L, Migration as M, SerializedTrial as S, TrialRecord as T, UndoStack as U, StorageAdapter as a, CreateLabStoreOptions as b, LabStore as c, LabStoreActions as d, LabStoreState as g, SavedSnapshot as h, TrialStateHandle as k, LabMode as w };
|
package/dist/canvas/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import * as react from 'react';
|
|
3
3
|
import { ReactNode } from 'react';
|
|
4
|
-
import { V as ViewTransform,
|
|
4
|
+
import { V as ViewTransform, e as Point } from '../_dts/index-iAP6XbH3.js';
|
|
5
5
|
import '../_dts/types-x92Kfeme.js';
|
|
6
6
|
|
|
7
7
|
/** One layer of a canvas stack: its id, whether it is currently shown, and how
|