@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.
Files changed (85) hide show
  1. package/README.md +6 -6
  2. package/dist/_dts/{index-CFlDPeZh.d.ts → index-iAP6XbH3.d.ts} +17 -7
  3. package/dist/_dts/{useExperimentState-D7EQnnwJ.d.ts → useTrialState-DYe2vUwN.d.ts} +60 -36
  4. package/dist/canvas/index.d.ts +1 -1
  5. package/dist/chunk-3TYUJR7Z.js +488 -0
  6. package/dist/chunk-3TYUJR7Z.js.map +1 -0
  7. package/dist/{chunk-BAPZPDDA.js → chunk-C6GJKPUI.js} +365 -340
  8. package/dist/chunk-C6GJKPUI.js.map +1 -0
  9. package/dist/{chunk-DWV7SFKR.js → chunk-DJLDIRFN.js} +28 -12
  10. package/dist/chunk-DJLDIRFN.js.map +1 -0
  11. package/dist/{chunk-DEWXYFEU.js → chunk-NRKWVTVT.js} +3 -3
  12. package/dist/{chunk-DEWXYFEU.js.map → chunk-NRKWVTVT.js.map} +1 -1
  13. package/dist/dragdrop/index.d.ts +1 -1
  14. package/dist/index.d.ts +149 -149
  15. package/dist/index.js +164 -159
  16. package/dist/index.js.map +1 -1
  17. package/dist/layers/index.d.ts +2 -2
  18. package/dist/passthrough/weasel-canvas.js +1 -1
  19. package/dist/passthrough/weasel-ui.d.ts +9 -2
  20. package/dist/passthrough/weasel-ui.js +2 -2
  21. package/dist/state/index.d.ts +12 -12
  22. package/dist/state/index.js +5 -5
  23. package/dist/state/index.js.map +1 -1
  24. package/dist/styles.css +21 -13
  25. package/dist/ui/layers/index.js +3 -3
  26. package/dist/undo/index.d.ts +1 -1
  27. package/package.json +1 -1
  28. package/src/canvas/AGENTS.md +6 -2
  29. package/src/dragdrop/dragDrop.test.tsx +4 -7
  30. package/src/index.test.ts +52 -0
  31. package/src/index.ts +11 -7
  32. package/src/instrument/SineWave.smoke.test.tsx +1 -1
  33. package/src/instrument/capabilityDetector.ts +1 -1
  34. package/src/instrument/types.ts +18 -6
  35. package/src/lab/Lab.less +16 -0
  36. package/src/lab/Lab.stories.tsx +5 -5
  37. package/src/lab/Lab.test.tsx +25 -25
  38. package/src/lab/Lab.tsx +50 -50
  39. package/src/lab/LabContext.ts +12 -12
  40. package/src/lab/LabShell.tsx +1 -1
  41. package/src/lab/{WorkspaceGrid.less → Workspace.less} +2 -2
  42. package/src/lab/{WorkspaceGrid.stories.tsx → Workspace.stories.tsx} +13 -13
  43. package/src/lab/{WorkspaceGrid.test.tsx → Workspace.test.tsx} +24 -24
  44. package/src/lab/{WorkspaceGrid.tsx → Workspace.tsx} +18 -18
  45. package/src/lab/index.ts +2 -2
  46. package/src/layers/AGENTS.md +3 -3
  47. package/src/primitives/Toolbar.stories.tsx +2 -2
  48. package/src/state/SingletonExperiment.test.tsx +8 -8
  49. package/src/state/SingletonExperiment.tsx +9 -9
  50. package/src/state/context.tsx +12 -12
  51. package/src/state/document.test.ts +369 -0
  52. package/src/state/document.ts +194 -0
  53. package/src/state/helpers.test.ts +74 -46
  54. package/src/state/helpers.ts +21 -29
  55. package/src/state/index.ts +16 -8
  56. package/src/state/store.test.ts +440 -41
  57. package/src/state/store.ts +159 -102
  58. package/src/state/types.ts +29 -12
  59. package/src/state/{useExperimentState.test.tsx → useTrialState.test.tsx} +18 -18
  60. package/src/state/useTrialState.ts +29 -0
  61. package/src/styles.less +2 -2
  62. package/src/theme/base.less +1 -1
  63. package/src/{workspace → trial}/DefaultSidebar.tsx +3 -3
  64. package/src/{workspace → trial}/DefaultStatusBar.tsx +5 -4
  65. package/src/{workspace → trial}/DefaultToolbar.tsx +9 -8
  66. package/src/trial/Trial.canvas.test.tsx +80 -0
  67. package/src/{workspace/Workspace.less → trial/Trial.less} +1 -1
  68. package/src/{workspace/Workspace.stories.tsx → trial/Trial.stories.tsx} +13 -18
  69. package/src/{workspace/Workspace.test.tsx → trial/Trial.test.tsx} +26 -26
  70. package/src/{workspace/Workspace.tsx → trial/Trial.tsx} +48 -43
  71. package/src/{workspace/WorkspaceChrome.tsx → trial/TrialChrome.tsx} +42 -42
  72. package/src/{workspace → trial}/index.ts +13 -13
  73. package/src/{workspace → trial}/slotTypes.ts +16 -16
  74. package/src/{workspace/workspaceOps.test.ts → trial/trialOps.test.ts} +38 -44
  75. package/src/trial/trialOps.ts +99 -0
  76. package/src/ui/format.test.ts +33 -0
  77. package/src/ui/format.ts +11 -0
  78. package/src/ui/properties/CurveField.tsx +6 -6
  79. package/src/ui/properties/SpeechBalloonPanels.stories.tsx +3 -3
  80. package/dist/chunk-BAPZPDDA.js.map +0 -1
  81. package/dist/chunk-DWV7SFKR.js.map +0 -1
  82. package/dist/chunk-KSTEW2AF.js +0 -367
  83. package/dist/chunk-KSTEW2AF.js.map +0 -1
  84. package/src/state/useExperimentState.ts +0 -31
  85. 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/Workspace/Instrument runtime arrives in later plans; v0.0.1 ships presentational primitives.
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, WorkspaceGrid, FpsMeter } from '@weasel-js/labkit';
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
- <WorkspaceGrid>
38
- <div>Workspace 1</div>
39
- <div>Workspace 2</div>
40
- </WorkspaceGrid>
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 workspace it is mounted in, and a way to emit named events. */
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
- workspace: {
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 workspace should offer
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 workspace's camera. */
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 workspace provide the corresponding chrome.
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, CanvasLayer as a, DragFeedback as b, LayerDescriptor as c, Point as d };
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 workspace's undo history, as snapshots of its state either side of the
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 workspace as the store holds it: which instrument it runs, that
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 WorkspaceRecord<TS = unknown, TC = unknown> {
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 workspace's config and state, restorable into any
28
- * workspace running the same instrument. */
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
- workspaceId: string;
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 workspaces, its saved snapshots, and the
40
+ /** Everything a lab persists: its trials, its saved snapshots, and the
41
41
  * chosen color mode. */
42
42
  interface LabStoreState {
43
- workspaces: WorkspaceRecord[];
43
+ trials: TrialRecord[];
44
44
  savedSnapshots: SavedSnapshot[];
45
45
  mode: LabMode;
46
- /** Per-workspace tile extents, keyed by workspace id. Opaque here — the
47
- * shape belongs to whatever lays the workspaces out. */
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 `useExperimentState` hands an instrument: its state and config, with
58
+ /** What `useTrialState` hands an instrument: its state and config, with
59
59
  * a setter for each. */
60
- interface ExperimentStateHandle<TS, TC> {
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 workspaces, saving and
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
- addWorkspace: (record: Omit<WorkspaceRecord, 'undoStack'>) => void;
98
- removeWorkspace: (id: string) => void;
99
- updateWorkspaceState: <TS>(id: string, next: TS | ((prev: TS) => TS)) => void;
100
- updateWorkspaceConfig: <TC>(id: string, key: keyof TC, value: TC[keyof TC]) => void;
101
- updateWorkspaceView: (id: string, view: WorkspaceRecord['view']) => void;
102
- updateWorkspaceUndoStack: (id: string, next: WorkspaceRecord['undoStack'] | ((prev: WorkspaceRecord['undoStack']) => WorkspaceRecord['undoStack'])) => void;
103
- setWorkspaceInstrument: (id: string, instrumentName: string) => void;
104
- saveSnapshot: (workspaceId: string, name: string) => void;
105
- loadSnapshot: (snapshotId: string, workspaceId: string) => void;
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: (workspaceId?: string) => SavedSnapshot[];
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 workspace the subtree belongs to. */
134
- declare const WorkspaceIdContext: react.Context<string | null>;
135
- /** Names the workspace its subtree belongs to, so an instrument's hooks can
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 WorkspaceIdProvider({ workspaceId, children, }: {
138
- workspaceId: string;
151
+ declare function TrialIdProvider({ trialId, children, }: {
152
+ trialId: string;
139
153
  children: ReactNode;
140
154
  }): ReactElement;
141
- /** The id of the workspace this component is inside. Throws outside a
142
- * `<WorkspaceIdProvider>`. */
143
- declare function useWorkspaceId(): string;
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
- * workspace id, so an instrument never has to know which workspace it is
147
- * running in. Throws outside a lab store and workspace. */
148
- declare function useExperimentState<TS = unknown, TC = unknown>(): ExperimentStateHandle<TS, TC>;
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 b, LabStoreProvider as c, WorkspaceIdContext as f, WorkspaceIdProvider as g, createLabStore as h, createMemoryAdapter as i, useExperimentState as j, useLabStore as k, localStorageAdapter as l, useWorkspaceId as m, noneAdapter as n, sessionStorageAdapter as s, urlHashAdapter as u };
151
- export type { CreateLabStoreOptions as C, ExperimentStateHandle as E, InstrumentSerializers as I, LabStore as L, StorageAdapter as S, UndoStack as U, WorkspaceRecord as W, LabStoreActions as a, LabStoreState as d, SavedSnapshot as e, LabMode as o };
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 };
@@ -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, d as Point } from '../_dts/index-CFlDPeZh.js';
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