@weasel-js/labkit 1.0.4 → 1.2.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 (122) hide show
  1. package/README.md +33 -6
  2. package/dist/_dts/{DrawCommand-BkZztJsW.d.ts → DrawCommand-C_XboUpz.d.ts} +48 -6
  3. package/dist/_dts/{index-CFlDPeZh.d.ts → index-JFAYj5Tv.d.ts} +29 -7
  4. package/dist/_dts/types-DJ79Tg5J.d.ts +56 -0
  5. package/dist/_dts/useTrialState-BMNIx3Cy.d.ts +171 -0
  6. package/dist/canvas/index.d.ts +2 -1
  7. package/dist/{chunk-BAPZPDDA.js → chunk-73KA7WBO.js} +425 -394
  8. package/dist/chunk-73KA7WBO.js.map +1 -0
  9. package/dist/{chunk-DWV7SFKR.js → chunk-BOHF3PQO.js} +4397 -3726
  10. package/dist/chunk-BOHF3PQO.js.map +1 -0
  11. package/dist/{chunk-DEWXYFEU.js → chunk-G5TJVQQT.js} +3 -3
  12. package/dist/{chunk-DEWXYFEU.js.map → chunk-G5TJVQQT.js.map} +1 -1
  13. package/dist/chunk-LN6JDUGB.js +106 -0
  14. package/dist/chunk-LN6JDUGB.js.map +1 -0
  15. package/dist/chunk-THBG7FQZ.js +167 -0
  16. package/dist/chunk-THBG7FQZ.js.map +1 -0
  17. package/dist/chunk-VUU5UXHE.js +491 -0
  18. package/dist/chunk-VUU5UXHE.js.map +1 -0
  19. package/dist/dragdrop/index.d.ts +2 -1
  20. package/dist/index.d.ts +211 -151
  21. package/dist/index.js +317 -169
  22. package/dist/index.js.map +1 -1
  23. package/dist/job/index.d.ts +13 -0
  24. package/dist/job/index.js +3 -0
  25. package/dist/job/index.js.map +1 -0
  26. package/dist/layers/index.d.ts +3 -2
  27. package/dist/passthrough/weasel-canvas.d.ts +1 -1
  28. package/dist/passthrough/weasel-canvas.js +1 -1
  29. package/dist/passthrough/weasel-ui.d.ts +29 -6
  30. package/dist/passthrough/weasel-ui.js +2 -2
  31. package/dist/state/index.d.ts +12 -12
  32. package/dist/state/index.js +5 -5
  33. package/dist/state/index.js.map +1 -1
  34. package/dist/styles.css +31 -13
  35. package/dist/surface/index.d.ts +77 -0
  36. package/dist/surface/index.js +3 -0
  37. package/dist/surface/index.js.map +1 -0
  38. package/dist/ui/layers/index.js +3 -3
  39. package/dist/undo/index.d.ts +2 -1
  40. package/package.json +9 -1
  41. package/src/canvas/AGENTS.md +14 -2
  42. package/src/canvas/useOrbit.test.ts +71 -0
  43. package/src/canvas/useOrbit.ts +149 -0
  44. package/src/dragdrop/dragDrop.test.tsx +4 -7
  45. package/src/index.test.ts +70 -0
  46. package/src/index.ts +24 -7
  47. package/src/instrument/SineWave.smoke.test.tsx +2 -1
  48. package/src/instrument/capabilityDetector.ts +1 -1
  49. package/src/instrument/types.ts +30 -6
  50. package/src/job/index.ts +3 -0
  51. package/src/job/types.ts +47 -0
  52. package/src/job/useJob.test.tsx +210 -0
  53. package/src/job/useJob.ts +134 -0
  54. package/src/lab/Lab.less +16 -0
  55. package/src/lab/Lab.stories.tsx +6 -7
  56. package/src/lab/Lab.test.tsx +25 -25
  57. package/src/lab/Lab.tsx +50 -50
  58. package/src/lab/LabContext.ts +12 -12
  59. package/src/lab/LabShell.tsx +1 -1
  60. package/src/lab/{WorkspaceGrid.less → Workspace.less} +2 -2
  61. package/src/lab/{WorkspaceGrid.stories.tsx → Workspace.stories.tsx} +13 -13
  62. package/src/lab/Workspace.surface.test.tsx +49 -0
  63. package/src/lab/{WorkspaceGrid.test.tsx → Workspace.test.tsx} +24 -24
  64. package/src/lab/{WorkspaceGrid.tsx → Workspace.tsx} +32 -19
  65. package/src/lab/index.ts +2 -2
  66. package/src/layers/AGENTS.md +3 -3
  67. package/src/primitives/Toolbar.stories.tsx +2 -2
  68. package/src/state/SingletonExperiment.test.tsx +8 -8
  69. package/src/state/SingletonExperiment.tsx +9 -9
  70. package/src/state/context.tsx +12 -12
  71. package/src/state/document.test.ts +369 -0
  72. package/src/state/document.ts +194 -0
  73. package/src/state/helpers.test.ts +74 -46
  74. package/src/state/helpers.ts +21 -29
  75. package/src/state/index.ts +16 -8
  76. package/src/state/store.test.ts +440 -41
  77. package/src/state/store.ts +165 -102
  78. package/src/state/types.ts +32 -13
  79. package/src/state/{useExperimentState.test.tsx → useTrialState.test.tsx} +18 -18
  80. package/src/state/useTrialState.ts +29 -0
  81. package/src/state/view.test.ts +127 -0
  82. package/src/state/view.ts +18 -0
  83. package/src/styles.less +2 -2
  84. package/src/surface/AGENTS.md +64 -0
  85. package/src/surface/SurfaceContext.ts +5 -0
  86. package/src/surface/composeRects.test.ts +50 -0
  87. package/src/surface/composeRects.ts +19 -0
  88. package/src/surface/deviceRect.test.ts +40 -0
  89. package/src/surface/deviceRect.ts +19 -0
  90. package/src/surface/index.ts +7 -0
  91. package/src/surface/rect.ts +16 -0
  92. package/src/surface/useSurfaceTile.test.tsx +67 -0
  93. package/src/surface/useSurfaceTile.ts +32 -0
  94. package/src/surface/useTiledSurface.test.tsx +231 -0
  95. package/src/surface/useTiledSurface.ts +157 -0
  96. package/src/theme/base.less +1 -1
  97. package/src/{workspace → trial}/DefaultSidebar.tsx +3 -3
  98. package/src/trial/DefaultStatusBar.tsx +19 -0
  99. package/src/{workspace → trial}/DefaultToolbar.tsx +9 -8
  100. package/src/trial/Trial.canvas.test.tsx +80 -0
  101. package/src/trial/Trial.job.test.tsx +75 -0
  102. package/src/{workspace/Workspace.less → trial/Trial.less} +13 -1
  103. package/src/{workspace/Workspace.stories.tsx → trial/Trial.stories.tsx} +20 -18
  104. package/src/{workspace/Workspace.test.tsx → trial/Trial.test.tsx} +26 -26
  105. package/src/{workspace/Workspace.tsx → trial/Trial.tsx} +77 -46
  106. package/src/{workspace/WorkspaceChrome.tsx → trial/TrialChrome.tsx} +73 -46
  107. package/src/{workspace → trial}/index.ts +13 -13
  108. package/src/{workspace → trial}/slotTypes.ts +18 -17
  109. package/src/{workspace/workspaceOps.test.ts → trial/trialOps.test.ts} +38 -44
  110. package/src/trial/trialOps.ts +99 -0
  111. package/src/ui/format.test.ts +33 -0
  112. package/src/ui/format.ts +11 -0
  113. package/src/ui/properties/CurveField.tsx +6 -6
  114. package/src/ui/properties/SpeechBalloonPanels.stories.tsx +3 -3
  115. package/dist/_dts/useExperimentState-D7EQnnwJ.d.ts +0 -151
  116. package/dist/chunk-BAPZPDDA.js.map +0 -1
  117. package/dist/chunk-DWV7SFKR.js.map +0 -1
  118. package/dist/chunk-KSTEW2AF.js +0 -367
  119. package/dist/chunk-KSTEW2AF.js.map +0 -1
  120. package/src/state/useExperimentState.ts +0 -31
  121. package/src/workspace/DefaultStatusBar.tsx +0 -18
  122. 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
  }
