@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
@@ -41,50 +41,50 @@ function mountLab(props: Partial<Parameters<typeof Lab>[0]> = {}) {
41
41
  }
42
42
 
43
43
  describe('<Lab>', () => {
44
- it('renders one workspace by default', () => {
44
+ it('renders one trial by default', () => {
45
45
  mountLab();
46
- expect(screen.getAllByRole('region', { name: /workspace/i })).toHaveLength(1);
46
+ expect(screen.getAllByRole('region', { name: /trial/i })).toHaveLength(1);
47
47
  });
48
48
 
49
- it('addWorkspace adds a second workspace', () => {
49
+ it('addTrial adds a second trial', () => {
50
50
  mountLab();
51
- act(() => labRef?.addWorkspace('Stub'));
52
- expect(screen.getAllByRole('region', { name: /workspace/i })).toHaveLength(2);
51
+ act(() => labRef?.addTrial('Stub'));
52
+ expect(screen.getAllByRole('region', { name: /trial/i })).toHaveLength(2);
53
53
  });
54
54
 
55
- it('closeWorkspace removes one when more than one exists', () => {
55
+ it('closeTrial removes one when more than one exists', () => {
56
56
  mountLab();
57
- act(() => labRef?.addWorkspace('Stub'));
58
- const first = labRef?.workspaces[0];
59
- act(() => labRef?.closeWorkspace(first?.id ?? ''));
60
- expect(screen.getAllByRole('region', { name: /workspace/i })).toHaveLength(1);
57
+ act(() => labRef?.addTrial('Stub'));
58
+ const first = labRef?.trials[0];
59
+ act(() => labRef?.closeTrial(first?.id ?? ''));
60
+ expect(screen.getAllByRole('region', { name: /trial/i })).toHaveLength(1);
61
61
  });
62
62
 
63
- it('closeWorkspace is a no-op on the last workspace', () => {
63
+ it('closeTrial is a no-op on the last trial', () => {
64
64
  mountLab();
65
- const only = labRef?.workspaces[0];
66
- act(() => labRef?.closeWorkspace(only?.id ?? ''));
67
- expect(screen.getAllByRole('region', { name: /workspace/i })).toHaveLength(1);
65
+ const only = labRef?.trials[0];
66
+ act(() => labRef?.closeTrial(only?.id ?? ''));
67
+ expect(screen.getAllByRole('region', { name: /trial/i })).toHaveLength(1);
68
68
  });
69
69
 
70
- it('cloneWorkspace inserts immediately after source', () => {
70
+ it('cloneTrial inserts immediately after source', () => {
71
71
  mountLab();
72
- act(() => labRef?.addWorkspace('StubB'));
73
- const ws0 = labRef?.workspaces[0];
74
- const ws1 = labRef?.workspaces[1];
75
- act(() => labRef?.cloneWorkspace(ws0?.id ?? ''));
76
- const ids = labRef?.workspaces.map((w) => w.id) ?? [];
72
+ act(() => labRef?.addTrial('StubB'));
73
+ const ws0 = labRef?.trials[0];
74
+ const ws1 = labRef?.trials[1];
75
+ act(() => labRef?.cloneTrial(ws0?.id ?? ''));
76
+ const ids = labRef?.trials.map((w) => w.id) ?? [];
77
77
  expect(ids).toHaveLength(3);
78
78
  expect(ids[0]).toBe(ws0?.id);
79
79
  expect(ids[2]).toBe(ws1?.id);
80
80
  });
81
81
 
82
- it('resetWorkspace restores defaults', () => {
82
+ it('resetTrial restores defaults', () => {
83
83
  mountLab();
84
- const ws = labRef?.workspaces[0];
85
- if (!ws) throw new Error('no workspace');
86
- act(() => labRef?.resetWorkspace(ws.id));
87
- const reset = labRef?.workspaces[0];
84
+ const ws = labRef?.trials[0];
85
+ if (!ws) throw new Error('no trial');
86
+ act(() => labRef?.resetTrial(ws.id));
87
+ const reset = labRef?.trials[0];
88
88
  expect(reset?.config).toEqual({ count: 0 });
89
89
  expect(reset?.state).toEqual({ value: 0 });
90
90
  });
package/src/lab/Lab.tsx CHANGED
@@ -1,28 +1,28 @@
1
1
  import { ThemeProvider } from '@weasel-js/theme/react';
2
2
  import { type CSSProperties, type ReactNode, useEffect, useMemo, useRef } from 'react';
3
3
  import { useStore } from 'zustand/react';
4
- import type { Instrument } from '../instrument/types';
4
+ import type { InstrumentList } from '../instrument/types';
5
5
  import { noneAdapter } from '../state/adapters';
6
6
  import { LabStoreContext } from '../state/context';
7
7
  import { createLabStore, type LabStore } from '../state/store';
8
- import type { LabMode, StorageAdapter, WorkspaceRecord } from '../state/types';
8
+ import type { LabMode, StorageAdapter, TrialRecord } from '../state/types';
9
9
  import { interstellarTheme } from '../theme/interstellar';
10
- import { Workspace } from '../workspace/Workspace';
10
+ import { Trial } from '../trial/Trial';
11
11
  import {
12
- addWorkspace as addWorkspaceOp,
13
- cloneWorkspace as cloneWorkspaceOp,
14
- closeWorkspace as closeWorkspaceOp,
15
- reorderWorkspaces as reorderWorkspacesOp,
16
- resetWorkspace as resetWorkspaceOp,
17
- } from '../workspace/workspaceOps';
12
+ addTrial as addTrialOp,
13
+ cloneTrial as cloneTrialOp,
14
+ closeTrial as closeTrialOp,
15
+ reorderTrials as reorderTrialsOp,
16
+ resetTrial as resetTrialOp,
17
+ } from '../trial/trialOps';
18
18
  import { LabContext, type LabContextValue } from './LabContext';
19
19
  import { LabShell } from './LabShell';
20
20
  import { useResolvedMode } from './useSystemMode';
21
- import { WorkspaceGrid, type WorkspaceLayout } from './WorkspaceGrid';
21
+ import { type TrialLayout, Workspace } from './Workspace';
22
22
 
23
23
  /** Props for `<Lab>`. */
24
24
  export interface LabProps {
25
- instruments: Instrument[];
25
+ instruments: InstrumentList;
26
26
  defaultInstrument: string;
27
27
  storage?: StorageAdapter | null;
28
28
  storageKey?: string;
@@ -39,19 +39,19 @@ export interface LabProps {
39
39
  }
40
40
 
41
41
  function buildStore(
42
- instruments: Instrument[],
42
+ instruments: InstrumentList,
43
43
  defaultInstrument: string,
44
44
  storage: StorageAdapter,
45
45
  storageKey: string,
46
46
  initialMode: LabMode,
47
47
  ): LabStore {
48
48
  const store = createLabStore({ storageKey, storage, initialMode });
49
- if (store.getState().workspaces.length === 0) {
50
- const seeded = addWorkspaceOp([], instruments, defaultInstrument);
49
+ if (store.getState().trials.length === 0) {
50
+ const seeded = addTrialOp([], instruments, defaultInstrument);
51
51
  const record = seeded[0];
52
52
  if (record) {
53
53
  const { undoStack: _undoStack, ...rest } = record;
54
- store.getState().addWorkspace(rest);
54
+ store.getState().addTrial(rest);
55
55
  }
56
56
  }
57
57
  return store;
@@ -71,15 +71,15 @@ const NEBULA_SLOTS = [
71
71
 
72
72
  function buildNebula(colors: readonly string[]): string {
73
73
  const blobs = colors.map((c, i) => {
74
- const p = NEBULA_SLOTS[i % NEBULA_SLOTS.length]!;
74
+ const p = NEBULA_SLOTS[i % NEBULA_SLOTS.length];
75
75
  return `radial-gradient(ellipse ${p.sx} ${p.sy} at ${p.cx} ${p.cy}, color-mix(in srgb, ${c} 22%, transparent), transparent ${p.stop})`;
76
76
  });
77
77
  blobs.push('radial-gradient(ellipse at center, #0a0a18 0%, #02020a 100%)');
78
78
  return blobs.join(', ');
79
79
  }
80
80
 
81
- /** The lab runtime: creates the store, provides it, and renders one workspace
82
- * per record in a grid. Each workspace runs one of `instruments`. */
81
+ /** The lab runtime: creates the store, provides it, and renders one trial
82
+ * per record in a grid. Each trial runs one of `instruments`. */
83
83
  export function Lab({
84
84
  instruments,
85
85
  defaultInstrument,
@@ -106,7 +106,7 @@ export function Lab({
106
106
  }
107
107
  const store = storeRef.current;
108
108
 
109
- const workspaces = useStore(store, (s) => s.workspaces);
109
+ const trials = useStore(store, (s) => s.trials);
110
110
  const savedSnapshots = useStore(store, (s) => s.savedSnapshots);
111
111
  const modeValue = useStore(store, (s) => s.mode);
112
112
  const layout = useStore(store, (s) => s.layout);
@@ -119,51 +119,51 @@ export function Lab({
119
119
  }, [mode, store]);
120
120
 
121
121
  const contextValue = useMemo<LabContextValue>(() => {
122
- const replaceWorkspaces = (next: WorkspaceRecord[]): void => {
123
- const currentById = new Map(store.getState().workspaces.map((w) => [w.id, w]));
122
+ const replaceTrials = (next: TrialRecord[]): void => {
123
+ const currentById = new Map(store.getState().trials.map((w) => [w.id, w]));
124
124
  const merged = next.map((w) => currentById.get(w.id) ?? w);
125
- store.setState({ workspaces: merged });
125
+ store.setState({ trials: merged });
126
126
  // Trigger persistence flush via a tracked action.
127
127
  store.getState().setMode(store.getState().mode);
128
128
  };
129
129
 
130
130
  return {
131
131
  instruments,
132
- workspaces,
133
- addWorkspace: (instrumentName) => {
134
- const next = addWorkspaceOp(store.getState().workspaces, instruments, instrumentName);
135
- replaceWorkspaces(next);
132
+ trials,
133
+ addTrial: (instrumentName) => {
134
+ const next = addTrialOp(store.getState().trials, instruments, instrumentName);
135
+ replaceTrials(next);
136
136
  },
137
- cloneWorkspace: (id) => {
138
- const next = cloneWorkspaceOp(store.getState().workspaces, id);
139
- replaceWorkspaces(next);
137
+ cloneTrial: (id) => {
138
+ const next = cloneTrialOp(store.getState().trials, id);
139
+ replaceTrials(next);
140
140
  },
141
- closeWorkspace: (id) => {
142
- const next = closeWorkspaceOp(store.getState().workspaces, id);
143
- replaceWorkspaces(next);
141
+ closeTrial: (id) => {
142
+ const next = closeTrialOp(store.getState().trials, id);
143
+ replaceTrials(next);
144
144
  },
145
- reorderWorkspaces: (ids) => {
146
- replaceWorkspaces(reorderWorkspacesOp(store.getState().workspaces, ids));
145
+ reorderTrials: (ids) => {
146
+ replaceTrials(reorderTrialsOp(store.getState().trials, ids));
147
147
  },
148
- resetWorkspace: (id) => {
149
- const next = resetWorkspaceOp(store.getState().workspaces, id, instruments);
148
+ resetTrial: (id) => {
149
+ const next = resetTrialOp(store.getState().trials, id, instruments);
150
150
  const record = next.find((w) => w.id === id);
151
151
  if (!record) return;
152
152
  store.setState((s) => ({
153
- workspaces: s.workspaces.map((w) =>
153
+ trials: s.trials.map((w) =>
154
154
  w.id === id
155
155
  ? { ...w, config: record.config, state: record.state, view: record.view }
156
156
  : w,
157
157
  ),
158
158
  }));
159
- store.getState().updateWorkspaceView(id, record.view);
159
+ store.getState().updateTrialView(id, record.view);
160
160
  },
161
161
  savedSnapshots,
162
- saveSnapshot: (workspaceId, name) => {
163
- store.getState().saveSnapshot(workspaceId, name ?? `Save ${new Date().toLocaleString()}`);
162
+ saveSnapshot: (trialId, name) => {
163
+ store.getState().saveSnapshot(trialId, name ?? `Save ${new Date().toLocaleString()}`);
164
164
  },
165
- loadSnapshot: (workspaceId, snapshotId) => {
166
- store.getState().loadSnapshot(snapshotId, workspaceId);
165
+ loadSnapshot: (trialId, snapshotId) => {
166
+ store.getState().loadSnapshot(snapshotId, trialId);
167
167
  },
168
168
  deleteSnapshot: (snapshotId) => {
169
169
  store.getState().deleteSnapshot(snapshotId);
@@ -173,7 +173,7 @@ export function Lab({
173
173
  store.getState().setMode(m);
174
174
  },
175
175
  };
176
- }, [instruments, workspaces, savedSnapshots, modeValue, store]);
176
+ }, [instruments, trials, savedSnapshots, modeValue, store]);
177
177
 
178
178
  // Only override the backdrop in dark, where there is one to override.
179
179
  // Setting a CSS custom property is the sanctioned use of inline style.
@@ -192,18 +192,18 @@ export function Lab({
192
192
  style={backdropStyle}
193
193
  >
194
194
  <LabShell title={title ?? 'Labkit'} mode={modeValue} header={children}>
195
- <WorkspaceGrid
196
- ids={workspaces.map((w) => w.id)}
195
+ <Workspace
196
+ ids={trials.map((w) => w.id)}
197
197
  resizable
198
198
  reorderable
199
- onReorder={(ids) => contextValue.reorderWorkspaces(ids)}
200
- layout={layout as WorkspaceLayout}
199
+ onReorder={(ids) => contextValue.reorderTrials(ids)}
200
+ layout={layout as TrialLayout}
201
201
  onLayoutChange={(next) => store.getState().setLayout(next)}
202
202
  >
203
- {workspaces.map((w) => (
204
- <Workspace key={w.id} id={w.id} />
203
+ {trials.map((w) => (
204
+ <Trial key={w.id} id={w.id} />
205
205
  ))}
206
- </WorkspaceGrid>
206
+ </Workspace>
207
207
  </LabShell>
208
208
  </ThemeProvider>
209
209
  </LabContext.Provider>
@@ -1,21 +1,21 @@
1
1
  import { createContext, useContext } from 'react';
2
- import type { Instrument } from '../instrument/types';
3
- import type { LabMode, SavedSnapshot, WorkspaceRecord } from '../state/types';
2
+ import type { InstrumentList } from '../instrument/types';
3
+ import type { LabMode, SavedSnapshot, TrialRecord } from '../state/types';
4
4
 
5
5
  /** Lab-wide state and commands: the available instruments, the open
6
- * workspaces and the operations over them, saved snapshots, and the color
6
+ * trials and the operations over them, saved snapshots, and the color
7
7
  * mode. */
8
8
  export interface LabContextValue {
9
- instruments: Instrument[];
10
- workspaces: WorkspaceRecord[];
11
- addWorkspace: (instrumentName: string) => void;
12
- cloneWorkspace: (id: string) => void;
13
- closeWorkspace: (id: string) => void;
14
- resetWorkspace: (id: string) => void;
15
- reorderWorkspaces: (ids: readonly string[]) => void;
9
+ instruments: InstrumentList;
10
+ trials: TrialRecord[];
11
+ addTrial: (instrumentName: string) => void;
12
+ cloneTrial: (id: string) => void;
13
+ closeTrial: (id: string) => void;
14
+ resetTrial: (id: string) => void;
15
+ reorderTrials: (ids: readonly string[]) => void;
16
16
  savedSnapshots: SavedSnapshot[];
17
- saveSnapshot: (workspaceId: string, name?: string) => void;
18
- loadSnapshot: (workspaceId: string, snapshotId: string) => void;
17
+ saveSnapshot: (trialId: string, name?: string) => void;
18
+ loadSnapshot: (trialId: string, snapshotId: string) => void;
19
19
  deleteSnapshot: (snapshotId: string) => void;
20
20
  mode: LabMode;
21
21
  setMode: (m: LabMode) => void;
@@ -18,7 +18,7 @@ export interface LabShellProps {
18
18
 
19
19
  /** Page frame for a lab: a titled header, a body, and an optional footer,
20
20
  * themed for the resolved color mode. Presentational only — use `<Lab>` when
21
- * the workspace runtime is wanted too. */
21
+ * the trial runtime is wanted too. */
22
22
  export function LabShell({ title, children, header, footer, mode = 'auto' }: LabShellProps) {
23
23
  const resolved = useResolvedMode(mode);
24
24
  const outer = useThemeOptional();
@@ -1,13 +1,13 @@
1
1
  // Tiles are positioned by windease at strategy-computed rects; `.windease-zone`
2
2
  // (from windease/styles.css) supplies the containing block and the 100% box.
3
3
  // This class exists for consumers to target.
4
- .lk-workspace-grid {
4
+ .lk-workspace {
5
5
  min-height: 0;
6
6
  }
7
7
 
8
8
  // Only rendered when `reorderable` — a tile is full of controls, so the drag
9
9
  // target has to be its own strip rather than the whole pane.
10
- .lk-workspace-tile {
10
+ .lk-trial-tile {
11
11
  display: flex;
12
12
  flex-direction: column;
13
13
  height: 100%;
@@ -1,13 +1,13 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react-vite';
2
- import { WorkspaceGrid } from './WorkspaceGrid';
2
+ import { Workspace } from './Workspace';
3
3
 
4
- const meta: Meta<typeof WorkspaceGrid> = {
5
- title: 'labkit/Lab/WorkspaceGrid',
6
- component: WorkspaceGrid,
4
+ const meta: Meta<typeof Workspace> = {
5
+ title: 'labkit/Lab/Workspace',
6
+ component: Workspace,
7
7
  };
8
8
  export default meta;
9
9
 
10
- type Story = StoryObj<typeof WorkspaceGrid>;
10
+ type Story = StoryObj<typeof Workspace>;
11
11
 
12
12
  const Tile = ({ children }: { children: React.ReactNode }) => (
13
13
  <div
@@ -28,9 +28,9 @@ const Tile = ({ children }: { children: React.ReactNode }) => (
28
28
  export const OneTile: Story = {
29
29
  render: () => (
30
30
  <div style={{ height: 400 }}>
31
- <WorkspaceGrid>
31
+ <Workspace>
32
32
  <Tile>1</Tile>
33
- </WorkspaceGrid>
33
+ </Workspace>
34
34
  </div>
35
35
  ),
36
36
  };
@@ -38,11 +38,11 @@ export const OneTile: Story = {
38
38
  export const ThreeTiles: Story = {
39
39
  render: () => (
40
40
  <div style={{ height: 400 }}>
41
- <WorkspaceGrid>
41
+ <Workspace>
42
42
  <Tile>1</Tile>
43
43
  <Tile>2</Tile>
44
44
  <Tile>3</Tile>
45
- </WorkspaceGrid>
45
+ </Workspace>
46
46
  </div>
47
47
  ),
48
48
  };
@@ -50,12 +50,12 @@ export const ThreeTiles: Story = {
50
50
  export const SevenTiles: Story = {
51
51
  render: () => (
52
52
  <div style={{ height: 600 }}>
53
- <WorkspaceGrid>
53
+ <Workspace>
54
54
  {Array.from({ length: 7 }).map((_, i) => (
55
55
  // biome-ignore lint/suspicious/noArrayIndexKey: static demo list
56
56
  <Tile key={i}>{i + 1}</Tile>
57
57
  ))}
58
- </WorkspaceGrid>
58
+ </Workspace>
59
59
  </div>
60
60
  ),
61
61
  };
@@ -65,13 +65,13 @@ export const SevenTiles: Story = {
65
65
  export const Resizable: Story = {
66
66
  render: () => (
67
67
  <div style={{ height: 500 }}>
68
- <WorkspaceGrid ids={['a', 'b', 'c', 'd', 'e']} resizable>
68
+ <Workspace ids={['a', 'b', 'c', 'd', 'e']} resizable>
69
69
  <Tile>1</Tile>
70
70
  <Tile>2</Tile>
71
71
  <Tile>3</Tile>
72
72
  <Tile>4</Tile>
73
73
  <Tile>5</Tile>
74
- </WorkspaceGrid>
74
+ </Workspace>
75
75
  </div>
76
76
  ),
77
77
  };
@@ -1,17 +1,17 @@
1
1
  import { render, screen } from '@testing-library/react';
2
2
  import { describe, expect, test, vi } from 'vitest';
3
- import { WorkspaceGrid } from './WorkspaceGrid';
3
+ import { Workspace } from './Workspace';
4
4
 
5
5
  const VIEWPORT = { w: 800, h: 600 };
6
6
 
7
- describe('WorkspaceGrid', () => {
7
+ describe('Workspace', () => {
8
8
  test('renders all children', () => {
9
9
  render(
10
- <WorkspaceGrid viewport={VIEWPORT}>
10
+ <Workspace viewport={VIEWPORT}>
11
11
  <div>one</div>
12
12
  <div>two</div>
13
13
  <div>three</div>
14
- </WorkspaceGrid>,
14
+ </Workspace>,
15
15
  );
16
16
  expect(screen.getByText('one')).toBeInTheDocument();
17
17
  expect(screen.getByText('two')).toBeInTheDocument();
@@ -21,54 +21,54 @@ describe('WorkspaceGrid', () => {
21
21
  test('keeps a child with the tile its id names when an earlier one closes', () => {
22
22
  const ids = ['a', 'b', 'c'];
23
23
  const { rerender, container } = render(
24
- <WorkspaceGrid ids={ids} viewport={VIEWPORT}>
24
+ <Workspace ids={ids} viewport={VIEWPORT}>
25
25
  <div>a</div>
26
26
  <div>b</div>
27
27
  <div>c</div>
28
- </WorkspaceGrid>,
28
+ </Workspace>,
29
29
  );
30
30
  expect(container.querySelector('[data-node="c"]')).toHaveTextContent('c');
31
31
 
32
32
  rerender(
33
- <WorkspaceGrid ids={['b', 'c']} viewport={VIEWPORT}>
33
+ <Workspace ids={['b', 'c']} viewport={VIEWPORT}>
34
34
  <div>b</div>
35
35
  <div>c</div>
36
- </WorkspaceGrid>,
36
+ </Workspace>,
37
37
  );
38
38
  expect(container.querySelector('[data-node="c"]')).toHaveTextContent('c');
39
39
  expect(container.querySelector('[data-node="a"]')).toBeNull();
40
40
  });
41
41
 
42
- test('uses lk-workspace-grid class', () => {
42
+ test('uses lk-workspace class', () => {
43
43
  const { container } = render(
44
- <WorkspaceGrid viewport={VIEWPORT}>
44
+ <Workspace viewport={VIEWPORT}>
45
45
  <div />
46
- </WorkspaceGrid>,
46
+ </Workspace>,
47
47
  );
48
- expect((container.firstChild as HTMLElement).className).toContain('lk-workspace-grid');
48
+ expect((container.firstChild as HTMLElement).className).toContain('lk-workspace');
49
49
  });
50
50
 
51
51
  test('renders resize affordances only when resizable', () => {
52
52
  const { container, rerender } = render(
53
- <WorkspaceGrid ids={['a', 'b']} viewport={VIEWPORT}>
53
+ <Workspace ids={['a', 'b']} viewport={VIEWPORT}>
54
54
  <div>a</div>
55
55
  <div>b</div>
56
- </WorkspaceGrid>,
56
+ </Workspace>,
57
57
  );
58
58
  expect(container.querySelectorAll('[role="separator"]')).toHaveLength(0);
59
59
 
60
60
  rerender(
61
- <WorkspaceGrid ids={['a', 'b']} resizable viewport={VIEWPORT}>
61
+ <Workspace ids={['a', 'b']} resizable viewport={VIEWPORT}>
62
62
  <div>a</div>
63
63
  <div>b</div>
64
- </WorkspaceGrid>,
64
+ </Workspace>,
65
65
  );
66
66
  expect(container.querySelectorAll('[role="separator"]').length).toBeGreaterThan(0);
67
67
  });
68
68
 
69
69
  test('applies a saved extent to a tile as it registers', () => {
70
70
  const { container } = render(
71
- <WorkspaceGrid
71
+ <Workspace
72
72
  ids={['a', 'b']}
73
73
  resizable
74
74
  viewport={VIEWPORT}
@@ -76,7 +76,7 @@ describe('WorkspaceGrid', () => {
76
76
  >
77
77
  <div>a</div>
78
78
  <div>b</div>
79
- </WorkspaceGrid>,
79
+ </Workspace>,
80
80
  );
81
81
  const a = container.querySelector('[data-node="a"]') as HTMLElement;
82
82
  const b = container.querySelector('[data-node="b"]') as HTMLElement;
@@ -87,14 +87,14 @@ describe('WorkspaceGrid', () => {
87
87
  test('reports the order a drop would produce instead of applying it', () => {
88
88
  const onReorder = vi.fn();
89
89
  const { container } = render(
90
- <WorkspaceGrid ids={['a', 'b']} reorderable onReorder={onReorder} viewport={VIEWPORT}>
90
+ <Workspace ids={['a', 'b']} reorderable onReorder={onReorder} viewport={VIEWPORT}>
91
91
  <div>a</div>
92
92
  <div>b</div>
93
- </WorkspaceGrid>,
93
+ </Workspace>,
94
94
  );
95
95
  // The grid is controlled: it renders a handle per tile and commits nothing
96
96
  // itself. Order still comes from `ids`.
97
- expect(container.querySelectorAll('.lk-workspace-tile__grip')).toHaveLength(2);
97
+ expect(container.querySelectorAll('.lk-trial-tile__grip')).toHaveLength(2);
98
98
  expect(onReorder).not.toHaveBeenCalled();
99
99
  const nodes = [...container.querySelectorAll('[data-node]')].map((el) =>
100
100
  el.getAttribute('data-node'),
@@ -104,11 +104,11 @@ describe('WorkspaceGrid', () => {
104
104
 
105
105
  test('renders no drag handles unless reorderable', () => {
106
106
  const { container } = render(
107
- <WorkspaceGrid ids={['a', 'b']} viewport={VIEWPORT}>
107
+ <Workspace ids={['a', 'b']} viewport={VIEWPORT}>
108
108
  <div>a</div>
109
109
  <div>b</div>
110
- </WorkspaceGrid>,
110
+ </Workspace>,
111
111
  );
112
- expect(container.querySelectorAll('.lk-workspace-tile__grip')).toHaveLength(0);
112
+ expect(container.querySelectorAll('.lk-trial-tile__grip')).toHaveLength(0);
113
113
  });
114
114
  });
@@ -17,18 +17,18 @@ import {
17
17
  StrategyRegistryProvider,
18
18
  } from 'windease/react';
19
19
 
20
- const ZONE_ID = asNodeId('lk-workspaces');
20
+ const ZONE_ID = asNodeId('lk-workspace');
21
21
  const STRATEGIES = { grid: gridStrategy as never };
22
- const KIND = 'workspace';
22
+ const KIND = 'trial';
23
23
 
24
24
  /** A tile's persisted extent, keyed by the id its caller gave it. Grid resizes
25
25
  * write `span`; `size` is here because a strategy swap would write that. */
26
- export type WorkspaceLayout = Record<
26
+ export type TrialLayout = Record<
27
27
  string,
28
28
  { size?: { w?: number; h?: number }; span?: { cols?: number; rows?: number } }
29
29
  >;
30
30
 
31
- export interface WorkspaceGridProps {
31
+ export interface WorkspaceProps {
32
32
  children: ReactNode;
33
33
  /**
34
34
  * Stable id per child, positionally matched. Supply these whenever a tile
@@ -48,10 +48,10 @@ export interface WorkspaceGridProps {
48
48
  /** The full id list a drop would produce, in its new order. */
49
49
  onReorder?: (ids: string[]) => void;
50
50
  /** Extents from a previous session, applied to tiles as they register. */
51
- layout?: WorkspaceLayout;
51
+ layout?: TrialLayout;
52
52
  /** Fires when a tile's extent changes. Persist it and hand it back as
53
53
  * `layout` to make a resize survive a reload. */
54
- onLayoutChange?: (layout: WorkspaceLayout) => void;
54
+ onLayoutChange?: (layout: TrialLayout) => void;
55
55
  gap?: number;
56
56
  padding?: number;
57
57
  /**
@@ -62,23 +62,23 @@ export interface WorkspaceGridProps {
62
62
  viewport?: { w: number; h: number };
63
63
  }
64
64
 
65
- function extentOf(store: Store, id: NodeId): WorkspaceLayout[string] | null {
66
- const p = store.getNode(id)?.membership?.placement as WorkspaceLayout[string] | undefined;
65
+ function extentOf(store: Store, id: NodeId): TrialLayout[string] | null {
66
+ const p = store.getNode(id)?.membership?.placement as TrialLayout[string] | undefined;
67
67
  if (!p) return null;
68
- const out: WorkspaceLayout[string] = {};
68
+ const out: TrialLayout[string] = {};
69
69
  if (p.size) out.size = p.size;
70
70
  if (p.span) out.span = p.span;
71
71
  return Object.keys(out).length > 0 ? out : null;
72
72
  }
73
73
 
74
74
  /**
75
- * Auto-balanced tiling of workspaces, `ceil(sqrt(n))` columns wide.
75
+ * Auto-balanced tiling of trials, `ceil(sqrt(n))` columns wide.
76
76
  *
77
77
  * Tiles are absolutely positioned at the rects `gridStrategy` computes, not
78
78
  * laid out by CSS — `windease/styles.css` (folded into
79
79
  * `@weasel-js/labkit/styles.css`) carries the rules that positioning depends on.
80
80
  */
81
- export function WorkspaceGrid({
81
+ export function Workspace({
82
82
  children,
83
83
  ids,
84
84
  resizable = false,
@@ -89,7 +89,7 @@ export function WorkspaceGrid({
89
89
  gap = 12,
90
90
  padding = 0,
91
91
  viewport,
92
- }: WorkspaceGridProps) {
92
+ }: WorkspaceProps) {
93
93
  const items = Children.toArray(children);
94
94
  const idKey = ids ? ids.join(',') : `#${items.length}`;
95
95
  // biome-ignore lint/correctness/useExhaustiveDependencies: idKey is the stable projection of items/ids; depending on those directly rebuilds every render and re-runs the sync effect forever
@@ -142,7 +142,7 @@ export function WorkspaceGrid({
142
142
  useEffect(() => {
143
143
  if (!onLayoutChange) return;
144
144
  return store.events.on('node.placementChanged', () => {
145
- const next: WorkspaceLayout = {};
145
+ const next: TrialLayout = {};
146
146
  for (const child of store.getChildren(ZONE_ID)) {
147
147
  const extent = extentOf(store, child.id);
148
148
  if (extent) next[child.id] = extent;
@@ -164,11 +164,11 @@ export function WorkspaceGrid({
164
164
  if (!reorderable) return content;
165
165
  // A tile is full of controls, so the whole thing can't be the handle.
166
166
  return (
167
- <div className="lk-workspace-tile">
168
- <DragHandle nodeId={node.id} className="lk-workspace-tile__grip">
169
- <span className="lk-workspace-tile__grip-dots" aria-hidden="true" />
167
+ <div className="lk-trial-tile">
168
+ <DragHandle nodeId={node.id} className="lk-trial-tile__grip">
169
+ <span className="lk-trial-tile__grip-dots" aria-hidden="true" />
170
170
  </DragHandle>
171
- <div className="lk-workspace-tile__body">{content}</div>
171
+ <div className="lk-trial-tile__body">{content}</div>
172
172
  </div>
173
173
  );
174
174
  },
@@ -179,7 +179,7 @@ export function WorkspaceGrid({
179
179
  <Container
180
180
  parentId={ZONE_ID}
181
181
  chrome={chrome}
182
- className="lk-workspace-grid windease-zone"
182
+ className="lk-workspace windease-zone"
183
183
  affordances={resizable}
184
184
  viewport={viewport}
185
185
  onChildOrderChange={reorderable ? commitOrder : undefined}
package/src/lab/index.ts CHANGED
@@ -4,5 +4,5 @@ export type { LabContextValue } from './LabContext';
4
4
  export { LabContext, useLabContext } from './LabContext';
5
5
  export type { LabShellProps } from './LabShell';
6
6
  export { LabShell } from './LabShell';
7
- export type { WorkspaceGridProps } from './WorkspaceGrid';
8
- export { WorkspaceGrid } from './WorkspaceGrid';
7
+ export type { WorkspaceProps } from './Workspace';
8
+ export { Workspace } from './Workspace';