@weasel-js/labkit 1.0.1 → 1.0.3

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 (100) hide show
  1. package/dist/_dts/{DrawCommand-CitWPxMY.d.ts → DrawCommand-uKHt4Vul.d.ts} +56 -0
  2. package/dist/_dts/{index-C6Yze7sQ.d.ts → index-CFlDPeZh.d.ts} +30 -1
  3. package/dist/_dts/{types-Si4Fw-1F.d.ts → types-x92Kfeme.d.ts} +14 -0
  4. package/dist/_dts/{useExperimentState-CJn2hHzd.d.ts → useExperimentState-vrttakTt.d.ts} +43 -0
  5. package/dist/canvas/index.d.ts +13 -2
  6. package/dist/canvas/index.js +2 -2
  7. package/dist/{chunk-ISEK5LXT.js → chunk-3WPOGKUP.js} +1603 -1670
  8. package/dist/chunk-3WPOGKUP.js.map +1 -0
  9. package/dist/{chunk-54IQ2DX7.js → chunk-574LJAV4.js} +3 -3
  10. package/dist/chunk-574LJAV4.js.map +1 -0
  11. package/dist/{chunk-VLAHRJOC.js → chunk-5R2ATYPJ.js} +2 -2
  12. package/dist/{chunk-VLAHRJOC.js.map → chunk-5R2ATYPJ.js.map} +1 -1
  13. package/dist/{chunk-EXBV7A6X.js → chunk-73PXCRCR.js} +3 -3
  14. package/dist/chunk-73PXCRCR.js.map +1 -0
  15. package/dist/{chunk-PWC7AQZM.js → chunk-CPUJ3QXL.js} +2 -2
  16. package/dist/chunk-CPUJ3QXL.js.map +1 -0
  17. package/dist/{chunk-NRD3TDNQ.js → chunk-N5KTQKQA.js} +1462 -1487
  18. package/dist/chunk-N5KTQKQA.js.map +1 -0
  19. package/dist/{chunk-HXZHVU4G.js → chunk-PMAU3SEE.js} +3 -3
  20. package/dist/chunk-PMAU3SEE.js.map +1 -0
  21. package/dist/{chunk-7BKDG73Z.js → chunk-RL2LOLNI.js} +2 -2
  22. package/dist/chunk-RL2LOLNI.js.map +1 -0
  23. package/dist/{chunk-2QNYYL3V.js → chunk-SFL7NFKN.js} +2 -2
  24. package/dist/chunk-SFL7NFKN.js.map +1 -0
  25. package/dist/{chunk-53XSBIUK.js → chunk-T7OKNJTY.js} +2 -2
  26. package/dist/chunk-T7OKNJTY.js.map +1 -0
  27. package/dist/controls/index.d.ts +4 -2
  28. package/dist/controls/index.js +1 -1
  29. package/dist/dragdrop/index.d.ts +2 -2
  30. package/dist/index.d.ts +122 -6
  31. package/dist/index.js +18 -18
  32. package/dist/index.js.map +1 -1
  33. package/dist/layers/index.d.ts +6 -3
  34. package/dist/layers/index.js +1 -1
  35. package/dist/passthrough/weasel-canvas.d.ts +5 -1
  36. package/dist/passthrough/weasel-canvas.js +1 -1
  37. package/dist/passthrough/weasel-ui.d.ts +514 -36
  38. package/dist/passthrough/weasel-ui.js +2 -2
  39. package/dist/primitives/index.d.ts +13 -0
  40. package/dist/primitives/index.js +2 -2
  41. package/dist/state/index.d.ts +15 -2
  42. package/dist/state/index.js +2 -2
  43. package/dist/state/index.js.map +1 -1
  44. package/dist/styles.css +147 -0
  45. package/dist/ui/layers/index.d.ts +5 -0
  46. package/dist/ui/layers/index.js +3 -3
  47. package/dist/undo/index.d.ts +17 -2
  48. package/dist/undo/index.js +1 -1
  49. package/package.json +2 -2
  50. package/src/canvas/CanvasStack.tsx +4 -0
  51. package/src/canvas/CanvasStackContext.ts +3 -0
  52. package/src/canvas/canvasCoords.ts +2 -0
  53. package/src/canvas/useLayerScheduler.ts +2 -0
  54. package/src/controls/ControlPanel.tsx +2 -0
  55. package/src/controls/types.ts +14 -0
  56. package/src/instrument/capabilityDetector.ts +3 -0
  57. package/src/instrument/defineInstrument.ts +2 -0
  58. package/src/instrument/types.ts +29 -0
  59. package/src/instrument/validateConfigSchema.ts +5 -0
  60. package/src/lab/Lab.tsx +3 -0
  61. package/src/lab/LabContext.ts +5 -0
  62. package/src/lab/LabShell.tsx +4 -0
  63. package/src/lab/WorkspaceGrid.tsx +2 -0
  64. package/src/lab/gridDims.ts +2 -0
  65. package/src/layers/LayerList.tsx +3 -0
  66. package/src/primitives/FpsMeter.tsx +1 -0
  67. package/src/primitives/ScaleIndicator.tsx +4 -0
  68. package/src/primitives/Sidebar.tsx +3 -0
  69. package/src/primitives/StatusBar.tsx +3 -0
  70. package/src/primitives/Toolbar.tsx +6 -0
  71. package/src/state/SingletonExperiment.tsx +1 -0
  72. package/src/state/adapters.ts +8 -0
  73. package/src/state/context.tsx +9 -0
  74. package/src/state/helpers.ts +12 -0
  75. package/src/state/store.ts +6 -0
  76. package/src/state/types.ts +17 -0
  77. package/src/state/useExperimentState.ts +3 -0
  78. package/src/ui/layers/LayerStack.tsx +5 -0
  79. package/src/ui/properties/CurveField.tsx +4 -1
  80. package/src/ui/properties/EffectCard.tsx +12 -0
  81. package/src/ui/properties/PropertyGroup.tsx +1 -0
  82. package/src/ui/properties/PropertyPanel.tsx +28 -0
  83. package/src/undo/eventBus.ts +4 -0
  84. package/src/undo/undoStack.ts +11 -0
  85. package/src/workspace/DefaultSidebar.tsx +3 -0
  86. package/src/workspace/DefaultStatusBar.tsx +3 -0
  87. package/src/workspace/DefaultToolbar.tsx +3 -0
  88. package/src/workspace/Workspace.tsx +3 -0
  89. package/src/workspace/WorkspaceChrome.tsx +4 -0
  90. package/src/workspace/slotTypes.ts +9 -0
  91. package/src/workspace/workspaceOps.ts +8 -0
  92. package/dist/chunk-2QNYYL3V.js.map +0 -1
  93. package/dist/chunk-53XSBIUK.js.map +0 -1
  94. package/dist/chunk-54IQ2DX7.js.map +0 -1
  95. package/dist/chunk-7BKDG73Z.js.map +0 -1
  96. package/dist/chunk-EXBV7A6X.js.map +0 -1
  97. package/dist/chunk-HXZHVU4G.js.map +0 -1
  98. package/dist/chunk-ISEK5LXT.js.map +0 -1
  99. package/dist/chunk-NRD3TDNQ.js.map +0 -1
  100. package/dist/chunk-PWC7AQZM.js.map +0 -1