@@ -59,6 +59,33 @@ import { ThemeProvider } from '@weasel-js/theme/react';
59
59
  `"light"` or `"dark"`. Only `styles.css` needs importing — the token values
60
60
  arrive through the provider.
61
61
 
62
+ ## Driving your own renderer
63
+
64
+ `CanvasStack` is 2D. For three.js or raw WebGL, take rects and dirtiness from
65
+ labkit and keep the GL yourself:
66
+
67
+ ```tsx
68
+ import { toDeviceRect, useSurfaceTile, useTiledSurface } from '@weasel-js/labkit/surface';
69
+ ```
70
+
71
+ A trial's `view` is opaque to labkit — it is persisted, restored on Reset and
72
+ handed to the instrument without being read into — so a 3D lab stores an orbit
73
+ there and gets all three. `useOrbit` is the 3D peer of `usePanZoom`.
74
+
75
+ See `src/surface/AGENTS.md` for the contract and the traps.
76
+
77
+ ## Long-running work
78
+
79
+ An instrument with work too slow for a render declares a `job`. labkit starts it,
80
+ aborts it on unmount and on a key change, discards results from a superseded run,
81
+ and renders progress and a cancel control into the trial chrome. Per-item failure
82
+ is an event rather than a thrown error, so a run with two failed items is a
83
+ partial success.
84
+
85
+ ```tsx
86
+ import type { JobCapability } from '@weasel-js/labkit/job';
87
+ ```
88
+
62
89
  ## Development
