@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
@@ -1,13 +1,12 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
  import {
3
3
  decodeUrlHash,
4
- deserializeWorkspaces,
4
+ deserializeTrials,
5
5
  emptyUndoStack,
6
6
  encodeUrlHash,
7
7
  labStorageKey,
8
- serializeWorkspaces,
8
+ serializeTrials,
9
9
  } from './helpers';
10
- import type { WorkspaceRecord } from './types';
11
10
 
12
11
  describe('labStorageKey', () => {
13
12
  it('produces namespaced keys', () => {
@@ -19,7 +18,7 @@ describe('labStorageKey', () => {
19
18
 
20
19
  describe('encodeUrlHash / decodeUrlHash', () => {
21
20
  it('round-trips a string', () => {
22
- const original = JSON.stringify({ workspaces: '[]', saves: '[]' });
21
+ const original = JSON.stringify({ trials: '[]', saves: '[]' });
23
22
  expect(decodeUrlHash(encodeUrlHash(original))).toBe(original);
24
23
  });
25
24
 
@@ -35,55 +34,84 @@ describe('emptyUndoStack', () => {
35
34
  });
36
35
  });
37
36
 
38
- describe('serializeWorkspaces / deserializeWorkspaces', () => {
39
- const ws: WorkspaceRecord = {
40
- id: 'w1',
41
- instrumentName: 'Test',
42
- config: { x: 1 },
43
- state: { items: [] },
44
- view: { zoom: 1, pan: { x: 0, y: 0 } },
45
- undoStack: { past: [1, 2], future: [] },
46
- };
47
-
48
- it('round-trips workspace records', () => {
49
- const serialized = serializeWorkspaces([ws], {});
50
- const [result] = deserializeWorkspaces(serialized, {});
51
- expect(result?.id).toBe('w1');
52
- expect(result?.state).toEqual({ items: [] });
53
- });
54
-
55
- it('strips undoStack on serialization', () => {
56
- const serialized = serializeWorkspaces([ws], {});
57
- const parsed = JSON.parse(serialized) as unknown[];
58
- expect((parsed[0] as Record<string, unknown>).undoStack).toBeUndefined();
37
+ describe('serializeTrials', () => {
38
+ it('returns records with the undo stack dropped', () => {
39
+ const records = serializeTrials(
40
+ [
41
+ {
42
+ id: 'w1',
43
+ instrumentName: 'Test',
44
+ config: { a: 1 },
45
+ state: { b: 2 },
46
+ view: { zoom: 1, pan: { x: 0, y: 0 } },
47
+ undoStack: { past: [{ b: 1 }], future: [] },
48
+ },
49
+ ],
50
+ {},
51
+ );
52
+ expect(records).toEqual([
53
+ {
54
+ id: 'w1',
55
+ instrumentName: 'Test',
56
+ config: { a: 1 },
57
+ state: { b: 2 },
58
+ view: { zoom: 1, pan: { x: 0, y: 0 } },
59
+ },
60
+ ]);
59
61
  });
60
62
 
61
- it('restores emptyUndoStack after deserialization', () => {
62
- const serialized = serializeWorkspaces([ws], {});
63
- const [result] = deserializeWorkspaces(serialized, {});
64
- expect(result?.undoStack).toEqual({ past: [], future: [] });
63
+ it('runs the instrument serializer over the state', () => {
64
+ const records = serializeTrials(
65
+ [
66
+ {
67
+ id: 'w1',
68
+ instrumentName: 'Test',
69
+ config: {},
70
+ state: { n: 2 },
71
+ view: { zoom: 1, pan: { x: 0, y: 0 } },
72
+ undoStack: { past: [], future: [] },
73
+ },
74
+ ],
75
+ { Test: { serialize: (s) => ({ doubled: (s as { n: number }).n * 2 }) } },
76
+ );
77
+ expect(records[0].state).toEqual({ doubled: 4 });
65
78
  });
79
+ });
66
80
 
67
- it('uses instrument.serialize / deserialize when provided', () => {
68
- const serializers = {
69
- Test: {
70
- serialize: (s: unknown) => ({ compressed: true, data: s }),
71
- deserialize: (d: unknown) => (d as { data: unknown }).data,
72
- },
73
- };
74
- const serialized = serializeWorkspaces([ws], serializers);
75
- const [result] = deserializeWorkspaces(serialized, serializers);
76
- expect(result?.state).toEqual({ items: [] });
81
+ describe('deserializeTrials', () => {
82
+ it('rebuilds records with an empty undo stack', () => {
83
+ const out = deserializeTrials(
84
+ [
85
+ {
86
+ id: 'w1',
87
+ instrumentName: 'Test',
88
+ config: {},
89
+ state: { n: 1 },
90
+ view: { zoom: 1, pan: { x: 0, y: 0 } },
91
+ },
92
+ ],
93
+ {},
94
+ );
95
+ expect(out[0].undoStack).toEqual({ past: [], future: [] });
77
96
  });
78
97
 
79
- it('falls back to identity when instrumentName not in registry', () => {
80
- const serialized = serializeWorkspaces([ws], {});
81
- const [result] = deserializeWorkspaces(serialized, {});
82
- expect(result?.state).toEqual({ items: [] });
98
+ it('runs the instrument deserializer over the state', () => {
99
+ const out = deserializeTrials(
100
+ [
101
+ {
102
+ id: 'w1',
103
+ instrumentName: 'Test',
104
+ config: {},
105
+ state: { doubled: 4 },
106
+ view: { zoom: 1, pan: { x: 0, y: 0 } },
107
+ },
108
+ ],
109
+ { Test: { deserialize: (d) => ({ n: (d as { doubled: number }).doubled / 2 }) } },
110
+ );
111
+ expect(out[0].state).toEqual({ n: 2 });
83
112
  });
84
113
 
85
- it('returns empty array for malformed JSON', () => {
86
- const result = deserializeWorkspaces('NOT JSON', {});
87
- expect(result).toEqual([]);
114
+ it('returns an empty list when given something that is not an array', () => {
115
+ expect(deserializeTrials(undefined as never, {})).toEqual([]);
88
116
  });
89
117
  });
@@ -1,4 +1,4 @@
1
- import type { InstrumentSerializers, UndoStack, WorkspaceRecord } from './types';
1
+ import type { InstrumentSerializers, SerializedTrial, TrialRecord, UndoStack } from './types';
2
2
 
3
3
  /** The storage key a lab writes one of its buckets under. Namespaced by
4
4
  * `storageKey` so two labs sharing an origin do not collide. */
@@ -30,44 +30,36 @@ export function emptyUndoStack(): UndoStack {
30
30
  return { past: [], future: [] };
31
31
  }
32
32
 
33
- type SerializedRecord = Omit<WorkspaceRecord, 'undoStack'>;
34
-
35
- /** Serialize workspaces for storage, running each instrument's own serializer
33
+ /** Serialize trials for storage, running each instrument's own serializer
36
34
  * over its state. Undo history is deliberately dropped — it does not survive
37
35
  * a reload. */
38
- export function serializeWorkspaces(
39
- workspaces: WorkspaceRecord[],
36
+ export function serializeTrials(
37
+ trials: TrialRecord[],
40
38
  serializers: InstrumentSerializers,
41
- ): string {
42
- const records: SerializedRecord[] = workspaces.map(({ undoStack: _undo, ...w }) => {
39
+ ): SerializedTrial[] {
40
+ return trials.map(({ undoStack: _undo, ...w }) => {
43
41
  const s = serializers[w.instrumentName];
44
- return {
45
- ...w,
46
- state: s?.serialize ? s.serialize(w.state) : w.state,
47
- };
42
+ return { ...w, state: s?.serialize ? s.serialize(w.state) : w.state };
48
43
  });
49
- return JSON.stringify(records);
50
44
  }
51
45
 
52
- /** Rebuild workspaces from storage, running each instrument's deserializer
46
+ /** Rebuild trials from storage, running each instrument's deserializer
53
47
  * over its state and starting each with an empty undo history. Returns an
54
48
  * empty list rather than throwing on malformed input. */
55
- export function deserializeWorkspaces(
56
- raw: string,
49
+ export function deserializeTrials(
50
+ records: SerializedTrial[],
57
51
  deserializers: InstrumentSerializers,
58
- ): WorkspaceRecord[] {
59
- try {
60
- const records = JSON.parse(raw) as SerializedRecord[];
61
- return records.map((r) => {
62
- const d = deserializers[r.instrumentName];
63
- return {
64
- ...r,
65
- state: d?.deserialize ? d.deserialize(r.state) : r.state,
66
- undoStack: emptyUndoStack(),
67
- };
68
- });
69
- } catch {
70
- console.warn('[labkit] deserializeWorkspaces: failed to parse, returning empty list');
52
+ ): TrialRecord[] {
53
+ if (!Array.isArray(records)) {
54
+ console.warn('[labkit] deserializeTrials: not an array, returning empty list');
71
55
  return [];
72
56
  }
57
+ return records.map((r) => {
58
+ const d = deserializers[r.instrumentName];
59
+ return {
60
+ ...r,
61
+ state: d?.deserialize ? d.deserialize(r.state) : r.state,
62
+ undoStack: emptyUndoStack(),
63
+ };
64
+ });
73
65
  }
@@ -8,18 +8,23 @@ export {
8
8
  export {
9
9
  LabStoreContext,
10
10
  LabStoreProvider,
11
+ TrialIdContext,
12
+ TrialIdProvider,
11
13
  useLabStore,
12
- useWorkspaceId,
13
- WorkspaceIdContext,
14
- WorkspaceIdProvider,
14
+ useTrialId,
15
15
  } from './context';
16
+ export {
17
+ CURRENT_DOCUMENT_VERSION,
18
+ labDocumentKey,
19
+ quarantineKey,
20
+ } from './document';
16
21
  export {
17
22
  decodeUrlHash,
18
- deserializeWorkspaces,
23
+ deserializeTrials,
19
24
  emptyUndoStack,
20
25
  encodeUrlHash,
21
26
  labStorageKey,
22
- serializeWorkspaces,
27
+ serializeTrials,
23
28
  } from './helpers';
24
29
  export {
25
30
  SingletonExperimentProvider,
@@ -29,12 +34,15 @@ export type { LabStore, LabStoreActions } from './store';
29
34
  export { createLabStore } from './store';
30
35
  export type {
31
36
  CreateLabStoreOptions,
32
- ExperimentStateHandle,
33
37
  InstrumentSerializers,
38
+ LabDocument,
34
39
  LabStoreState,
40
+ Migration,
35
41
  SavedSnapshot,
42
+ SerializedTrial,
36
43
  StorageAdapter,
44
+ TrialRecord,
45
+ TrialStateHandle,
37
46
  UndoStack,
38
- WorkspaceRecord,
39
47
  } from './types';
40
- export { useExperimentState } from './useExperimentState';
48
+ export { useTrialState } from './useTrialState';