@@ -1,11 +1,15 @@
1
+ /** Which control a config field is edited with. */
1
2
  export type ConfigFieldType = 'slider' | 'checkbox' | 'select' | 'number' | 'text' | 'color';
2
3
 
4
+ /** What every config field carries: the config key it writes, the label shown
5
+ * beside it, and which control renders it. */
3
6
  export interface ConfigFieldBase {
4
7
  key: string;
5
8
  label: string;
6
9
  type: ConfigFieldType;
7
10
  }
8
11
 
12
+ /** A bounded number edited by dragging. */
9
13
  export interface SliderField extends ConfigFieldBase {
10
14
  type: 'slider';
11
15
  default: number;
@@ -14,22 +18,27 @@ export interface SliderField extends ConfigFieldBase {
14
18
  step?: number;
15
19
  }
16
20
 
21
+ /** A boolean. */
17
22
  export interface CheckboxField extends ConfigFieldBase {
18
23
  type: 'checkbox';
19
24
  default: boolean;
20
25
  }
21
26
 
27
+ /** One choice in a select field. */
22
28
  export interface SelectOption {
23
29
  value: string;
24
30
  label: string;
25
31
  }
26
32
 
33
+ /** A fixed set of labeled choices. */
27
34
  export interface SelectField extends ConfigFieldBase {
28
35
  type: 'select';
29
36
  default: string;
30
37
  options: SelectOption[];
31
38
  }
32
39
 
40
+ /** A number typed directly, optionally bounded. Use a slider field instead
41
+ * when the range matters more than the exact value. */
33
42
  export interface NumberField extends ConfigFieldBase {
34
43
  type: 'number';
35
44
  default: number;
@@ -38,6 +47,8 @@ export interface NumberField extends ConfigFieldBase {
38
47
  step?: number;
39
48
  }
40
49
 
50
+ /** A free-text string. Writes are debounced so typing does not re-run the
51
+ * instrument on every keystroke. */
41
52
  export interface TextField extends ConfigFieldBase {
42
53
  type: 'text';
43
54
  default: string;
@@ -47,11 +58,14 @@ export interface TextField extends ConfigFieldBase {
47
58
  debounceMs?: number;
48
59
  }
49
60
 
61
+ /** A color, as a CSS color string. */
50
62
  export interface ColorField extends ConfigFieldBase {
51
63
  type: 'color';
52
64
  default: string;
53
65
  }
54
66
 
67
+ /** One field of an instrument's config schema. The schema is what the control
68
+ * panel renders, and what `validateConfigSchema` checks. */
55
69
  export type ConfigField =
56
70
  | SliderField
57
71
  | CheckboxField
@@ -1,5 +1,7 @@
1
1
  import type { Instrument } from './types';
2
2
 
3
+ /** Which optional capabilities an instrument declared — what the workspace
4
+ * consults to decide which chrome to show. */
3
5
  export interface CapabilityFlags {
4
6
  hasCanvas: boolean;
5
7
  hasLayers: boolean;
@@ -7,6 +9,7 @@ export interface CapabilityFlags {
7
9
  hasUndo: boolean;
8
10
  }
9
11
 
12
+ /** Read an instrument's declared capabilities off its definition. */
10
13
  export function detectCapabilities(instrument: Instrument<unknown, unknown>): CapabilityFlags {
11
14
  return {
12
15
  hasCanvas: instrument.canvas != null,
@@ -1,5 +1,7 @@
1
1
  import type { Instrument } from './types';
2
2
 
3
+ /** Identity at runtime; exists so an instrument's state and config types are
4
+ * inferred from the spec rather than having to be written out. */
3
5
  export function defineInstrument<TS, TC>(spec: Instrument<TS, TC>): Instrument<TS, TC> {
4
6
  return spec;
5
7
  }
@@ -1,6 +1,8 @@
1
1
  import type { ReactNode } from 'react';
2
2
  import type { ConfigField } from '../controls/types';
3
3
 
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. */
4
6
  export interface RenderContext<TS = unknown, TC = unknown> {
5
7
  state: TS;
6
8
  config: TC;
@@ -14,20 +16,28 @@ export interface RenderContext<TS = unknown, TC = unknown> {
14
16
  emit: (event: string) => void;
15
17
  }
16
18
 
19
+ /** One 2D canvas layer of an instrument, drawn in declaration order. */
17
20
  export interface CanvasLayer<TS = unknown, TC = unknown> {
18
21
  id: string;
19
22
  draw: (ctx: CanvasRenderingContext2D, args: { state: TS; config: TC; zoom: number }) => void;
20
23
  }
21
24
 
25
+ /** Declares that an instrument draws to a canvas: its layers, and where the
26
+ * view starts. */
22
27
  export interface CanvasCapability<TS = unknown, TC = unknown> {
23
28
  layers: CanvasLayer<TS, TC>[];
24
29
  initialView?: { zoom: number; pan: { x: number; y: number } };
25
30
  }
26
31
 
32
+ /** Declares which of an instrument's layers the workspace should offer
33
+ * show/hide controls for. */
27
34
  export interface LayerCapability {
28
35
  ids: string[];
29
36
  }
30
37
 
38
+ /** Declares that an instrument accepts items dragged from a palette: what the
39
+ * palette offers, what a drop does to the state, and — optionally — live
40
+ * feedback during the drag and the ability to drag existing items back out. */
31
41
  export interface DragDropCapability<TS = unknown, TC = unknown> {
32
42
  palette: PaletteItem[] | ((state: TS, config: TC) => PaletteItem[]);
33
43
  onDrop: (worldPos: Point, item: PaletteItem, state: TS, config: TC) => TS;
@@ -35,20 +45,39 @@ export interface DragDropCapability<TS = unknown, TC = unknown> {
35
45
  pickUp?: (hit: HitResult, state: TS, config: TC) => { item: PaletteItem; state: TS } | null;
36
46
  }
37
47
 
48
+ /** Declares that an instrument's state is undoable: which emitted events
49
+ * snapshot it, and how many snapshots to keep. */
38
50
  export interface UndoCapability {
39
51
  snapshotOn?: string[];
40
52
  maxDepth?: number;
41
53
  }
42
54
 
55
+ /** The name of an event an instrument emits through `RenderContext.emit`. */
43
56
  export type SystemEvent = string;
44
57
 
58
+ /** A point in world coordinates. */
45
59
  export type Point = { x: number; y: number };
60
+ /** What a hit-test found, and where. */
46
61
  export type HitResult = { hit: boolean; layerId?: string; pointId?: string };
62
+ /** A workspace's camera. */
47
63
  export type ViewTransform = { zoom: number; pan: Point };
64
+ /** A layer as the layer list shows it. `alwaysOn` layers cannot be hidden. */
48
65
  export type LayerDescriptor = { id: string; label: string; alwaysOn?: boolean };
66
+ /** One draggable entry in an instrument's palette. */
49
67
  export type PaletteItem = { id: string; label: string; data?: unknown };
68
+ /** Whether a drop would be accepted at the current position, and why not if
69
+ * it would not. */
50
70
  export type DragFeedback = { ok: boolean; reason?: string };
51
71
 
72
+ /**
73
+ * An instrument: one self-contained interactive experiment a lab can host.
74
+ *
75
+ * It owns two pieces of data — `config`, the settings the control panel edits,
76
+ * and `state`, what the experiment is currently doing — and renders from both.
77
+ * The optional capability fields declare what else it wants from the runtime:
78
+ * a canvas, a layer list, palette drag-and-drop, undo. Declaring a capability
79
+ * is what makes the workspace provide the corresponding chrome.
80
+ */
52
81
  export interface Instrument<TS = unknown, TC = unknown> {
53
82
  name: string;
54
83
  defaultConfig: () => TC;
@@ -1,5 +1,7 @@
1
1
  import type { ConfigField, ConfigFieldType } from '../controls/types';
2
2
 
3
+ /** Whether a config schema is usable, and every problem found rather than
4
+ * just the first. */
3
5
  export interface ValidationResult {
4
6
  valid: boolean;
5
7
  errors: string[];
@@ -7,6 +9,9 @@ export interface ValidationResult {
7
9
 
8
10
  const KNOWN_TYPES: ConfigFieldType[] = ['slider', 'checkbox', 'select', 'number', 'text', 'color'];
9
11
 
12
+ /** Check a config schema for the mistakes that would otherwise surface as a
13
+ * silently broken control: empty or duplicate keys, unknown field types, and
14
+ * fields whose own constraints do not hold. */
10
15
  export function validateConfigSchema(fields: ConfigField[]): ValidationResult {
11
16
  const errors: string[] = [];
12
17
  const seenKeys = new Set<string>();
package/src/lab/Lab.tsx CHANGED
@@ -19,6 +19,7 @@ import { LabShell } from './LabShell';
19
19
  import { useResolvedMode } from './useSystemMode';
20
20
  import { WorkspaceGrid } from './WorkspaceGrid';
21
21
 
22
+ /** Props for `<Lab>`. */
22
23
  export interface LabProps {
23
24
  instruments: Instrument[];
24
25
  defaultInstrument: string;
@@ -76,6 +77,8 @@ function buildNebula(colors: readonly string[]): string {
76
77
  return blobs.join(', ');
77
78
  }
78
79
 
80
+ /** The lab runtime: creates the store, provides it, and renders one workspace
81
+ * per record in a grid. Each workspace runs one of `instruments`. */
79
82
  export function Lab({
80
83
  instruments,
81
84
  defaultInstrument,
@@ -2,6 +2,9 @@ import { createContext, useContext } from 'react';
2
2
  import type { Instrument } from '../instrument/types';
3
3
  import type { LabMode, SavedSnapshot, WorkspaceRecord } from '../state/types';
4
4
 
5
+ /** Lab-wide state and commands: the available instruments, the open
6
+ * workspaces and the operations over them, saved snapshots, and the color
7
+ * mode. */
5
8
  export interface LabContextValue {
6
9
  instruments: Instrument[];
7
10
  workspaces: WorkspaceRecord[];
@@ -17,8 +20,10 @@ export interface LabContextValue {
17
20
  setMode: (m: LabMode) => void;
18
21
  }
19
22
 
23
+ /** Context carrying the surrounding lab. Prefer `useLabContext`. */
20
24
  export const LabContext = createContext<LabContextValue | null>(null);
21
25
 
26
+ /** The surrounding lab. Throws outside a `<Lab>`. */
22
27
  export function useLabContext(): LabContextValue {
23
28
  const ctx = useContext(LabContext);
24
29
  if (ctx === null) {
@@ -4,6 +4,7 @@ import type { LabMode } from '../state/types';
4
4
  import { interstellarTheme } from '../theme/interstellar';
5
5
  import { useResolvedMode } from './useSystemMode';
6
6
 
7
+ /** Props for `<LabShell>`. */
7
8
  export interface LabShellProps {
8
9
  title: string;
9
10
  children: ReactNode;
@@ -15,6 +16,9 @@ export interface LabShellProps {
15
16
  mode?: LabMode;
16
17
  }
17
18
 
19
+ /** Page frame for a lab: a titled header, a body, and an optional footer,
20
+ * themed for the resolved color mode. Presentational only — use `<Lab>` when
21
+ * the workspace runtime is wanted too. */
18
22
  export function LabShell({ title, children, header, footer, mode = 'auto' }: LabShellProps) {
19
23
  const resolved = useResolvedMode(mode);
20
24
  const outer = useThemeOptional();
@@ -1,10 +1,12 @@
1
1
  import { Children, type CSSProperties, type ReactNode } from 'react';
2
2
  import { gridDims } from './gridDims';
3
3
 
4
+ /** Props for `<WorkspaceGrid>`. */
4
5
  export interface WorkspaceGridProps {
5
6
  children: ReactNode;
6
7
  }
7
8
 
9
+ /** Lays its children out in the most nearly square grid that fits them. */
8
10
  export function WorkspaceGrid({ children }: WorkspaceGridProps) {
9
11
  const count = Children.count(children);
10
12
  const { cols, rows } = gridDims(count);
@@ -1,8 +1,10 @@
1
+ /** A grid's column and row counts. */
1
2
  export interface GridDims {
2
3
  cols: number;
3
4
  rows: number;
4
5
  }
5
6
 
7
+ /** The most nearly square grid that fits `count` items. */
6
8
  export function gridDims(count: number): GridDims {
7
9
  if (count <= 1) return { cols: 1, rows: 1 };
8
10
  const cols = Math.ceil(Math.sqrt(count));
@@ -1,6 +1,7 @@
1
1
  import { type PointerEvent, useRef, useState } from 'react';
2
2
  import type { LayerDescriptor } from '../instrument/types';
3
3
 
4
+ /** Props for `<LayerList>`. */
4
5
  export interface LayerListProps {
5
6
  layers: LayerDescriptor[];
6
7
  visibility: Record<string, boolean>;
@@ -15,6 +16,8 @@ interface DragState {
15
16
  startY: number;
16
17
  }
17
18
 
19
+ /** A reorderable list of layers with per-layer visibility toggles. Layers
20
+ * marked `alwaysOn` are pinned and cannot be reordered or hidden. */
18
21
  export function LayerList({ layers, visibility, onReorder, onToggle, className }: LayerListProps) {
19
22
  const reorderable = layers.filter((l) => !l.alwaysOn);
20
23
  const pinned = layers.filter((l) => l.alwaysOn);
@@ -3,6 +3,7 @@ import { rollingAverage } from './fpsAverage';
3
3
 
4
4
  const SAMPLE_WINDOW = 30;
5
5
 
6
+ /** A live frame-rate readout, averaged over the last 30 frames. */
6
7
  export function FpsMeter() {
7
8
  const [fps, setFps] = useState(0);
8
9
  const samplesRef = useRef<number[]>([]);
@@ -1,6 +1,7 @@
1
1
  import { useContext } from 'react';
2
2
  import { CanvasStackContext } from '../canvas/CanvasStackContext';
3
3
 
4
+ /** Props for `<ScaleIndicator>`. */
4
5
  export interface ScaleIndicatorProps {
5
6
  /** Current view zoom factor. If omitted, reads from CanvasStackContext. Defaults to 1. */
6
7
  zoom?: number;
@@ -25,6 +26,9 @@ function niceNumber(n: number): number {
25
26
  return nice * 10 ** exp;
26
27
  }
27
28
 
29
+ /** A scale bar: a labeled rule showing how far a round number of world units
30
+ * is at the current zoom. Reads the zoom from the surrounding canvas stack
31
+ * unless one is passed. */
28
32
  export function ScaleIndicator({
29
33
  zoom,
30
34
  pixelsPerUnit = 1,
@@ -1,5 +1,6 @@
1
1
  import type { ReactNode } from 'react';
2
2
 
3
+ /** Props for `<Sidebar>`. */
3
4
  export interface SidebarProps {
4
5
  children: ReactNode;
5
6
  title?: string;
@@ -7,6 +8,8 @@ export interface SidebarProps {
7
8
  onToggle?: () => void;
8
9
  }
9
10
 
11
+ /** A collapsible side panel with an optional title. Collapse state is the
12
+ * caller's to hold. */
10
13
  export function Sidebar({ children, title, collapsed = false, onToggle }: SidebarProps) {
11
14
  const className = `lk-sidebar${collapsed ? ' lk-sidebar--collapsed' : ''}`;
12
15
  return (
@@ -1,9 +1,11 @@
1
1
  import type { ReactNode } from 'react';
2
2
 
3
+ /** Props for `<StatusBar>`. */
3
4
  export interface StatusBarProps {
4
5
  children: ReactNode;
5
6
  }
6
7
 
8
+ /** A footer strip for readouts. Fill it with `<StatusBar.Section>`. */
7
9
  export function StatusBar({ children }: StatusBarProps) {
8
10
  return <div className="lk-status-bar">{children}</div>;
9
11
  }
@@ -15,4 +17,5 @@ function Section({ children }: SectionProps) {
15
17
  return <span className="lk-status-bar-section">{children}</span>;
16
18
  }
17
19
 
20
+ /** One readout within a status bar. */
18
21
  StatusBar.Section = Section;
@@ -1,9 +1,12 @@
1
1
  import type { MouseEventHandler, ReactNode } from 'react';
2
2
 
3
+ /** Props for `<Toolbar>`. */
3
4
  export interface ToolbarProps {
4
5
  children: ReactNode;
5
6
  }
6
7
 
8
+ /** A horizontal bar of controls. Fill it with `<Toolbar.Title>`,
9
+ * `<Toolbar.Button>` and `<Toolbar.Spacer>`. */
7
10
  export function Toolbar({ children }: ToolbarProps) {
8
11
  return <div className="lk-toolbar">{children}</div>;
9
12
  }
@@ -39,6 +42,9 @@ function Spacer() {
39
42
  return <span className="lk-toolbar-spacer" aria-hidden="true" />;
40
43
  }
41
44
 
45
+ /** A label within a toolbar. */
42
46
  Toolbar.Title = Title;
47
+ /** A button within a toolbar. */
43
48
  Toolbar.Button = Button;
49
+ /** Flexible space that pushes what follows to the far end of the toolbar. */
44
50
  Toolbar.Spacer = Spacer;
@@ -5,6 +5,7 @@ import type { StorageAdapter } from './types';
5
5
 
6
6
  const SINGLETON_INSTRUMENT = '__singleton__';
7
7
 
8
+ /** Props for `<SingletonExperimentProvider>`. */
8
9
  export interface SingletonExperimentProviderProps<TS, TC> {
9
10
  /** Stable id for the synthetic workspace; also doubles as the
10
11
  * WorkspaceIdContext value. */
@@ -1,6 +1,8 @@
1
1
  import { decodeUrlHash, encodeUrlHash } from './helpers';
2
2
  import type { StorageAdapter } from './types';
3
3
 
4
+ /** Persist to `localStorage` — state survives a reload and a new tab. Falls
5
+ * back to no-ops when storage is unavailable. */
4
6
  export const localStorageAdapter: StorageAdapter = {
5
7
  read: (key) => {
6
8
  try {
@@ -25,6 +27,7 @@ export const localStorageAdapter: StorageAdapter = {
25
27
  },
26
28
  };
27
29
 
30
+ /** Persist to `sessionStorage` — state survives a reload but not a new tab. */
28
31
  export const sessionStorageAdapter: StorageAdapter = {
29
32
  read: (key) => {
30
33
  try {
@@ -68,6 +71,8 @@ function writeHashMap(map: Record<string, string>): void {
68
71
  window.history.replaceState(null, '', `#${encoded}`);
69
72
  }
70
73
 
74
+ /** Persist into the URL fragment, so the page's link carries its state and
75
+ * can be shared or bookmarked. */
71
76
  export const urlHashAdapter: StorageAdapter = {
72
77
  read: (key) => readHashMap()[key] ?? null,
73
78
  write: (key, value) => {
@@ -82,6 +87,8 @@ export const urlHashAdapter: StorageAdapter = {
82
87
  },
83
88
  };
84
89
 
90
+ /** An in-memory store, discarded on reload. For tests, and for labs that
91
+ * should start fresh every time. */
85
92
  export function createMemoryAdapter(): StorageAdapter {
86
93
  const store = new Map<string, string>();
87
94
  return {
@@ -95,6 +102,7 @@ export function createMemoryAdapter(): StorageAdapter {
95
102
  };
96
103
  }
97
104
 
105
+ /** Persists nothing and reads back nothing. */
98
106
  export const noneAdapter: StorageAdapter = {
99
107
  read: () => null,
100
108
  write: () => {},
@@ -5,8 +5,11 @@ import type { LabStoreState } from './types';
5
5
 
6
6
  type LabStoreCtx = { store: LabStore } | null;
7
7
 
8
+ /** Context carrying the lab store. Prefer `useLabStore`; this is exported for
9
+ * code that needs to read the context without subscribing. */
8
10
  export const LabStoreContext = createContext<LabStoreCtx>(null);
9
11
 
12
+ /** Provides a lab store to its subtree. */
10
13
  export function LabStoreProvider({
11
14
  store,
12
15
  children,
@@ -17,14 +20,18 @@ export function LabStoreProvider({
17
20
  return <LabStoreContext.Provider value={{ store }}>{children}</LabStoreContext.Provider>;
18
21
  }
19
22
 
23
+ /** Subscribe to the whole lab store. Throws outside a `<LabStoreProvider>`. */
20
24
  export function useLabStore(): LabStoreState & ReturnType<LabStore['getState']> {
21
25
  const ctx = useContext(LabStoreContext);
22
26
  if (!ctx) throw new Error('[labkit] useLabStore must be used inside <LabStoreProvider>');
23
27
  return useStore(ctx.store);
24
28
  }
25
29
 
30
+ /** Context carrying which workspace the subtree belongs to. */
26
31
  export const WorkspaceIdContext = createContext<string | null>(null);
27
32
 
33
+ /** Names the workspace its subtree belongs to, so an instrument's hooks can
34
+ * find their own record in the store without being passed an id. */
28
35
  export function WorkspaceIdProvider({
29
36
  workspaceId,
30
37
  children,
@@ -35,6 +42,8 @@ export function WorkspaceIdProvider({
35
42
  return <WorkspaceIdContext.Provider value={workspaceId}>{children}</WorkspaceIdContext.Provider>;
36
43
  }
37
44
 
45
+ /** The id of the workspace this component is inside. Throws outside a
46
+ * `<WorkspaceIdProvider>`. */
38
47
  export function useWorkspaceId(): string {
39
48
  const id = useContext(WorkspaceIdContext);
40
49
  if (!id) throw new Error('[labkit] useWorkspaceId must be used inside <WorkspaceIdProvider>');
@@ -1,5 +1,7 @@
1
1
  import type { InstrumentSerializers, UndoStack, WorkspaceRecord } from './types';
2
2
 
3
+ /** The storage key a lab writes one of its three buckets under. Namespaced by
4
+ * `storageKey` so two labs sharing an origin do not collide. */
3
5
  export function labStorageKey(
4
6
  storageKey: string,
5
7
  bucket: 'workspaces' | 'saves' | 'theme',
@@ -7,10 +9,13 @@ export function labStorageKey(
7
9
  return `lk:${storageKey}:${bucket}`;
8
10
  }
9
11
 
12
+ /** Encode a string for the URL fragment. */
10
13
  export function encodeUrlHash(value: string): string {
11
14
  return btoa(encodeURIComponent(value));
12
15
  }
13
16
 
17
+ /** Decode a URL fragment written by `encodeUrlHash`, or `null` if it is
18
+ * malformed. */
14
19
  export function decodeUrlHash(hash: string): string | null {
15
20
  if (!hash) return null;
16
21
  try {
@@ -20,12 +25,16 @@ export function decodeUrlHash(hash: string): string | null {
20
25
  }
21
26
  }
22
27
 
28
+ /** A fresh, empty undo history. */
23
29
  export function emptyUndoStack(): UndoStack {
24
30
  return { past: [], future: [] };
25
31
  }
26
32
 
27
33
  type SerializedRecord = Omit<WorkspaceRecord, 'undoStack'>;
28
34
 
35
+ /** Serialize workspaces for storage, running each instrument's own serializer
36
+ * over its state. Undo history is deliberately dropped — it does not survive
37
+ * a reload. */
29
38
  export function serializeWorkspaces(
30
39
  workspaces: WorkspaceRecord[],
31
40
  serializers: InstrumentSerializers,
@@ -40,6 +49,9 @@ export function serializeWorkspaces(
40
49
  return JSON.stringify(records);
41
50
  }
42
51
 
52
+ /** Rebuild workspaces from storage, running each instrument's deserializer
53
+ * over its state and starting each with an empty undo history. Returns an
54
+ * empty list rather than throwing on malformed input. */
43
55
  export function deserializeWorkspaces(
44
56
  raw: string,
45
57
  deserializers: InstrumentSerializers,
@@ -14,6 +14,8 @@ import type {
14
14
  WorkspaceRecord,
15
15
  } from './types';
16
16
 
17
+ /** Every mutation a lab store supports: managing workspaces, saving and
18
+ * restoring snapshots, and setting the color mode. */
17
19
  export interface LabStoreActions {
18
20
  addWorkspace: (record: Omit<WorkspaceRecord, 'undoStack'>) => void;
19
21
  removeWorkspace: (id: string) => void;
@@ -34,10 +36,14 @@ export interface LabStoreActions {
34
36
  setMode: (mode: LabMode) => void;
35
37
  }
36
38
 
39
+ /** A lab's store: its state and actions, plus the hook instruments use to
40
+ * register how their state is serialized. */
37
41
  export type LabStore = StoreApi<LabStoreState & LabStoreActions> & {
38
42
  registerSerializers: (s: InstrumentSerializers) => void;
39
43
  };
40
44
 
45
+ /** Build a lab store, hydrating from storage if anything was saved under the
46
+ * same key. Writes back are debounced. */
41
47
  export function createLabStore(options: CreateLabStoreOptions): LabStore {
42
48
  let serializers: InstrumentSerializers = {};
43
49
  let flushTimer: ReturnType<typeof setTimeout> | null = null;
@@ -1,8 +1,12 @@
1
+ /** A workspace's undo history, as snapshots of its state either side of the
2
+ * present. */
1
3
  export interface UndoStack {
2
4
  past: unknown[];
3
5
  future: unknown[];
4
6
  }
5
7
 
8
+ /** One workspace as the store holds it: which instrument it runs, that
9
+ * instrument's config and state, the camera, and the undo history. */
6
10
  export interface WorkspaceRecord<TS = unknown, TC = unknown> {
7
11
  id: string;
8
12
  instrumentName: string;
@@ -12,6 +16,8 @@ export interface WorkspaceRecord<TS = unknown, TC = unknown> {
12
16
  undoStack: UndoStack;
13
17
  }
14
18
 
19
+ /** A named, saved copy of a workspace's config and state, restorable into any
20
+ * workspace running the same instrument. */
15
21
  export interface SavedSnapshot {
16
22
  id: string;
17
23
  name: string;
@@ -25,18 +31,25 @@ export interface SavedSnapshot {
25
31
  /** `auto` follows the OS; the other two are an explicit choice. */
26
32
  export type LabMode = 'auto' | 'light' | 'dark';
27
33
 
34
+ /** Everything a lab persists: its workspaces, its saved snapshots, and the
35
+ * chosen color mode. */
28
36
  export interface LabStoreState {
29
37
  workspaces: WorkspaceRecord[];
30
38
  savedSnapshots: SavedSnapshot[];
31
39
  mode: LabMode;
32
40
  }
33
41
 
42
+ /** Where a lab persists itself. Implementations are keyed string storage and
43
+ * nothing more, so the same store works against localStorage, the URL hash,
44
+ * or memory. */
34
45
  export interface StorageAdapter {
35
46
  read(key: string): string | null;
36
47
  write(key: string, value: string): void;
37
48
  delete?(key: string): void;
38
49
  }
39
50
 
51
+ /** What `useExperimentState` hands an instrument: its state and config, with
52
+ * a setter for each. */
40
53
  export interface ExperimentStateHandle<TS, TC> {
41
54
  state: TS;
42
55
  setState: (next: TS | ((prev: TS) => TS)) => void;
@@ -44,12 +57,16 @@ export interface ExperimentStateHandle<TS, TC> {
44
57
  setConfig: (key: keyof TC, value: TC[keyof TC]) => void;
45
58
  }
46
59
 
60
+ /** Options for `createLabStore`. `storageKey` namespaces the keys written, so
61
+ * two labs on one origin do not collide. */
47
62
  export interface CreateLabStoreOptions {
48
63
  storageKey: string;
49
64
  storage: StorageAdapter;
50
65
  initialMode?: LabMode;
51
66
  }
52
67
 
68
+ /** Per-instrument serialize/deserialize hooks, keyed by instrument name. An
69
+ * instrument whose state is already JSON-safe needs no entry. */
53
70
  export type InstrumentSerializers = Record<
54
71
  string,
55
72
  { serialize?: (state: unknown) => unknown; deserialize?: (data: unknown) => unknown } | undefined
@@ -3,6 +3,9 @@ import { useStore } from 'zustand/react';
3
3
  import { LabStoreContext, WorkspaceIdContext } from './context';
4
4
  import type { ExperimentStateHandle } from './types';
5
5
 
6
+ /** An instrument's own state and config, plus setters. Reads the surrounding
7
+ * workspace id, so an instrument never has to know which workspace it is
8
+ * running in. Throws outside a lab store and workspace. */
6
9
  export function useExperimentState<TS = unknown, TC = unknown>(): ExperimentStateHandle<TS, TC> {
7
10
  const ctx = useContext(LabStoreContext);
8
11
  if (!ctx) throw new Error('[labkit] useExperimentState must be used inside <LabStoreProvider>');
@@ -1,6 +1,8 @@
1
1
  import { type CSSProperties, type ReactNode, type RefCallback, useEffect, useState } from 'react';
2
2
  import { dlog, useReorderDragList } from '../../passthrough/weasel-ui';
3
3
 
4
+ /** One card in a layer stack: its identity, the label shown when collapsed,
5
+ * and the optional select hoisted into its header. */
4
6
  export interface LayerStackItem {
5
7
  /** Stable id used for keys, onRemove, onReorder. Numeric to match
6
8
  * common id-from-nextId conventions; string ids also work. */
@@ -22,6 +24,7 @@ export interface LayerStackItem {
22
24
  defaultExpanded?: boolean;
23
25
  }
24
26
 
27
+ /** Props for `<LayerStack>`. */
25
28
  export interface LayerStackProps {
26
29
  title: string;
27
30
  items: LayerStackItem[];
@@ -38,6 +41,8 @@ export interface LayerStackProps {
38
41
  hideHead?: boolean;
39
42
  }
40
43
 
44
+ /** A drag-reorderable stack of expandable cards, with a palette in the header
45
+ * for adding more. The body of each card is the caller's to render. */
41
46
  export function LayerStack({
42
47
  title,
43
48
  items,
@@ -1,10 +1,13 @@
1
1
  import { useCallback, useMemo } from 'react';
2
2
  import { type ControlPoint, CurveEditor, dlog } from '../../passthrough/weasel-ui';
3
3
 
4
+ /** An annotation drawn over a curve plot: a shaded band across a range of x,
5
+ * or a vertical line at one x. */
4
6
  export type CurveMark =
5
7
  | { kind: 'band'; x: [number, number]; color?: string }
6
8
  | { kind: 'line'; x: number; color?: string };
7
9
 
10
+ /** Props for `<CurveField>`. */
8
11
  export interface CurveFieldProps {
9
12
  /** Flat [x0, y0, x1, y1, …] — matches how curve-as-array configs
10
13
  * serialize in JSON snapshots. */
@@ -91,7 +94,7 @@ export function CurveField({
91
94
  <div className="lk-curve-field__plot">
92
95
  <CurveEditor
93
96
  value={points}
94
- onChange={handleChange}
97
+ onInput={handleChange}
95
98
  domain="1d"
96
99
  constrain="function"
97
100
  xRange={[0, 1]}