63
90
 
64
91
  ```bash
@@ -88,6 +88,8 @@ interface SerializedHistoryEntry {
88
88
  label: string;
89
89
  forwardOps: SerializedOp[];
90
90
  baseOps: SerializedOp[];
91
+ selectionBefore?: readonly string[];
92
+ selectionAfter?: readonly string[];
91
93
  }
92
94
  /** Snapshot of an entire `History` instance. Designed to live alongside the
93
95
  * scene snapshot in IDB so a reload restores the undo / redo stacks to
@@ -119,6 +121,10 @@ interface HistoryEntry {
119
121
  * contribute nothing. May be `undefined` for deserialized entries
120
122
  * restored from an older snapshot that predates this field. */
121
123
  touchedIds?: ReadonlySet<string>;
124
+ /** Selection restored when this entry is undone. */
125
+ selectionBefore?: readonly string[];
126
+ /** Selection restored when this entry is redone. */
127
+ selectionAfter?: readonly string[];
122
128
  }
123
129
  /** Op-batched undo/redo controller returned by `createHistory`. */
124
130
  interface History {
@@ -170,7 +176,7 @@ interface History {
170
176
  * Unlike `applyOps`, does NOT call `op.apply()`. Used by Journal.commit
171
177
  * to flush a session's net forward ops to the parent as one entry without
172
178
  * re-mutating the scene. */
173
- recordEntry(ops: Op[], label: string): void;
179
+ recordEntry(ops: Op[], label: string, options?: RecordEntryOptions): void;
174
180
  /** Concatenated forwardOps of every undo-stack entry, in order. Snapshot
175
181
  * of "what changes are currently applied via this history" — useful for
176
182
  * Journal.commit to flush to a parent, and for any caller that wants to
@@ -193,6 +199,14 @@ interface History {
193
199
  * to resume or discard before calling this. */
194
200
  resumeJournal(journal: Journal): void;
195
201
  }
202
+ /** Options for `recordEntry`. */
203
+ interface RecordEntryOptions {
204
+ /** Selection as of before the already-applied ops ran. `recordEntry` is
205
+ * called after the fact, so the live selection has moved on by then and
206
+ * the engine cannot sample it — a caller that wants undo to restore the
207
+ * selection captures it when the batch opens and passes it here. */
208
+ selectionBefore?: readonly string[];
209
+ }
196
210
 
197
211
  /** Fill rule used by polygon path hit-testing and `ctx.fill()`. */
198
212
  type PathFillRule = 'nonzero' | 'evenodd';
@@ -500,6 +514,14 @@ interface Scene<TData, TLayer extends string, TPose = RectPose> {
500
514
  * `SceneCanvasAdapter`). Pass `this` from `sceneToAdapter` or a compatible
501
515
  * adapter. */
502
516
  applyBatch(ops: Op[], label: string, adapter: unknown): void;
517
+ /** The transient set of active ids — "operate on these N as a unit".
518
+ * Shared by every view over this scene unless a view supplies its own
519
+ * (see `CanvasView.selection`). Not document content: it never appears
520
+ * in `toJSON`. It does ride on history entries, so undo and redo put
521
+ * back the selection an edit was made under; changing it is never an
522
+ * undo step of its own. */
523
+ getSelection(): readonly NodeId[];
524
+ setSelection(ids: readonly NodeId[]): void;
503
525
  undo(): boolean;
504
526
  redo(): boolean;
505
527
  canUndo(): boolean;
@@ -792,17 +814,22 @@ type StrokeAlign = 'center' | 'inner' | 'outer';
792
814
  /** Stroke style: a FillStyle plus structural line parameters. */
793
815
  interface Stroke {
794
816
  paint: FillStyle;
795
- width?: number;
817
+ /** World units, or `{ px }` for screen pixels — resolved against the
818
+ * accumulated transform scale at draw time, so it holds its on-screen
819
+ * thickness as the view zooms. */
820
+ width?: number | {
821
+ px: number;
822
+ };
796
823
  /** Per `CanvasRenderingContext2D.setLineDash` — empty/omitted = solid. */
797
824
  dash?: number[];
798
825
  cap?: 'butt' | 'round' | 'square';
799
826
  join?: 'miter' | 'round' | 'bevel';
800
827
  /**
801
828
  * Miter join fallback threshold. When the miter length exceeds
802
- * `miterLimit * width / 2`, the join falls back to a bevel. Default 10
803
- * (matching Canvas2D). SVG's default is 4; consumers that want SVG
804
- * fidelity should set this explicitly when constructing strokes from
805
- * SVG sources where the attribute was omitted.
829
+ * `miterLimit * width / 2`, the join falls back to a bevel. Default 4,
830
+ * matching SVG which is also what the kit's own serializer implies when
831
+ * it omits the attribute for an unset field. Canvas2D's 10 lets an acute
832
+ * corner throw a spike four times the half-width.
806
833
  */
807
834
  miterLimit?: number;
808
835
  /** Where the stroke sits relative to the geometric edge. Default `'center'`. */
@@ -1063,6 +1090,21 @@ interface ImageDrawCommand {
1063
1090
  * device pixels as hard squares — required by anything magnifying a
1064
1091
  * framebuffer readback, where blur destroys the point of the readback. */
1065
1092
  sampling?: 'linear' | 'nearest';
1093
+ /** Sub-rectangle of `image` to draw, in bitmap pixels from the top-left.
1094
+ * Omitted draws the whole bitmap. Not range-checked: a rect past the edge
1095
+ * samples outside [0..1], which CLAMP_TO_EDGE smears. With
1096
+ * `sampling: 'linear'` the filter reaches half a texel beyond `source`, so
1097
+ * atlas frames need a gutter (see `SpriteSheet.spacing`) or `'nearest'`. */
1098
+ source?: {
1099
+ x: number;
1100
+ y: number;
1101
+ w: number;
1102
+ h: number;
1103
+ };
1104
+ /** Mirror the sampled region within the destination rect. The quad does not
1105
+ * move — a flipped draw covers exactly the pixels an unflipped one does. */
1106
+ flipX?: boolean;
1107
+ flipY?: boolean;
1066
1108
  }
1067
1109
  /**
1068
1110
  * Custom shader draw command. The renderer generates a quad over `bounds`
@@ -1,21 +1,34 @@
1
1
  import { ReactNode } from 'react';
2
2
  import { C as ConfigField } from './types-x92Kfeme.js';
3
+ import { J as JobHandle, a as JobCapability } from './types-DJ79Tg5J.js';
3
4
 
4
5
  /** 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. */
6
+ * for both, the trial it is mounted in, and a way to emit named events. */
6
7
  interface RenderContext<TS = unknown, TC = unknown> {
7
8
  state: TS;
8
9
  config: TC;
9
10
  setState: (next: TS | ((prev: TS) => TS)) => void;
10
11
  setConfig: (key: keyof TC, value: unknown) => void;
11
- workspace: {
12
+ trial: {
12
13
  id: string;
14
+ /** The trial's view, in whatever shape this instrument chose. labkit persists
15
+ * it and restores it on Reset without ever reading into it. */
16
+ view: unknown;
17
+ setView: (next: unknown) => void;
18
+ /** 2D convenience over `view`. Reads 1 and writes nothing when the trial holds
19
+ * a view that is not the 2D one. */
13
20
  zoom: number;
14
21
  setZoom: (z: number) => void;
15
22
  };
16
23
  emit: (event: string) => void;
24
+ /** Present only when the instrument declares a `job`. */
25
+ job?: JobHandle;
17
26
  }
18
- /** One 2D canvas layer of an instrument, drawn in declaration order. */
27
+ /** One 2D canvas layer of an instrument, drawn in declaration order.
28
+ *
29
+ * `draw` is called with the camera already applied, so it works in world
30
+ * coordinates. `zoom` is passed for the things that must not scale with it —
31
+ * set `ctx.lineWidth = 1 / zoom` to keep a hairline hairline. */
19
32
  interface CanvasLayer<TS = unknown, TC = unknown> {
20
33
  id: string;
21
34
  draw: (ctx: CanvasRenderingContext2D, args: {
@@ -36,7 +49,7 @@ interface CanvasCapability<TS = unknown, TC = unknown> {
36
49
  };
37
50
  };
38
51
  }
39
- /** Declares which of an instrument's layers the workspace should offer
52
+ /** Declares which of an instrument's layers the trial should offer
40
53
  * show/hide controls for. */
41
54
  interface LayerCapability {
42
55
  ids: string[];
@@ -72,7 +85,7 @@ type HitResult = {
72
85
  layerId?: string;
73
86
  pointId?: string;
74
87
  };
75
- /** A workspace's camera. */
88
+ /** A trial's camera. */
76
89
  type ViewTransform = {
77
90
  zoom: number;
78
91
  pan: Point;
@@ -102,13 +115,15 @@ type DragFeedback = {
102
115
  * and `state`, what the experiment is currently doing — and renders from both.
103
116
  * The optional capability fields declare what else it wants from the runtime:
104
117
  * a canvas, a layer list, palette drag-and-drop, undo. Declaring a capability
105
- * is what makes the workspace provide the corresponding chrome.
118
+ * is what makes the trial provide the corresponding chrome.
106
119
  */
107
120
  interface Instrument<TS = unknown, TC = unknown> {
108
121
  name: string;
109
122
  defaultConfig: () => TC;
110
123
  initialState: (config: TC) => TS;
111
124
  configSchema?: () => ConfigField[];
125
+ /** The instrument's DOM. With `canvas`, this renders as an overlay above the
126
+ * layers rather than instead of them; return `null` for canvas only. */
112
127
  render: (ctx: RenderContext<TS, TC>) => ReactNode;
113
128
  onConfigChange?: (config: TC, prev: TC, state: TS) => TS;
114
129
  serialize?: (state: TS) => unknown;
@@ -117,6 +132,13 @@ interface Instrument<TS = unknown, TC = unknown> {
117
132
  layers?: LayerCapability;
118
133
  dragDrop?: DragDropCapability<TS, TC>;
119
134
  undo?: UndoCapability;
135
+ /** Work too slow to do during a render. The runtime starts it, aborts it on
136
+ * unmount and on a `key` change, and renders progress into the trial. */
137
+ job?: JobCapability<TS, TC, never>;
120
138
  }
139
+ /** Instruments as a lab receives them. `any` rather than `unknown` because
140
+ * parameter contravariance keeps a `defineInstrument<TS, TC>` result out of
141
+ * an `Instrument<unknown, unknown>[]`; it is contained to this alias. */
142
+ type InstrumentList = readonly Instrument<any, any>[];
121
143
 
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 };
144
+ 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 };
@@ -0,0 +1,56 @@
1
+ /** What a running job reports as it goes. `total` may arrive at any point and may
2
+ * arrive more than once; a job that cannot count up front simply never sends it.
3
+ *
4
+ * `failed` is a first-class event rather than a thrown error because these
5
+ * failures are per item: a run with two failed items is a partial success, and
6
+ * its other items are worth showing. */
7
+ type JobEvent<T> = {
8
+ kind: 'total';
9
+ total: number;
10
+ } | {
11
+ kind: 'item';
12
+ item: T;
13
+ } | {
14
+ kind: 'failed';
15
+ index: number;
16
+ error: string;
17
+ };
18
+ /** Where a job is. `idle` before its first run and after a cancel; `done` when the
19
+ * iterable finished, whether or not items failed along the way. */
20
+ type JobStatus = 'idle' | 'running' | 'done' | 'error';
21
+ /** Declares that an instrument has work too slow to do during a render: what to
22
+ * run, when to re-run it, and how each result folds into state. */
23
+ interface JobCapability<TS = unknown, TC = unknown, TItem = unknown> {
24
+ /** Re-run whenever this value changes, compared element-wise. A job with no
25
+ * `key` runs only when something calls `start()`. */
26
+ key?: (config: TC, state: TS) => readonly unknown[];
27
+ /** Start on mount and on every `key` change. Default false. */
28
+ auto?: boolean;
29
+ run: (args: {
30
+ config: TC;
31
+ state: TS;
32
+ signal: AbortSignal;
33
+ }) => AsyncIterable<JobEvent<TItem>>;
34
+ /** Fold one result into state. Called once per `item` event, in arrival order. */
35
+ onItem: (item: TItem, state: TS) => TS;
36
+ }
37
+ /** One item that failed, and why. */
38
+ interface JobFailure {
39
+ index: number;
40
+ error: string;
41
+ }
42
+ /** What `RenderContext.job` exposes. Present only when the instrument declares the
43
+ * capability; `undefined` otherwise. */
44
+ interface JobHandle {
45
+ status: JobStatus;
46
+ done: number;
47
+ /** Null until the job reports a total, and forever if it never does. */
48
+ total: number | null;
49
+ failures: readonly JobFailure[];
50
+ /** The error that ended the run, when `status` is `'error'`. */
51
+ error: string | null;
52
+ start: () => void;
53
+ cancel: () => void;
54
+ }
55
+
56
+ export type { JobHandle as J, JobCapability as a, JobEvent as b, JobFailure as c, JobStatus as d };
@@ -0,0 +1,171 @@
1
+ import * as react from 'react';
2
+ import { ReactNode, ReactElement } from 'react';
3
+ import { StoreApi } from 'zustand/vanilla';
4
+
5
+ /** A trial's undo history, as snapshots of its state either side of the
6
+ * present. */
7
+ interface UndoStack {
8
+ past: unknown[];
9
+ future: unknown[];
10
+ }
11
+ /** One trial as the store holds it: which instrument it runs, that
12
+ * instrument's config and state, the camera, and the undo history. */
13
+ interface TrialRecord<TS = unknown, TC = unknown, TV = unknown> {
14
+ id: string;
15
+ instrumentName: string;
16
+ config: TC;
17
+ state: TS;
18
+ /** Opaque to labkit: persisted, restored on Reset and handed to the instrument,
19
+ * but never read into. A 3D lab puts an orbit here and keeps all three. */
20
+ view: TV;
21
+ undoStack: UndoStack;
22
+ }
23
+ /** A named, saved copy of a trial's config and state, restorable into any
24
+ * trial running the same instrument. */
25
+ interface SavedSnapshot {
26
+ id: string;
27
+ name: string;
28
+ trialId: string;
29
+ instrumentName: string;
30
+ config: unknown;
31
+ state: unknown;
32
+ savedAt: number;
33
+ }
34
+ /** `auto` follows the OS; the other two are an explicit choice. */
35
+ type LabMode = 'auto' | 'light' | 'dark';
36
+ /** Everything a lab persists: its trials, its saved snapshots, and the
37
+ * chosen color mode. */
38
+ interface LabStoreState {
39
+ trials: TrialRecord[];
40
+ savedSnapshots: SavedSnapshot[];
41
+ mode: LabMode;
42
+ /** Per-trial tile extents, keyed by trial id. Opaque here — the
43
+ * shape belongs to whatever lays the trials out. */
44
+ layout: Record<string, unknown>;
45
+ }
46
+ /** Where a lab persists itself. Implementations are keyed string storage and
47
+ * nothing more, so the same store works against localStorage, the URL hash,
48
+ * or memory. */
49
+ interface StorageAdapter {
50
+ read(key: string): string | null;
51
+ write(key: string, value: string): void;
52
+ delete?(key: string): void;
53
+ }
54
+ /** What `useTrialState` hands an instrument: its state and config, with
55
+ * a setter for each. */
56
+ interface TrialStateHandle<TS, TC> {
57
+ state: TS;
58
+ setState: (next: TS | ((prev: TS) => TS)) => void;
59
+ config: TC;
60
+ setConfig: (key: keyof TC, value: TC[keyof TC]) => void;
61
+ }
62
+ /** Options for `createLabStore`. `storageKey` namespaces the keys written, so
63
+ * two labs on one origin do not collide. */
64
+ interface CreateLabStoreOptions {
65
+ storageKey: string;
66
+ storage: StorageAdapter;
67
+ initialMode?: LabMode;
68
+ }
69
+ /** Per-instrument serialize/deserialize hooks, keyed by instrument name. An
70
+ * instrument whose state is already JSON-safe needs no entry. */
71
+ type InstrumentSerializers = Record<string, {
72
+ serialize?: (state: unknown) => unknown;
73
+ deserialize?: (data: unknown) => unknown;
74
+ } | undefined>;
75
+ /** A trial as it is persisted: everything but the undo history, which is
76
+ * session-only. */
77
+ type SerializedTrial = Omit<TrialRecord, 'undoStack'>;
78
+ /** Everything a lab persists, under one key, at a known version. */
79
+ interface LabDocument {
80
+ version: number;
81
+ trials: SerializedTrial[];
82
+ saves: SavedSnapshot[];
83
+ layout: Record<string, unknown>;
84
+ mode: LabMode;
85
+ }
86
+ /** Migrates a document one version forward. Index `i` in the chain takes a
87
+ * version-`i` document to version `i + 1`. */
88
+ type Migration = (doc: Record<string, unknown>) => Record<string, unknown>;
89
+
90
+ /** Persist to `localStorage` — state survives a reload and a new tab. Falls
91
+ * back to no-ops when storage is unavailable. */
92
+ declare const localStorageAdapter: StorageAdapter;
93
+ /** Persist to `sessionStorage` — state survives a reload but not a new tab. */
94
+ declare const sessionStorageAdapter: StorageAdapter;
95
+ /** Persist into the URL fragment, so the page's link carries its state and
96
+ * can be shared or bookmarked. */
97
+ declare const urlHashAdapter: StorageAdapter;
98
+ /** An in-memory store, discarded on reload. For tests, and for labs that
99
+ * should start fresh every time. */
100
+ declare function createMemoryAdapter(): StorageAdapter;
101
+ /** Persists nothing and reads back nothing. */
102
+ declare const noneAdapter: StorageAdapter;
103
+
104
+ /** Every mutation a lab store supports: managing trials, saving and
105
+ * restoring snapshots, and setting the color mode. */
106
+ interface LabStoreActions {
107
+ addTrial: (record: Omit<TrialRecord, 'undoStack'>) => void;
108
+ removeTrial: (id: string) => void;
109
+ updateTrialState: <TS>(id: string, next: TS | ((prev: TS) => TS)) => void;
110
+ updateTrialConfig: <TC>(id: string, key: keyof TC, value: TC[keyof TC]) => void;
111
+ updateTrialView: (id: string, view: unknown) => void;
112
+ updateTrialUndoStack: (id: string, next: TrialRecord['undoStack'] | ((prev: TrialRecord['undoStack']) => TrialRecord['undoStack'])) => void;
113
+ setTrialInstrument: (id: string, instrumentName: string) => void;
114
+ saveSnapshot: (trialId: string, name: string) => void;
115
+ loadSnapshot: (snapshotId: string, trialId: string) => void;
116
+ deleteSnapshot: (snapshotId: string) => void;
117
+ listSnapshots: (trialId?: string) => SavedSnapshot[];
118
+ setMode: (mode: LabMode) => void;
119
+ setLayout: (layout: Record<string, unknown>) => void;
120
+ }
121
+ /** A lab's store: its state and actions, plus the hook instruments use to
122
+ * register how their state is serialized. */
123
+ type LabStore = StoreApi<LabStoreState & LabStoreActions> & {
124
+ registerSerializers: (s: InstrumentSerializers) => void;
125
+ };
126
+ /** Build a lab store, hydrating from storage if anything was saved under the
127
+ * same key. Writes back are debounced. */
128
+ declare function createLabStore(options: CreateLabStoreOptions): LabStore;
129
+
130
+ type LabStoreCtx = {
131
+ store: LabStore;
132
+ } | null;
133
+ /** Context carrying the lab store. Prefer `useLabStore`; this is exported for
134
+ * code that needs to read the context without subscribing. */
135
+ declare const LabStoreContext: react.Context<LabStoreCtx>;
136
+ /** Provides a lab store to its subtree. */
137
+ declare function LabStoreProvider({ store, children, }: {
138
+ store: LabStore;
139
+ children: ReactNode;
140
+ }): ReactElement;
141
+ /** Subscribe to the whole lab store. Throws outside a `<LabStoreProvider>`. */
142
+ declare function useLabStore(): LabStoreState & ReturnType<LabStore['getState']>;
143
+ /** Context carrying which trial the subtree belongs to. */
144
+ declare const TrialIdContext: react.Context<string | null>;
145
+ /** Names the trial its subtree belongs to, so an instrument's hooks can
146
+ * find their own record in the store without being passed an id. */
147
+ declare function TrialIdProvider({ trialId, children, }: {
148
+ trialId: string;
149
+ children: ReactNode;
150
+ }): ReactElement;
151
+ /** The id of the trial this component is inside. Throws outside a
152
+ * `<TrialIdProvider>`. */
153
+ declare function useTrialId(): string;
154
+
155
+ /** Bumped whenever the persisted shape changes; every bump needs a migration. */
156
+ declare const CURRENT_DOCUMENT_VERSION = 2;
157
+ /** The one key a lab persists under. The `:doc` suffix keeps it out of the
158
+ * legacy bucket namespace, where a lab named `a:saves` would otherwise write
159
+ * its document over lab `a`'s saves bucket. */
160
+ declare function labDocumentKey(storageKey: string): string;
161
+ /** Where a document that failed to migrate is set aside, so a bad migration
162
+ * loses state loudly rather than silently. */
163
+ declare function quarantineKey(storageKey: string): string;
164
+
165
+ /** An instrument's own state and config, plus setters. Reads the surrounding
166
+ * trial id, so an instrument never has to know which trial it is
167
+ * running in. Throws outside a lab store and trial. */
168
+ declare function useTrialState<TS = unknown, TC = unknown>(): TrialStateHandle<TS, TC>;
169
+
170
+ 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 };
171
+ 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,8 +1,9 @@
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-JFAYj5Tv.js';
5
5
  import '../_dts/types-x92Kfeme.js';
6
+ import '../_dts/types-DJ79Tg5J.js';
6
7
 
7
8
  /** One layer of a canvas stack: its id, whether it is currently shown, and how
8
9
  * it paints itself. */