@weasel-js/labkit 1.0.2 → 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.
- package/dist/_dts/{DrawCommand-CitWPxMY.d.ts → DrawCommand-uKHt4Vul.d.ts} +56 -0
- package/dist/_dts/{index-C6Yze7sQ.d.ts → index-CFlDPeZh.d.ts} +30 -1
- package/dist/_dts/{types-Si4Fw-1F.d.ts → types-x92Kfeme.d.ts} +14 -0
- package/dist/_dts/{useExperimentState-CJn2hHzd.d.ts → useExperimentState-vrttakTt.d.ts} +43 -0
- package/dist/canvas/index.d.ts +13 -2
- package/dist/canvas/index.js +2 -2
- package/dist/{chunk-3P56ZCCJ.js → chunk-3WPOGKUP.js} +93 -62
- package/dist/chunk-3WPOGKUP.js.map +1 -0
- package/dist/{chunk-54IQ2DX7.js → chunk-574LJAV4.js} +3 -3
- package/dist/chunk-574LJAV4.js.map +1 -0
- package/dist/{chunk-VLAHRJOC.js → chunk-5R2ATYPJ.js} +2 -2
- package/dist/{chunk-VLAHRJOC.js.map → chunk-5R2ATYPJ.js.map} +1 -1
- package/dist/{chunk-2ZRE7WGQ.js → chunk-73PXCRCR.js} +3 -3
- package/dist/chunk-73PXCRCR.js.map +1 -0
- package/dist/{chunk-PWC7AQZM.js → chunk-CPUJ3QXL.js} +2 -2
- package/dist/chunk-CPUJ3QXL.js.map +1 -0
- package/dist/{chunk-R4TACNW7.js → chunk-N5KTQKQA.js} +11 -11
- package/dist/chunk-N5KTQKQA.js.map +1 -0
- package/dist/{chunk-HXZHVU4G.js → chunk-PMAU3SEE.js} +3 -3
- package/dist/chunk-PMAU3SEE.js.map +1 -0
- package/dist/{chunk-7BKDG73Z.js → chunk-RL2LOLNI.js} +2 -2
- package/dist/chunk-RL2LOLNI.js.map +1 -0
- package/dist/{chunk-2QNYYL3V.js → chunk-SFL7NFKN.js} +2 -2
- package/dist/chunk-SFL7NFKN.js.map +1 -0
- package/dist/{chunk-53XSBIUK.js → chunk-T7OKNJTY.js} +2 -2
- package/dist/chunk-T7OKNJTY.js.map +1 -0
- package/dist/controls/index.d.ts +4 -2
- package/dist/controls/index.js +1 -1
- package/dist/dragdrop/index.d.ts +2 -2
- package/dist/index.d.ts +122 -6
- package/dist/index.js +18 -18
- package/dist/index.js.map +1 -1
- package/dist/layers/index.d.ts +6 -3
- package/dist/layers/index.js +1 -1
- package/dist/passthrough/weasel-canvas.d.ts +5 -1
- package/dist/passthrough/weasel-canvas.js +1 -1
- package/dist/passthrough/weasel-ui.d.ts +444 -7
- package/dist/passthrough/weasel-ui.js +2 -2
- package/dist/primitives/index.d.ts +13 -0
- package/dist/primitives/index.js +2 -2
- package/dist/state/index.d.ts +15 -2
- package/dist/state/index.js +2 -2
- package/dist/state/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/ui/layers/index.d.ts +5 -0
- package/dist/ui/layers/index.js +3 -3
- package/dist/undo/index.d.ts +17 -2
- package/dist/undo/index.js +1 -1
- package/package.json +1 -1
- package/src/canvas/CanvasStack.tsx +4 -0
- package/src/canvas/CanvasStackContext.ts +3 -0
- package/src/canvas/canvasCoords.ts +2 -0
- package/src/canvas/useLayerScheduler.ts +2 -0
- package/src/controls/ControlPanel.tsx +2 -0
- package/src/controls/types.ts +14 -0
- package/src/instrument/capabilityDetector.ts +3 -0
- package/src/instrument/defineInstrument.ts +2 -0
- package/src/instrument/types.ts +29 -0
- package/src/instrument/validateConfigSchema.ts +5 -0
- package/src/lab/Lab.tsx +3 -0
- package/src/lab/LabContext.ts +5 -0
- package/src/lab/LabShell.tsx +4 -0
- package/src/lab/WorkspaceGrid.tsx +2 -0
- package/src/lab/gridDims.ts +2 -0
- package/src/layers/LayerList.tsx +3 -0
- package/src/primitives/FpsMeter.tsx +1 -0
- package/src/primitives/ScaleIndicator.tsx +4 -0
- package/src/primitives/Sidebar.tsx +3 -0
- package/src/primitives/StatusBar.tsx +3 -0
- package/src/primitives/Toolbar.tsx +6 -0
- package/src/state/SingletonExperiment.tsx +1 -0
- package/src/state/adapters.ts +8 -0
- package/src/state/context.tsx +9 -0
- package/src/state/helpers.ts +12 -0
- package/src/state/store.ts +6 -0
- package/src/state/types.ts +17 -0
- package/src/state/useExperimentState.ts +3 -0
- package/src/ui/layers/LayerStack.tsx +5 -0
- package/src/ui/properties/CurveField.tsx +4 -1
- package/src/ui/properties/EffectCard.tsx +12 -0
- package/src/ui/properties/PropertyGroup.tsx +1 -0
- package/src/ui/properties/PropertyPanel.tsx +28 -0
- package/src/undo/eventBus.ts +4 -0
- package/src/undo/undoStack.ts +11 -0
- package/src/workspace/DefaultSidebar.tsx +3 -0
- package/src/workspace/DefaultStatusBar.tsx +3 -0
- package/src/workspace/DefaultToolbar.tsx +3 -0
- package/src/workspace/Workspace.tsx +3 -0
- package/src/workspace/WorkspaceChrome.tsx +4 -0
- package/src/workspace/slotTypes.ts +9 -0
- package/src/workspace/workspaceOps.ts +8 -0
- package/dist/chunk-2QNYYL3V.js.map +0 -1
- package/dist/chunk-2ZRE7WGQ.js.map +0 -1
- package/dist/chunk-3P56ZCCJ.js.map +0 -1
- package/dist/chunk-53XSBIUK.js.map +0 -1
- package/dist/chunk-54IQ2DX7.js.map +0 -1
- package/dist/chunk-7BKDG73Z.js.map +0 -1
- package/dist/chunk-HXZHVU4G.js.map +0 -1
- package/dist/chunk-PWC7AQZM.js.map +0 -1
- package/dist/chunk-R4TACNW7.js.map +0 -1
|
@@ -2,12 +2,15 @@ import { type ReactNode, useState } from 'react';
|
|
|
2
2
|
import { dlog } from '../../passthrough/weasel-ui';
|
|
3
3
|
import { formatNumber, parseSignedNumber } from '../format';
|
|
4
4
|
|
|
5
|
+
/** Props for `<PropertyPanel>`. */
|
|
5
6
|
export interface PropertyPanelProps {
|
|
6
7
|
title?: ReactNode;
|
|
7
8
|
children: ReactNode;
|
|
8
9
|
className?: string;
|
|
9
10
|
}
|
|
10
11
|
|
|
12
|
+
/** A titled panel holding property rows — the sidebar container the rest of
|
|
13
|
+
* this module's components fill. */
|
|
11
14
|
export function PropertyPanel({ title, children, className }: PropertyPanelProps) {
|
|
12
15
|
const cls = className ? `lk-property-panel ${className}` : 'lk-property-panel';
|
|
13
16
|
return (
|
|
@@ -18,8 +21,10 @@ export function PropertyPanel({ title, children, className }: PropertyPanelProps
|
|
|
18
21
|
);
|
|
19
22
|
}
|
|
20
23
|
|
|
24
|
+
/** How a property list packs its rows into two columns. */
|
|
21
25
|
export type PropertyListPack = 'auto-color' | 'pairs';
|
|
22
26
|
|
|
27
|
+
/** Props for `<PropertyList>`. */
|
|
23
28
|
export interface PropertyListProps {
|
|
24
29
|
children: ReactNode;
|
|
25
30
|
className?: string;
|
|
@@ -44,9 +49,12 @@ export function PropertyList({ children, className, pack = 'auto-color' }: Prope
|
|
|
44
49
|
return <div className={cls}>{children}</div>;
|
|
45
50
|
}
|
|
46
51
|
|
|
52
|
+
/** Which control shape a row holds, which decides its intrinsic layout. */
|
|
47
53
|
export type PropertyRowVariant = 'default' | 'color' | 'checkbox';
|
|
54
|
+
/** Whether a row's label sits above its control or beside it. */
|
|
48
55
|
export type PropertyRowLayout = 'block' | 'inline';
|
|
49
56
|
|
|
57
|
+
/** Props for `<PropertyRow>`. */
|
|
50
58
|
export interface PropertyRowProps {
|
|
51
59
|
label: ReactNode;
|
|
52
60
|
/** Right-aligned readout shown next to the label (e.g. current value). */
|
|
@@ -63,6 +71,8 @@ export interface PropertyRowProps {
|
|
|
63
71
|
className?: string;
|
|
64
72
|
}
|
|
65
73
|
|
|
74
|
+
/** The label-plus-control frame every typed row below is built from. Use it
|
|
75
|
+
* directly for a control this module does not cover. */
|
|
66
76
|
export function PropertyRow({
|
|
67
77
|
label,
|
|
68
78
|
readout,
|
|
@@ -91,6 +101,7 @@ export function PropertyRow({
|
|
|
91
101
|
|
|
92
102
|
// ── Row implementations ──────────────────────────────────────────────
|
|
93
103
|
|
|
104
|
+
/** Props for `<SliderRow>`. */
|
|
94
105
|
export interface SliderRowProps {
|
|
95
106
|
label: ReactNode;
|
|
96
107
|
value: number;
|
|
@@ -109,6 +120,8 @@ export interface SliderRowProps {
|
|
|
109
120
|
layout?: PropertyRowLayout;
|
|
110
121
|
}
|
|
111
122
|
|
|
123
|
+
/** A bounded number edited by dragging, with a live readout whose precision
|
|
124
|
+
* follows `step`. */
|
|
112
125
|
export function SliderRow({
|
|
113
126
|
label,
|
|
114
127
|
value,
|
|
@@ -237,6 +250,7 @@ function EditableReadout({ value, min, max, format, unit, onCommit }: EditableRe
|
|
|
237
250
|
);
|
|
238
251
|
}
|
|
239
252
|
|
|
253
|
+
/** Props for `<ColorRow>`. */
|
|
240
254
|
export interface ColorRowProps {
|
|
241
255
|
label: ReactNode;
|
|
242
256
|
value: string;
|
|
@@ -251,6 +265,7 @@ export interface ColorRowProps {
|
|
|
251
265
|
alphaDisabled?: boolean;
|
|
252
266
|
}
|
|
253
267
|
|
|
268
|
+
/** A color swatch, optionally with an alpha slider beneath it. */
|
|
254
269
|
export function ColorRow({
|
|
255
270
|
label,
|
|
256
271
|
value,
|
|
@@ -280,12 +295,14 @@ export function ColorRow({
|
|
|
280
295
|
);
|
|
281
296
|
}
|
|
282
297
|
|
|
298
|
+
/** Props for `<CheckboxRow>`. */
|
|
283
299
|
export interface CheckboxRowProps {
|
|
284
300
|
label: ReactNode;
|
|
285
301
|
value: boolean;
|
|
286
302
|
onChange: (next: boolean) => void;
|
|
287
303
|
}
|
|
288
304
|
|
|
305
|
+
/** A boolean checkbox. */
|
|
289
306
|
export function CheckboxRow({ label, value, onChange }: CheckboxRowProps) {
|
|
290
307
|
return (
|
|
291
308
|
<PropertyRow label={label} variant="checkbox">
|
|
@@ -294,6 +311,7 @@ export function CheckboxRow({ label, value, onChange }: CheckboxRowProps) {
|
|
|
294
311
|
);
|
|
295
312
|
}
|
|
296
313
|
|
|
314
|
+
/** Props for `<TextRow>`. */
|
|
297
315
|
export interface TextRowProps {
|
|
298
316
|
label: ReactNode;
|
|
299
317
|
value: string;
|
|
@@ -303,6 +321,7 @@ export interface TextRowProps {
|
|
|
303
321
|
layout?: PropertyRowLayout;
|
|
304
322
|
}
|
|
305
323
|
|
|
324
|
+
/** A single-line text input. */
|
|
306
325
|
export function TextRow({ label, value, onChange, placeholder, maxLength, layout }: TextRowProps) {
|
|
307
326
|
return (
|
|
308
327
|
<PropertyRow label={label} layout={layout}>
|
|
@@ -317,6 +336,7 @@ export function TextRow({ label, value, onChange, placeholder, maxLength, layout
|
|
|
317
336
|
);
|
|
318
337
|
}
|
|
319
338
|
|
|
339
|
+
/** Props for `<NumberRow>`. */
|
|
320
340
|
export interface NumberRowProps {
|
|
321
341
|
label: ReactNode;
|
|
322
342
|
value: number;
|
|
@@ -328,6 +348,8 @@ export interface NumberRowProps {
|
|
|
328
348
|
layout?: PropertyRowLayout;
|
|
329
349
|
}
|
|
330
350
|
|
|
351
|
+
/** A number typed directly. Reach for `<SliderRow>` when the range matters
|
|
352
|
+
* more than the exact value. */
|
|
331
353
|
export function NumberRow({
|
|
332
354
|
label,
|
|
333
355
|
value,
|
|
@@ -363,6 +385,7 @@ export interface SelectOption<T extends string> {
|
|
|
363
385
|
label: ReactNode;
|
|
364
386
|
}
|
|
365
387
|
|
|
388
|
+
/** Props for `<SelectRow>`. */
|
|
366
389
|
export interface SelectRowProps<T extends string> {
|
|
367
390
|
label: ReactNode;
|
|
368
391
|
value: T;
|
|
@@ -371,6 +394,8 @@ export interface SelectRowProps<T extends string> {
|
|
|
371
394
|
layout?: PropertyRowLayout;
|
|
372
395
|
}
|
|
373
396
|
|
|
397
|
+
/** A dropdown over a fixed set of choices. Prefer `<ToggleRow>` when there
|
|
398
|
+
* are only two or three and they should all stay visible. */
|
|
374
399
|
export function SelectRow<T extends string>({
|
|
375
400
|
label,
|
|
376
401
|
value,
|
|
@@ -399,6 +424,7 @@ export function SelectRow<T extends string>({
|
|
|
399
424
|
);
|
|
400
425
|
}
|
|
401
426
|
|
|
427
|
+
/** Props for `<ToggleRow>`. */
|
|
402
428
|
export interface ToggleRowProps<T extends string> {
|
|
403
429
|
label: ReactNode;
|
|
404
430
|
value: T;
|
|
@@ -407,6 +433,8 @@ export interface ToggleRowProps<T extends string> {
|
|
|
407
433
|
layout?: PropertyRowLayout;
|
|
408
434
|
}
|
|
409
435
|
|
|
436
|
+
/** A segmented control: the same choice as a select, with every option
|
|
437
|
+
* visible at once. */
|
|
410
438
|
export function ToggleRow<T extends string>({
|
|
411
439
|
label,
|
|
412
440
|
value,
|
package/src/undo/eventBus.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
|
+
/** A subscriber to a named event. */
|
|
1
2
|
export type EventListener = () => void;
|
|
2
3
|
|
|
4
|
+
/** Named-event pub/sub, the channel an instrument's `emit` writes to and undo
|
|
5
|
+
* snapshotting listens on. */
|
|
3
6
|
export interface EventBus {
|
|
4
7
|
on(event: string, listener: EventListener): () => void;
|
|
5
8
|
emit(event: string): void;
|
|
6
9
|
clear(): void;
|
|
7
10
|
}
|
|
8
11
|
|
|
12
|
+
/** Build an empty event bus. */
|
|
9
13
|
export function createEventBus(): EventBus {
|
|
10
14
|
const listeners = new Map<string, Set<EventListener>>();
|
|
11
15
|
return {
|
package/src/undo/undoStack.ts
CHANGED
|
@@ -1,18 +1,26 @@
|
|
|
1
|
+
/** Undo history as two stacks of state snapshots: what has been done, and
|
|
2
|
+
* what has been undone. The present state is held elsewhere, not here. */
|
|
1
3
|
export interface UndoStack {
|
|
2
4
|
past: unknown[];
|
|
3
5
|
future: unknown[];
|
|
4
6
|
}
|
|
5
7
|
|
|
8
|
+
/** A fresh, empty history. */
|
|
6
9
|
export function emptyStack(): UndoStack {
|
|
7
10
|
return { past: [], future: [] };
|
|
8
11
|
}
|
|
9
12
|
|
|
13
|
+
/** Record a new snapshot, discarding the redo branch and the oldest entry
|
|
14
|
+
* once `maxDepth` is reached. */
|
|
10
15
|
export function pushSnapshot(stack: UndoStack, snapshot: unknown, maxDepth: number): UndoStack {
|
|
11
16
|
const past = stack.past.length >= maxDepth ? stack.past.slice(1) : stack.past.slice();
|
|
12
17
|
past.push(snapshot);
|
|
13
18
|
return { past, future: [] };
|
|
14
19
|
}
|
|
15
20
|
|
|
21
|
+
/** Step back one snapshot, returning the new history and the state to restore.
|
|
22
|
+
* `null` when there is nothing to undo. The caller supplies `current` so it
|
|
23
|
+
* can be redone. */
|
|
16
24
|
export function undo(
|
|
17
25
|
stack: UndoStack,
|
|
18
26
|
current: unknown,
|
|
@@ -23,6 +31,8 @@ export function undo(
|
|
|
23
31
|
return { stack: { past, future: [...stack.future, current] }, snapshot };
|
|
24
32
|
}
|
|
25
33
|
|
|
34
|
+
/** Step forward one snapshot, returning the new history and the state to
|
|
35
|
+
* restore. `null` when there is nothing to redo. */
|
|
26
36
|
export function redo(
|
|
27
37
|
stack: UndoStack,
|
|
28
38
|
current: unknown,
|
|
@@ -33,6 +43,7 @@ export function redo(
|
|
|
33
43
|
return { stack: { past: [...stack.past, current], future }, snapshot };
|
|
34
44
|
}
|
|
35
45
|
|
|
46
|
+
/** Discard the whole history. */
|
|
36
47
|
export function clearUndo(_stack: UndoStack): UndoStack {
|
|
37
48
|
return emptyStack();
|
|
38
49
|
}
|
|
@@ -2,10 +2,13 @@ import { ControlPanel } from '../controls/ControlPanel';
|
|
|
2
2
|
import { Sidebar } from '../primitives/Sidebar';
|
|
3
3
|
import type { WorkspaceSidebarContext } from './slotTypes';
|
|
4
4
|
|
|
5
|
+
/** Props for `<DefaultSidebar>`. */
|
|
5
6
|
export interface DefaultSidebarProps {
|
|
6
7
|
ctx: WorkspaceSidebarContext;
|
|
7
8
|
}
|
|
8
9
|
|
|
10
|
+
/** The sidebar a workspace renders when no `sidebar` slot is supplied: a
|
|
11
|
+
* control panel over the instrument's config schema. */
|
|
9
12
|
export function DefaultSidebar({ ctx }: DefaultSidebarProps) {
|
|
10
13
|
if (ctx.configFields.length === 0) {
|
|
11
14
|
return (
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { StatusBar } from '../primitives/StatusBar';
|
|
2
2
|
import type { WorkspaceStatusBarContext } from './slotTypes';
|
|
3
3
|
|
|
4
|
+
/** Props for `<DefaultStatusBar>`. */
|
|
4
5
|
export interface DefaultStatusBarProps {
|
|
5
6
|
ctx: WorkspaceStatusBarContext;
|
|
6
7
|
}
|
|
7
8
|
|
|
9
|
+
/** The status bar a workspace renders when no `statusBar` slot is supplied:
|
|
10
|
+
* the instrument's name and the current zoom. */
|
|
8
11
|
export function DefaultStatusBar({ ctx }: DefaultStatusBarProps) {
|
|
9
12
|
return (
|
|
10
13
|
<StatusBar>
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { Toolbar } from '../primitives/Toolbar';
|
|
2
2
|
import type { WorkspaceToolbarContext } from './slotTypes';
|
|
3
3
|
|
|
4
|
+
/** Props for `<DefaultToolbar>`. */
|
|
4
5
|
export interface DefaultToolbarProps {
|
|
5
6
|
ctx: WorkspaceToolbarContext;
|
|
6
7
|
}
|
|
7
8
|
|
|
9
|
+
/** The toolbar a workspace renders when no `toolbar` slot is supplied. Only
|
|
10
|
+
* shows the controls the instrument's declared capabilities support. */
|
|
8
11
|
export function DefaultToolbar({ ctx }: DefaultToolbarProps) {
|
|
9
12
|
return (
|
|
10
13
|
<Toolbar>
|
|
@@ -21,10 +21,13 @@ import { pushSnapshot, redo as undoRedo, undo as undoUndo } from '../undo/undoSt
|
|
|
21
21
|
import type { UndoBindings } from './WorkspaceChrome';
|
|
22
22
|
import { WorkspaceChrome } from './WorkspaceChrome';
|
|
23
23
|
|
|
24
|
+
/** Props for `<Workspace>`. */
|
|
24
25
|
export interface WorkspaceProps {
|
|
25
26
|
id: string;
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
/** Renders one workspace from the lab store: looks up its record and
|
|
30
|
+
* instrument, and mounts the instrument inside the workspace chrome. */
|
|
28
31
|
export function Workspace({ id }: WorkspaceProps) {
|
|
29
32
|
const lab = useLabContext();
|
|
30
33
|
const storeCtx = useContext(LabStoreContext);
|
|
@@ -23,6 +23,7 @@ export interface UndoBindings {
|
|
|
23
23
|
redo: () => void;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/** Props for `<WorkspaceChrome>`. */
|
|
26
27
|
export interface WorkspaceChromeProps {
|
|
27
28
|
workspaceId: string;
|
|
28
29
|
record: WorkspaceRecord;
|
|
@@ -36,6 +37,9 @@ export interface WorkspaceChromeProps {
|
|
|
36
37
|
children: ReactNode;
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
/** The frame around a running instrument — toolbar, sidebar, status bar —
|
|
41
|
+
* each replaceable by a slot. Assembles the slot contexts and wires the undo
|
|
42
|
+
* keyboard shortcuts. */
|
|
39
43
|
export function WorkspaceChrome({
|
|
40
44
|
workspaceId,
|
|
41
45
|
record,
|
|
@@ -2,6 +2,8 @@ import type { ReactNode } from 'react';
|
|
|
2
2
|
import type { ConfigField } from '../controls/types';
|
|
3
3
|
import type { SavedSnapshot } from '../state/types';
|
|
4
4
|
|
|
5
|
+
/** What a workspace hands its toolbar: which instrument is running, the undo
|
|
6
|
+
* state and commands, the zoom controls, and the snapshot commands. */
|
|
5
7
|
export interface WorkspaceToolbarContext {
|
|
6
8
|
workspaceId: string;
|
|
7
9
|
instrumentName: string;
|
|
@@ -25,6 +27,8 @@ export interface WorkspaceToolbarContext {
|
|
|
25
27
|
isLastWorkspace: boolean;
|
|
26
28
|
}
|
|
27
29
|
|
|
30
|
+
/** What a workspace hands its sidebar: the instrument's config schema, its
|
|
31
|
+
* current values, and the setter. */
|
|
28
32
|
export interface WorkspaceSidebarContext {
|
|
29
33
|
workspaceId: string;
|
|
30
34
|
instrumentName: string;
|
|
@@ -33,12 +37,17 @@ export interface WorkspaceSidebarContext {
|
|
|
33
37
|
setConfig: (key: string, value: unknown) => void;
|
|
34
38
|
}
|
|
35
39
|
|
|
40
|
+
/** What a workspace hands its status bar. */
|
|
36
41
|
export interface WorkspaceStatusBarContext {
|
|
37
42
|
workspaceId: string;
|
|
38
43
|
instrumentName: string;
|
|
39
44
|
zoom: number;
|
|
40
45
|
}
|
|
41
46
|
|
|
47
|
+
/** Replaces a workspace's toolbar. Receives everything the default one uses,
|
|
48
|
+
* so a custom toolbar need not reach into the store. */
|
|
42
49
|
export type ToolbarSlot = (ctx: WorkspaceToolbarContext) => ReactNode;
|
|
50
|
+
/** Replaces a workspace's sidebar. */
|
|
43
51
|
export type SidebarSlot = (ctx: WorkspaceSidebarContext) => ReactNode;
|
|
52
|
+
/** Replaces a workspace's status bar. */
|
|
44
53
|
export type StatusBarSlot = (ctx: WorkspaceStatusBarContext) => ReactNode;
|
|
@@ -17,6 +17,8 @@ function initialView(instrument: Instrument): WorkspaceRecord['view'] {
|
|
|
17
17
|
: { ...DEFAULT_VIEW, pan: { ...DEFAULT_VIEW.pan } };
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/** Append a new workspace running `instrumentName`, at that instrument's
|
|
21
|
+
* default config and initial state. */
|
|
20
22
|
export function addWorkspace(
|
|
21
23
|
workspaces: WorkspaceRecord[],
|
|
22
24
|
instruments: Instrument[],
|
|
@@ -36,6 +38,8 @@ export function addWorkspace(
|
|
|
36
38
|
return [...workspaces, record];
|
|
37
39
|
}
|
|
38
40
|
|
|
41
|
+
/** Insert a deep copy of a workspace directly after it. The copy starts with
|
|
42
|
+
* an empty undo history — the original's is not shared. */
|
|
39
43
|
export function cloneWorkspace(workspaces: WorkspaceRecord[], id: string): WorkspaceRecord[] {
|
|
40
44
|
const sourceIdx = workspaces.findIndex((w) => w.id === id);
|
|
41
45
|
const source = workspaces[sourceIdx];
|
|
@@ -51,12 +55,16 @@ export function cloneWorkspace(workspaces: WorkspaceRecord[], id: string): Works
|
|
|
51
55
|
return [...workspaces.slice(0, sourceIdx + 1), clone, ...workspaces.slice(sourceIdx + 1)];
|
|
52
56
|
}
|
|
53
57
|
|
|
58
|
+
/** Remove a workspace, unless it is the last one — a lab always has at least
|
|
59
|
+
* one. */
|
|
54
60
|
export function closeWorkspace(workspaces: WorkspaceRecord[], id: string): WorkspaceRecord[] {
|
|
55
61
|
if (workspaces.length <= 1) return workspaces;
|
|
56
62
|
const next = workspaces.filter((w) => w.id !== id);
|
|
57
63
|
return next.length === workspaces.length ? workspaces : next;
|
|
58
64
|
}
|
|
59
65
|
|
|
66
|
+
/** Return a workspace to its instrument's defaults, keeping its id and its
|
|
67
|
+
* place in the list. */
|
|
60
68
|
export function resetWorkspace(
|
|
61
69
|
workspaces: WorkspaceRecord[],
|
|
62
70
|
id: string,
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/layers/LayerList.tsx"],"names":[],"mappings":"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","file":"chunk-2QNYYL3V.js","sourcesContent":["import { type PointerEvent, useRef, useState } from 'react';\nimport type { LayerDescriptor } from '../instrument/types';\n\nexport interface LayerListProps {\n layers: LayerDescriptor[];\n visibility: Record<string, boolean>;\n onReorder: (newOrder: LayerDescriptor[]) => void;\n onToggle: (id: string, visible: boolean) => void;\n className?: string;\n}\n\ninterface DragState {\n pointerId: number;\n fromIndex: number;\n startY: number;\n}\n\nexport function LayerList({ layers, visibility, onReorder, onToggle, className }: LayerListProps) {\n const reorderable = layers.filter((l) => !l.alwaysOn);\n const pinned = layers.filter((l) => l.alwaysOn);\n const [dragIndex, setDragIndex] = useState<number | null>(null);\n const dragRef = useRef<DragState | null>(null);\n\n if (layers.length === 0) {\n return (\n <div className={className ? `lk-layer-list ${className}` : 'lk-layer-list'}>\n <div className=\"lk-layer-list__empty\">No layers</div>\n </div>\n );\n }\n\n const startDrag = (e: PointerEvent<HTMLElement>, index: number) => {\n e.currentTarget.setPointerCapture(e.pointerId);\n dragRef.current = { pointerId: e.pointerId, fromIndex: index, startY: e.clientY };\n setDragIndex(index);\n };\n\n const moveDrag = (e: PointerEvent<HTMLElement>) => {\n const drag = dragRef.current;\n if (!drag || drag.pointerId !== e.pointerId) return;\n const rowHeight = 28;\n const delta = Math.round((e.clientY - drag.startY) / rowHeight);\n const target = Math.min(reorderable.length - 1, Math.max(0, drag.fromIndex + delta));\n setDragIndex(target);\n };\n\n const endDrag = (e: PointerEvent<HTMLElement>) => {\n const drag = dragRef.current;\n if (!drag || drag.pointerId !== e.pointerId) return;\n if (e.currentTarget.hasPointerCapture(e.pointerId)) {\n e.currentTarget.releasePointerCapture(e.pointerId);\n }\n if (dragIndex !== null && dragIndex !== drag.fromIndex) {\n const next = [...reorderable];\n const [moved] = next.splice(drag.fromIndex, 1);\n if (moved) next.splice(dragIndex, 0, moved);\n onReorder([...next, ...pinned]);\n }\n dragRef.current = null;\n setDragIndex(null);\n };\n\n return (\n <div className={className ? `lk-layer-list ${className}` : 'lk-layer-list'}>\n {reorderable.map((layer, i) => {\n const isDragging = dragIndex === i;\n return (\n <div\n key={layer.id}\n className={\n isDragging ? 'lk-layer-list__row lk-layer-list__row--dragging' : 'lk-layer-list__row'\n }\n >\n <button\n type=\"button\"\n className=\"lk-layer-list__handle\"\n aria-label={`Reorder ${layer.label}`}\n onPointerDown={(e) => startDrag(e, i)}\n onPointerMove={moveDrag}\n onPointerUp={endDrag}\n >\n ⋮⋮\n </button>\n <input\n className=\"lk-layer-list__check\"\n type=\"checkbox\"\n checked={visibility[layer.id] !== false}\n onChange={(e) => onToggle(layer.id, e.target.checked)}\n aria-label={`Toggle ${layer.label}`}\n />\n <span className=\"lk-layer-list__label\">{layer.label}</span>\n </div>\n );\n })}\n {pinned.map((layer) => (\n <div key={layer.id} className=\"lk-layer-list__row lk-layer-list__row--pinned\">\n <span className=\"lk-layer-list__lock\" role=\"img\" aria-label=\"Always on\">\n 🔒\n </span>\n <span className=\"lk-layer-list__label\">{layer.label}</span>\n </div>\n ))}\n </div>\n );\n}\n"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/ui/layers/LayerStack.tsx"],"names":["s"],"mappings":"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","file":"chunk-2ZRE7WGQ.js","sourcesContent":["import { type CSSProperties, type ReactNode, type RefCallback, useEffect, useState } from 'react';\nimport { dlog, useReorderDragList } from '../../passthrough/weasel-ui';\n\nexport interface LayerStackItem {\n /** Stable id used for keys, onRemove, onReorder. Numeric to match\n * common id-from-nextId conventions; string ids also work. */\n id: number | string;\n /** Short kind label rendered in the header when no primary select\n * is hoisted (e.g. \"shadow\", \"stroke\"). */\n kind: string;\n /** When present, hoist this select into the card header so the user\n * can switch mode/shape without expanding. */\n primaryValue?: string;\n primaryOptions?: string[];\n /** Accent CSS color used as the left border / index-badge fill. */\n accent?: string;\n /** Optional badge text rendered before the primary control\n * (e.g. tail index \"1\", \"2\", \"3\"). When omitted a drag handle\n * glyph renders in its place. */\n badge?: string;\n /** Initial expanded state. Defaults to true for newly-added items. */\n defaultExpanded?: boolean;\n}\n\nexport interface LayerStackProps {\n title: string;\n items: LayerStackItem[];\n /** Kinds the user can add via the header palette. */\n paletteKinds: string[];\n onAdd: (kind: string) => void;\n onRemove: (id: number | string) => void;\n onReorder: (orderedIds: Array<number | string>) => void;\n onPrimaryChange: (id: number | string, nextValue: string) => void;\n /** Render the body controls for each item. */\n renderBody: (item: LayerStackItem) => ReactNode;\n /** Hide the title + palette row (used when an outer wrap renders its\n * own head — see speech-balloons Tails panel). */\n hideHead?: boolean;\n}\n\nexport function LayerStack({\n title,\n items,\n paletteKinds,\n onAdd,\n onRemove,\n onReorder,\n onPrimaryChange,\n renderBody,\n hideHead,\n}: LayerStackProps) {\n const [expandedIds, setExpandedIds] = useState<Set<number | string>>(\n () => new Set(items.filter((i) => i.defaultExpanded !== false).map((i) => i.id)),\n );\n\n useEffect(() => {\n setExpandedIds((prev) => {\n let next: Set<number | string> | null = null;\n for (const item of items) {\n if (item.defaultExpanded === false) continue;\n if (prev.has(item.id)) continue;\n if (next === null) next = new Set(prev);\n next.add(item.id);\n }\n return next ?? prev;\n });\n }, [items]);\n\n const dragItems = items.map((it) => ({ id: String(it.id), label: it.kind }));\n const drag = useReorderDragList({\n items: dragItems,\n selectedIds: [],\n onReorder: (ids, targetIndex) => {\n dlog('layer-stack', 'onReorder', { ids, targetIndex });\n const orig = items.map((i) => i.id);\n const moving = new Set(ids);\n const remaining = orig.filter((id) => !moving.has(String(id)));\n const movedIds = items.map((i) => i.id).filter((id) => moving.has(String(id)));\n // weasel reports targetIndex against the original list (which includes\n // the dragged row). Each moved item that originally sat before\n // targetIndex needs to shift the insertion point left by one.\n const shift = orig.filter((id, idx) => moving.has(String(id)) && idx < targetIndex).length;\n const adjusted = targetIndex - shift;\n const out = [...remaining];\n out.splice(adjusted, 0, ...movedIds);\n onReorder(out);\n },\n });\n\n const toggleExpanded = (id: number | string) => {\n setExpandedIds((s) => {\n const n = new Set(s);\n if (n.has(id)) n.delete(id);\n else n.add(id);\n return n;\n });\n };\n\n return (\n <div className=\"lk-layer-stack\">\n {!hideHead && (\n <div className=\"lk-layer-stack__head\">\n <h2 className=\"lk-layer-stack__title\">{title}</h2>\n <div className=\"lk-layer-stack__palette\">\n {paletteKinds.map((k) => (\n <button\n key={k}\n type=\"button\"\n className=\"lk-layer-stack__add\"\n onClick={() => {\n dlog('layer-stack', 'onAdd', { kind: k });\n onAdd(k);\n }}\n aria-label={`Add ${k}`}\n >\n {k}\n </button>\n ))}\n </div>\n </div>\n )}\n <div\n className=\"lk-layer-stack__list\"\n ref={drag.containerProps.ref as RefCallback<HTMLDivElement>}\n onPointerMove={drag.containerProps.onPointerMove}\n onPointerUp={drag.containerProps.onPointerUp}\n onPointerCancel={drag.containerProps.onPointerCancel}\n >\n {items.map((item, i) => {\n const expanded = expandedIds.has(item.id);\n const draggedId = drag.state.draggedIds?.[0];\n const isDragging = draggedId === String(item.id);\n const showHintBefore = drag.state.targetIndex === i && draggedId !== String(item.id);\n const showHintAfter = drag.state.targetIndex === items.length && i === items.length - 1;\n const cardCls = [\n 'lk-layer-card',\n expanded ? 'is-expanded' : 'is-collapsed',\n isDragging ? 'is-dragging' : '',\n item.accent ? 'has-accent' : '',\n ]\n .filter(Boolean)\n .join(' ');\n const cardStyle = item.accent\n ? ({ '--lk-layer-card-accent': item.accent } as CSSProperties)\n : undefined;\n const { onPointerDown } = drag.rowProps(String(item.id), i);\n return (\n <div key={item.id} className=\"lk-layer-card-wrap\">\n {showHintBefore && <div className=\"lk-layer-stack__drop-hint\" />}\n <div className={cardCls} data-testid={`lk-layer-card-${item.id}`} style={cardStyle}>\n <div className=\"lk-layer-card__head\">\n <button\n type=\"button\"\n className=\"lk-layer-card__handle\"\n aria-label={`Drag to reorder layer ${item.id}`}\n onPointerDown={onPointerDown}\n onClick={() => toggleExpanded(item.id)}\n >\n {item.badge ?? <DragHandleGlyph />}\n </button>\n {item.primaryValue !== undefined && item.primaryOptions ? (\n <select\n className=\"lk-layer-card__primary\"\n value={item.primaryValue}\n aria-label={`Primary select for layer ${item.id}`}\n onChange={(e) => onPrimaryChange(item.id, e.target.value)}\n onClick={(e) => e.stopPropagation()}\n >\n {item.primaryOptions.map((o) => (\n <option key={o} value={o}>\n {o}\n </option>\n ))}\n </select>\n ) : (\n <span className=\"lk-layer-card__kind\">{item.kind}</span>\n )}\n <button\n type=\"button\"\n className=\"lk-layer-card__remove\"\n aria-label=\"Remove layer\"\n onClick={(e) => {\n e.stopPropagation();\n onRemove(item.id);\n }}\n >\n ✕\n </button>\n </div>\n {expanded && <div className=\"lk-layer-card__body\">{renderBody(item)}</div>}\n </div>\n {showHintAfter && <div className=\"lk-layer-stack__drop-hint\" />}\n </div>\n );\n })}\n {items.length === 0 && (\n <div className=\"lk-layer-stack__empty\">No layers — add one above.</div>\n )}\n </div>\n </div>\n );\n}\n\nfunction DragHandleGlyph() {\n return (\n <svg width=\"12\" height=\"16\" viewBox=\"0 0 8 16\" fill=\"currentColor\" aria-hidden=\"true\">\n <circle cx=\"2\" cy=\"3\" r=\"1.1\" />\n <circle cx=\"6\" cy=\"3\" r=\"1.1\" />\n <circle cx=\"2\" cy=\"8\" r=\"1.1\" />\n <circle cx=\"6\" cy=\"8\" r=\"1.1\" />\n <circle cx=\"2\" cy=\"13\" r=\"1.1\" />\n <circle cx=\"6\" cy=\"13\" r=\"1.1\" />\n </svg>\n );\n}\n"]}
|