@weasel-js/labkit 1.1.0 → 1.3.0-pre.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.
- package/README.md +27 -0
- package/dist/_dts/{DrawCommand-BkZztJsW.d.ts → DrawCommand-DBB45NfN.d.ts} +244 -58
- package/dist/_dts/PrefsForm-BYa6cWnO.d.ts +201 -0
- package/dist/_dts/index-DgcNdEdh.d.ts +314 -0
- package/dist/_dts/types-1Sdxy_Pv.d.ts +90 -0
- package/dist/_dts/types-AHlQxBNN.d.ts +162 -0
- package/dist/_dts/types-DJ79Tg5J.d.ts +56 -0
- package/dist/_dts/{useTrialState-DYe2vUwN.d.ts → useTrialState-CsGMjhu9.d.ts} +6 -92
- package/dist/canvas/index.d.ts +8 -3
- package/dist/canvas/index.js +2 -1
- package/dist/chrome/index.d.ts +86 -0
- package/dist/chrome/index.js +8 -0
- package/dist/chrome/index.js.map +1 -0
- package/dist/{chunk-3TYUJR7Z.js → chunk-4TMMVIDM.js} +9 -3
- package/dist/chunk-4TMMVIDM.js.map +1 -0
- package/dist/chunk-HFDVGF4X.js +170 -0
- package/dist/chunk-HFDVGF4X.js.map +1 -0
- package/dist/chunk-LN6JDUGB.js +106 -0
- package/dist/chunk-LN6JDUGB.js.map +1 -0
- package/dist/{chunk-574LJAV4.js → chunk-NS54R52R.js} +31 -21
- package/dist/chunk-NS54R52R.js.map +1 -0
- package/dist/{chunk-NRKWVTVT.js → chunk-O2BULFHX.js} +5 -14
- package/dist/chunk-O2BULFHX.js.map +1 -0
- package/dist/chunk-O7NVSCLN.js +266 -0
- package/dist/chunk-O7NVSCLN.js.map +1 -0
- package/dist/chunk-PO6L3NSH.js +366 -0
- package/dist/chunk-PO6L3NSH.js.map +1 -0
- package/dist/chunk-PQJ5B2U2.js +27 -0
- package/dist/chunk-PQJ5B2U2.js.map +1 -0
- package/dist/chunk-RVNN4CHQ.js +212 -0
- package/dist/chunk-RVNN4CHQ.js.map +1 -0
- package/dist/chunk-TLGRYALP.js +6151 -0
- package/dist/chunk-TLGRYALP.js.map +1 -0
- package/dist/{chunk-SFL7NFKN.js → chunk-V7PRSIRQ.js} +15 -7
- package/dist/chunk-V7PRSIRQ.js.map +1 -0
- package/dist/{chunk-DJLDIRFN.js → chunk-Z6HJ5FEM.js} +9626 -6050
- package/dist/chunk-Z6HJ5FEM.js.map +1 -0
- package/dist/chunk-ZZAYVX4X.js +505 -0
- package/dist/chunk-ZZAYVX4X.js.map +1 -0
- package/dist/controls/index.d.ts +19 -5
- package/dist/controls/index.js +3 -1
- package/dist/dragdrop/index.d.ts +5 -2
- package/dist/index.d.ts +307 -107
- package/dist/index.js +797 -540
- package/dist/index.js.map +1 -1
- package/dist/job/index.d.ts +13 -0
- package/dist/job/index.js +3 -0
- package/dist/job/index.js.map +1 -0
- package/dist/layers/index.d.ts +6 -3
- package/dist/layers/index.js +2 -1
- package/dist/passthrough/weasel-canvas.d.ts +69 -13
- package/dist/passthrough/weasel-canvas.js +1 -1
- package/dist/passthrough/weasel-ui.d.ts +3804 -3661
- package/dist/passthrough/weasel-ui.js +2 -2
- package/dist/primitives/index.d.ts +102 -8
- package/dist/primitives/index.js +4 -1
- package/dist/state/index.d.ts +3 -2
- package/dist/state/index.js +2 -2
- package/dist/styles.css +543 -144
- package/dist/surface/index.d.ts +77 -0
- package/dist/surface/index.js +4 -0
- package/dist/surface/index.js.map +1 -0
- package/dist/ui/layers/index.js +4 -3
- package/dist/undo/index.d.ts +5 -2
- package/package.json +24 -7
- package/src/canvas/AGENTS.md +12 -0
- package/src/canvas/CanvasStack.tsx +6 -2
- package/src/canvas/useLayerScheduler.ts +22 -15
- package/src/canvas/useOrbit.test.ts +71 -0
- package/src/canvas/useOrbit.ts +149 -0
- package/src/canvas/usePanZoom.test.ts +80 -0
- package/src/canvas/usePanZoom.ts +16 -2
- package/src/chrome/ChromeRegions.stories.tsx +106 -0
- package/src/chrome/builtins.test.ts +153 -0
- package/src/chrome/builtins.tsx +205 -0
- package/src/chrome/index.ts +25 -0
- package/src/chrome/merge.test.ts +51 -0
- package/src/chrome/merge.ts +52 -0
- package/src/chrome/regions/PaletteRegion.less +14 -0
- package/src/chrome/regions/PaletteRegion.test.tsx +36 -0
- package/src/chrome/regions/PaletteRegion.tsx +41 -0
- package/src/chrome/regions/SidebarRegion.tsx +56 -0
- package/src/chrome/regions/StatusRegion.tsx +31 -0
- package/src/chrome/regions/TitleBarRegion.tsx +41 -0
- package/src/chrome/regions/ToolbarRegion.test.tsx +82 -0
- package/src/chrome/regions/ToolbarRegion.tsx +71 -0
- package/src/chrome/regions/ViewportRegion.less +42 -0
- package/src/chrome/regions/ViewportRegion.tsx +37 -0
- package/src/chrome/regions/regions.test.tsx +81 -0
- package/src/chrome/types.ts +127 -0
- package/src/config/builder.test.ts +87 -0
- package/src/config/builder.ts +224 -0
- package/src/config/fromConfigField.test.ts +93 -0
- package/src/config/fromConfigField.ts +74 -0
- package/src/config/index.ts +22 -0
- package/src/config/resolve.test.ts +120 -0
- package/src/config/resolve.ts +67 -0
- package/src/config/rules.ts +68 -0
- package/src/config/types.ts +108 -0
- package/src/config/useConfigSchema.ts +30 -0
- package/src/config/visible.ts +20 -0
- package/src/controls/ControlPanel.less +4 -58
- package/src/controls/ControlPanel.stories.tsx +86 -0
- package/src/controls/ControlPanel.test.tsx +223 -10
- package/src/controls/ControlPanel.tsx +202 -203
- package/src/dragdrop/DragDropRuntime.tsx +36 -18
- package/src/dragdrop/DragGhost.less +1 -1
- package/src/dragdrop/Palette.less +1 -1
- package/src/index.test.ts +18 -0
- package/src/index.ts +38 -3
- package/src/instrument/SineWave.smoke.test.tsx +9 -1
- package/src/instrument/defineInstrument.test.ts +38 -1
- package/src/instrument/defineInstrument.ts +31 -4
- package/src/instrument/index.ts +1 -0
- package/src/instrument/types.ts +38 -3
- package/src/instrument/validateConfigSchema.test.ts +13 -3
- package/src/instrument/validateConfigSchema.ts +7 -10
- package/src/job/index.ts +3 -0
- package/src/job/types.ts +47 -0
- package/src/job/useJob.test.tsx +210 -0
- package/src/job/useJob.ts +134 -0
- package/src/lab/Lab.chrome.test.tsx +65 -0
- package/src/lab/Lab.stories.tsx +1 -2
- package/src/lab/Lab.tsx +58 -15
- package/src/lab/LabContext.ts +6 -0
- package/src/lab/LabFullChrome.stories.tsx +308 -0
- package/src/lab/LabHeader.test.tsx +42 -0
- package/src/lab/LabHeader.tsx +57 -0
- package/src/lab/LabPalette.test.tsx +35 -0
- package/src/lab/LabPalette.tsx +34 -0
- package/src/lab/LabShell.less +50 -3
- package/src/lab/Workspace.less +1 -30
- package/src/lab/Workspace.surface.test.tsx +49 -0
- package/src/lab/Workspace.test.tsx +20 -10
- package/src/lab/Workspace.tsx +19 -9
- package/src/lab/index.ts +2 -0
- package/src/layers/AGENTS.md +1 -1
- package/src/layers/LayerList.less +31 -7
- package/src/layers/LayerList.tsx +17 -4
- package/src/passthrough/weasel-ui.ts +23 -0
- package/src/primitives/DragHandleGlyph.tsx +29 -0
- package/src/primitives/FloatingPanel.less +28 -0
- package/src/primitives/FloatingPanel.stories.tsx +46 -0
- package/src/primitives/FloatingPanel.test.tsx +240 -0
- package/src/primitives/FloatingPanel.tsx +190 -0
- package/src/primitives/FpsMeter.test.tsx +44 -0
- package/src/primitives/FpsMeter.tsx +20 -9
- package/src/primitives/JobProgress.less +69 -0
- package/src/primitives/JobProgress.stories.tsx +48 -0
- package/src/primitives/JobProgress.tsx +65 -0
- package/src/primitives/Legend.less +47 -0
- package/src/primitives/Legend.stories.tsx +32 -0
- package/src/primitives/Legend.test.tsx +57 -0
- package/src/primitives/Legend.tsx +40 -0
- package/src/primitives/Sidebar.less +32 -2
- package/src/primitives/StatusBar.less +7 -0
- package/src/primitives/StatusBar.tsx +10 -3
- package/src/primitives/Toolbar.less +50 -3
- package/src/primitives/Toolbar.test.tsx +67 -1
- package/src/primitives/Toolbar.tsx +72 -6
- package/src/primitives/ZoomControl.less +31 -0
- package/src/primitives/ZoomControl.tsx +76 -0
- package/src/primitives/floating.entry.test.tsx +14 -0
- package/src/primitives/index.ts +10 -2
- package/src/primitives/useRovingTabIndex.test.ts +27 -0
- package/src/primitives/useRovingTabIndex.ts +79 -0
- package/src/state/store.ts +16 -2
- package/src/state/toolSlot.test.ts +36 -0
- package/src/state/types.ts +8 -2
- package/src/state/view.test.ts +127 -0
- package/src/state/view.ts +18 -0
- package/src/styles.less +6 -0
- package/src/surface/AGENTS.md +64 -0
- package/src/surface/SurfaceContext.ts +5 -0
- package/src/surface/composeRects.test.ts +50 -0
- package/src/surface/composeRects.ts +19 -0
- package/src/surface/deviceRect.test.ts +40 -0
- package/src/surface/deviceRect.ts +19 -0
- package/src/surface/index.ts +7 -0
- package/src/surface/rect.ts +16 -0
- package/src/surface/useSurfaceTile.test.tsx +67 -0
- package/src/surface/useSurfaceTile.ts +32 -0
- package/src/surface/useTiledSurface.test.tsx +231 -0
- package/src/surface/useTiledSurface.ts +161 -0
- package/src/test-setup.ts +26 -0
- package/src/theme/base.less +19 -2
- package/src/theme/interstellar.tokens.json +6 -2
- package/src/tools/types.ts +24 -0
- package/src/trial/Trial.config.test.tsx +113 -0
- package/src/trial/Trial.job.test.tsx +73 -0
- package/src/trial/Trial.less +121 -4
- package/src/trial/Trial.stories.tsx +9 -1
- package/src/trial/Trial.test.tsx +67 -96
- package/src/trial/Trial.tsx +112 -45
- package/src/trial/TrialChrome.tsx +110 -64
- package/src/trial/TrialDragContext.ts +10 -0
- package/src/trial/TrialTitleBar.tsx +41 -0
- package/src/trial/index.ts +1 -15
- package/src/trial/trialChrome.borders.test.ts +38 -0
- package/src/ui/layers/LayerStack.less +9 -9
- package/src/ui/layers/LayerStack.tsx +1 -13
- package/src/ui/properties/CurveField.less +2 -2
- package/src/ui/properties/CurveField.test.tsx +11 -11
- package/src/ui/properties/PropertyGroup.less +2 -2
- package/src/ui/properties/PropertyPanel.less +55 -41
- package/src/ui/properties/SpeechBalloonPanels.stories.tsx +4 -2
- package/src/ui/properties/storyLayouts.tsx +2 -2
- package/dist/_dts/index-iAP6XbH3.d.ts +0 -132
- package/dist/_dts/types-x92Kfeme.d.ts +0 -62
- package/dist/chunk-3TYUJR7Z.js.map +0 -1
- package/dist/chunk-574LJAV4.js.map +0 -1
- package/dist/chunk-5R2ATYPJ.js +0 -201
- package/dist/chunk-5R2ATYPJ.js.map +0 -1
- package/dist/chunk-C6GJKPUI.js +0 -5986
- package/dist/chunk-C6GJKPUI.js.map +0 -1
- package/dist/chunk-DJLDIRFN.js.map +0 -1
- package/dist/chunk-NRKWVTVT.js.map +0 -1
- package/dist/chunk-PMAU3SEE.js +0 -126
- package/dist/chunk-PMAU3SEE.js.map +0 -1
- package/dist/chunk-SFL7NFKN.js.map +0 -1
- package/src/instrument/capabilityDetector.test.ts +0 -64
- package/src/instrument/capabilityDetector.ts +0 -20
- package/src/trial/DefaultSidebar.tsx +0 -29
- package/src/trial/DefaultStatusBar.tsx +0 -19
- package/src/trial/DefaultToolbar.tsx +0 -92
- package/src/trial/slotTypes.ts +0 -53
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { IconComponent } from '../chrome/types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A tool a trial can be in. labkit's own — core's `ToolsApi` carries hotkey
|
|
5
|
+
* slots, ambient tools, eligibility tiers and canvas overlay layers, all bound
|
|
6
|
+
* to the gesture dispatcher, and a labkit instrument is an arbitrary canvas or
|
|
7
|
+
* DOM tree rather than a weasel scene.
|
|
8
|
+
*/
|
|
9
|
+
export interface TrialTool {
|
|
10
|
+
id: string;
|
|
11
|
+
label: string;
|
|
12
|
+
icon: IconComponent;
|
|
13
|
+
/** Shown in the tooltip. Not bound here — the instrument owns its keymap. */
|
|
14
|
+
shortcut?: string;
|
|
15
|
+
/** Presentation grouping in the palette. Ungrouped tools sort after grouped. */
|
|
16
|
+
group?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** What an instrument declares to get a palette region. */
|
|
20
|
+
export interface ToolCapability {
|
|
21
|
+
tools: TrialTool[];
|
|
22
|
+
/** Which tool a fresh trial starts in. Defaults to the first. */
|
|
23
|
+
initial?: string;
|
|
24
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { f } from '../config/builder';
|
|
4
|
+
import type { ConfigRule, ControlRenderer } from '../config/types';
|
|
5
|
+
import { defineInstrument } from '../instrument/defineInstrument';
|
|
6
|
+
import { Lab } from '../lab/Lab';
|
|
7
|
+
|
|
8
|
+
const schemaInstrument = defineInstrument({
|
|
9
|
+
name: 'Schema',
|
|
10
|
+
config: f.schema({
|
|
11
|
+
showGrid: f.boolean(true),
|
|
12
|
+
cellSize: f.number(20).range(5, 80).step(5).label('Grid spacing'),
|
|
13
|
+
}),
|
|
14
|
+
initialState: () => ({}),
|
|
15
|
+
render: () => null,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const legacyInstrument = defineInstrument({
|
|
19
|
+
name: 'Legacy',
|
|
20
|
+
defaultConfig: () => ({ showGrid: true }),
|
|
21
|
+
configSchema: () => [
|
|
22
|
+
{ type: 'checkbox' as const, key: 'showGrid', label: 'Show grid', default: true },
|
|
23
|
+
],
|
|
24
|
+
initialState: () => ({}),
|
|
25
|
+
render: () => null,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('a trial renders its instrument config', () => {
|
|
29
|
+
it('renders a builder schema into the settings sidebar', () => {
|
|
30
|
+
render(<Lab instruments={[schemaInstrument]} defaultInstrument="Schema" />);
|
|
31
|
+
expect(screen.getByLabelText('Show grid')).toBeInTheDocument();
|
|
32
|
+
expect(screen.getByText('Grid spacing')).toBeInTheDocument();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('renders a legacy ConfigField list through the same path', () => {
|
|
36
|
+
render(<Lab instruments={[legacyInstrument]} defaultInstrument="Legacy" />);
|
|
37
|
+
expect(screen.getByLabelText('Show grid')).toBeInTheDocument();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('writes a config change back to the trial', () => {
|
|
41
|
+
render(<Lab instruments={[schemaInstrument]} defaultInstrument="Schema" />);
|
|
42
|
+
const checkbox = screen.getByLabelText('Show grid') as HTMLInputElement;
|
|
43
|
+
expect(checkbox.checked).toBe(true);
|
|
44
|
+
fireEvent.click(checkbox);
|
|
45
|
+
expect((screen.getByLabelText('Show grid') as HTMLInputElement).checked).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('lab-wide config seams', () => {
|
|
50
|
+
it('applies a lab-wide rule before labkit inference', () => {
|
|
51
|
+
const inst = defineInstrument({
|
|
52
|
+
name: 'Ruled',
|
|
53
|
+
config: f.schema({ tintColor: f.value('#ffffff') }),
|
|
54
|
+
initialState: () => ({}),
|
|
55
|
+
render: () => null,
|
|
56
|
+
});
|
|
57
|
+
const colorByName: ConfigRule = (ctx) =>
|
|
58
|
+
ctx.key.endsWith('Color') ? { kind: 'color' } : null;
|
|
59
|
+
render(
|
|
60
|
+
<Lab instruments={[inst]} defaultInstrument="Ruled" configRules={[colorByName]} />,
|
|
61
|
+
);
|
|
62
|
+
expect(screen.getByLabelText('Tint color')).toHaveAttribute('type', 'color');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('without the rule the same leaf falls back to a text input', () => {
|
|
66
|
+
const inst = defineInstrument({
|
|
67
|
+
name: 'Unruled',
|
|
68
|
+
config: f.schema({ tintColor: f.value('#ffffff') }),
|
|
69
|
+
initialState: () => ({}),
|
|
70
|
+
render: () => null,
|
|
71
|
+
});
|
|
72
|
+
render(<Lab instruments={[inst]} defaultInstrument="Unruled" />);
|
|
73
|
+
expect(screen.getByLabelText('Tint color')).toHaveAttribute('type', 'text');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('supplies a lab-wide control for a kind labkit does not ship', () => {
|
|
77
|
+
const inst = defineInstrument({
|
|
78
|
+
name: 'Custom',
|
|
79
|
+
config: f.schema({ offset: f.custom('vector2', { x: 3 }) }),
|
|
80
|
+
initialState: () => ({}),
|
|
81
|
+
render: () => null,
|
|
82
|
+
});
|
|
83
|
+
const vector2: ControlRenderer = (ctx) => (
|
|
84
|
+
<span>vec:{String((ctx.value as { x: number }).x)}</span>
|
|
85
|
+
);
|
|
86
|
+
render(
|
|
87
|
+
<Lab instruments={[inst]} defaultInstrument="Custom" controls={{ vector2 }} />,
|
|
88
|
+
);
|
|
89
|
+
expect(screen.getByText('vec:3')).toBeInTheDocument();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('shows the placeholder when no control is supplied for that kind', () => {
|
|
93
|
+
const inst = defineInstrument({
|
|
94
|
+
name: 'Unwired',
|
|
95
|
+
config: f.schema({ offset: f.custom('vector2', { x: 3 }) }),
|
|
96
|
+
initialState: () => ({}),
|
|
97
|
+
render: () => null,
|
|
98
|
+
});
|
|
99
|
+
render(<Lab instruments={[inst]} defaultInstrument="Unwired" />);
|
|
100
|
+
expect(screen.getByText(/vector2/)).toBeInTheDocument();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('omits the settings section for an instrument with no config', () => {
|
|
104
|
+
const inst = defineInstrument({
|
|
105
|
+
name: 'Bare',
|
|
106
|
+
defaultConfig: () => ({}),
|
|
107
|
+
initialState: () => ({}),
|
|
108
|
+
render: () => null,
|
|
109
|
+
});
|
|
110
|
+
render(<Lab instruments={[inst]} defaultInstrument="Bare" />);
|
|
111
|
+
expect(screen.queryByText('Settings')).not.toBeInTheDocument();
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import { defineInstrument } from '../instrument/defineInstrument';
|
|
5
|
+
import type { JobEvent } from '../job/types';
|
|
6
|
+
import { Lab } from '../lab/Lab';
|
|
7
|
+
|
|
8
|
+
interface S {
|
|
9
|
+
items: number[];
|
|
10
|
+
}
|
|
11
|
+
interface C {
|
|
12
|
+
n: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function makeInstrument(name: string, n: number, delay: number) {
|
|
16
|
+
return defineInstrument<S, C, number>({
|
|
17
|
+
name,
|
|
18
|
+
defaultConfig: () => ({ n }),
|
|
19
|
+
initialState: () => ({ items: [] }),
|
|
20
|
+
render: () => null,
|
|
21
|
+
job: {
|
|
22
|
+
auto: true,
|
|
23
|
+
run: async function* ({ config, signal }): AsyncGenerator<JobEvent<number>> {
|
|
24
|
+
yield { kind: 'total', total: config.n };
|
|
25
|
+
for (let i = 0; i < config.n; i++) {
|
|
26
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
27
|
+
if (signal.aborted) return;
|
|
28
|
+
yield { kind: 'item', item: i };
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
onItem: (item, state) => ({ items: [...state.items, item] }),
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe('a trial whose instrument declares a job', () => {
|
|
37
|
+
it('shows progress in the trial chrome', async () => {
|
|
38
|
+
const fast = makeInstrument('fast', 3, 1);
|
|
39
|
+
render(<Lab instruments={[fast]} defaultInstrument="fast" storage={null} />);
|
|
40
|
+
const bar = await waitFor(() => screen.getByRole('progressbar', { name: /job progress/i }));
|
|
41
|
+
await waitFor(() => {
|
|
42
|
+
// A job that reported a total drives a determinate bar, so the count and
|
|
43
|
+
// the accessible value have to agree.
|
|
44
|
+
expect(bar).toHaveAttribute('aria-valuemax', '3');
|
|
45
|
+
expect(bar).toHaveAttribute('aria-valuenow', '3');
|
|
46
|
+
});
|
|
47
|
+
expect(document.querySelector('.lk-job__count')?.textContent).toMatch(/3\s*\/\s*3/);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('offers a cancel control while running, and drops it once cancelled', async () => {
|
|
51
|
+
const slow = makeInstrument('slow', 40, 20);
|
|
52
|
+
render(<Lab instruments={[slow]} defaultInstrument="slow" storage={null} />);
|
|
53
|
+
const cancel = await waitFor(() => screen.getByRole('button', { name: /cancel/i }));
|
|
54
|
+
await userEvent.click(cancel);
|
|
55
|
+
await waitFor(() => {
|
|
56
|
+
expect(screen.queryByRole('button', { name: /cancel/i })).not.toBeInTheDocument();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('renders no job chrome for an instrument that declares none', async () => {
|
|
61
|
+
const plain = defineInstrument<S, C>({
|
|
62
|
+
name: 'plain',
|
|
63
|
+
defaultConfig: () => ({ n: 0 }),
|
|
64
|
+
initialState: () => ({ items: [] }),
|
|
65
|
+
render: () => null,
|
|
66
|
+
});
|
|
67
|
+
render(<Lab instruments={[plain]} defaultInstrument="plain" storage={null} />);
|
|
68
|
+
await waitFor(() => {
|
|
69
|
+
expect(document.querySelector('.lk-trial')).toBeInTheDocument();
|
|
70
|
+
});
|
|
71
|
+
expect(document.querySelector('.lk-trial__job')).not.toBeInTheDocument();
|
|
72
|
+
});
|
|
73
|
+
});
|
package/src/trial/Trial.less
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
.lk-trial {
|
|
2
2
|
display: flex;
|
|
3
|
+
// Click-focusable so the trial's own shortcuts reach it; the trial is not a
|
|
4
|
+
// control, so it draws no focus ring of its own.
|
|
5
|
+
outline: none;
|
|
3
6
|
flex-direction: column;
|
|
4
7
|
height: 100%;
|
|
5
|
-
|
|
6
|
-
|
|
8
|
+
// The lab paints --wzl-surface, so a trial on the same token was separated
|
|
9
|
+
// from its own workspace by nothing but a hairline. Raised reads as a panel,
|
|
10
|
+
// and in dark it is translucent, so the backdrop still shows through.
|
|
11
|
+
background: var(--wzl-surface-raised);
|
|
12
|
+
border: var(--wzl-border-w) solid var(--wzl-border-raised);
|
|
7
13
|
border-radius: var(--wzl-radius-md);
|
|
14
|
+
box-shadow: 0 1px 3px var(--wzl-shadow);
|
|
8
15
|
overflow: hidden;
|
|
9
16
|
|
|
10
17
|
&__body {
|
|
@@ -15,21 +22,52 @@
|
|
|
15
22
|
|
|
16
23
|
&__sidebar {
|
|
17
24
|
flex-shrink: 0;
|
|
25
|
+
display: flex;
|
|
26
|
+
flex-direction: column;
|
|
27
|
+
min-height: 0;
|
|
28
|
+
// Sections floor at their content height, so without this the last ones
|
|
29
|
+
// are cut rather than reachable.
|
|
30
|
+
overflow-y: auto;
|
|
18
31
|
}
|
|
19
32
|
|
|
33
|
+
// An instrument's rendered output is dropped in here directly, so the
|
|
34
|
+
// content well is what makes `height: 100%` mean anything to it. Without
|
|
35
|
+
// the column context and the stretched child, anything a lab returns
|
|
36
|
+
// collapses to its intrinsic height and the trial looks empty below it.
|
|
20
37
|
&__content {
|
|
38
|
+
// The viewport region anchors against this well.
|
|
39
|
+
position: relative;
|
|
21
40
|
flex: 1;
|
|
41
|
+
min-width: 0;
|
|
42
|
+
min-height: 0;
|
|
43
|
+
display: flex;
|
|
44
|
+
flex-direction: column;
|
|
22
45
|
overflow: auto;
|
|
46
|
+
|
|
47
|
+
> * {
|
|
48
|
+
flex: 1 1 auto;
|
|
49
|
+
min-height: 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// An instrument owns its own interior; the frame contributes chrome, not
|
|
53
|
+
// inset. `--flush` is now only about scrolling.
|
|
54
|
+
&--flush {
|
|
55
|
+
overflow: hidden;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
&__canvas-host {
|
|
60
|
+
position: relative;
|
|
61
|
+
width: 100%;
|
|
62
|
+
height: 100%;
|
|
23
63
|
}
|
|
24
64
|
|
|
25
65
|
&__toolbar {
|
|
26
66
|
flex-shrink: 0;
|
|
27
|
-
border-bottom: 1px solid var(--wzl-line-subtle);
|
|
28
67
|
}
|
|
29
68
|
|
|
30
69
|
&__status {
|
|
31
70
|
flex-shrink: 0;
|
|
32
|
-
border-top: 1px solid var(--wzl-line-subtle);
|
|
33
71
|
}
|
|
34
72
|
|
|
35
73
|
&--unknown {
|
|
@@ -51,8 +89,87 @@
|
|
|
51
89
|
font-size: var(--wzl-font-size-sm);
|
|
52
90
|
}
|
|
53
91
|
|
|
92
|
+
// Same `width: 100%` default as the header's Select — pinned so it cannot
|
|
93
|
+
// absorb the toolbar's slack. Placeholder-only, so it need only fit "Load…".
|
|
94
|
+
.lk-root .lk-toolbar__load-select {
|
|
95
|
+
width: 88px;
|
|
96
|
+
}
|
|
97
|
+
|
|
54
98
|
.lk-sidebar__placeholder {
|
|
55
99
|
padding: var(--wzl-space-sm);
|
|
56
100
|
color: var(--wzl-fg-muted);
|
|
57
101
|
font-size: var(--wzl-font-size-sm);
|
|
58
102
|
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
.lk-trial__titlebar {
|
|
106
|
+
display: flex;
|
|
107
|
+
align-items: center;
|
|
108
|
+
gap: var(--wzl-space-sm);
|
|
109
|
+
flex-shrink: 0;
|
|
110
|
+
padding: 0 var(--wzl-space-sm);
|
|
111
|
+
min-height: calc(var(--wzl-font-size-sm) * var(--wzl-leading) + var(--wzl-space-xs));
|
|
112
|
+
background: var(--wzl-surface-sunken);
|
|
113
|
+
border-bottom: var(--wzl-border-w) solid var(--wzl-border);
|
|
114
|
+
border-radius: var(--wzl-radius-md) var(--wzl-radius-md) 0 0;
|
|
115
|
+
|
|
116
|
+
// The bar is the drag surface, so it carries the cursor rather than an
|
|
117
|
+
// affordance of its own.
|
|
118
|
+
&--draggable {
|
|
119
|
+
cursor: grab;
|
|
120
|
+
touch-action: none;
|
|
121
|
+
|
|
122
|
+
&:active { cursor: grabbing; }
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.lk-trial__titlebar-actions {
|
|
127
|
+
display: inline-flex;
|
|
128
|
+
align-items: center;
|
|
129
|
+
gap: 2px;
|
|
130
|
+
margin-left: auto;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Prefixed with `.lk-root` to outrank base.less's `:where(button)` default,
|
|
134
|
+
// which would give a 12px glyph a 24px chrome box on the title bar.
|
|
135
|
+
.lk-root .lk-titlebar-button {
|
|
136
|
+
display: inline-flex;
|
|
137
|
+
align-items: center;
|
|
138
|
+
justify-content: center;
|
|
139
|
+
width: 18px;
|
|
140
|
+
height: 18px;
|
|
141
|
+
padding: 0;
|
|
142
|
+
border: 0;
|
|
143
|
+
border-radius: var(--wzl-radius-sm);
|
|
144
|
+
background: transparent;
|
|
145
|
+
backdrop-filter: none;
|
|
146
|
+
-webkit-backdrop-filter: none;
|
|
147
|
+
// Matches .lk-trial__title — the X reads as part of the bar's own type,
|
|
148
|
+
// not as chrome sitting on top of it.
|
|
149
|
+
color: var(--wzl-fg-muted);
|
|
150
|
+
cursor: pointer;
|
|
151
|
+
|
|
152
|
+
&:hover {
|
|
153
|
+
background: var(--wzl-surface-hover);
|
|
154
|
+
color: var(--wzl-fg);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
&:disabled {
|
|
158
|
+
opacity: 0.4;
|
|
159
|
+
cursor: default;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
&--danger:hover:not(:disabled) {
|
|
163
|
+
background: color-mix(in srgb, var(--wzl-danger) 15%, transparent);
|
|
164
|
+
color: var(--wzl-danger);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.lk-trial__title {
|
|
169
|
+
font-size: var(--wzl-font-size-sm);
|
|
170
|
+
font-weight: var(--wzl-font-weight-bold);
|
|
171
|
+
color: var(--wzl-fg-muted);
|
|
172
|
+
white-space: nowrap;
|
|
173
|
+
overflow: hidden;
|
|
174
|
+
text-overflow: ellipsis;
|
|
175
|
+
}
|
|
@@ -5,6 +5,7 @@ import { noneAdapter } from '../state/adapters';
|
|
|
5
5
|
import { LabStoreContext } from '../state/context';
|
|
6
6
|
import { createLabStore } from '../state/store';
|
|
7
7
|
import type { TrialRecord } from '../state/types';
|
|
8
|
+
import { DEFAULT_VIEW } from '../state/view';
|
|
8
9
|
import { TrialChrome } from './TrialChrome';
|
|
9
10
|
|
|
10
11
|
const noop = () => {};
|
|
@@ -52,7 +53,14 @@ function Harness() {
|
|
|
52
53
|
config: {},
|
|
53
54
|
setState: () => {},
|
|
54
55
|
setConfig: () => {},
|
|
55
|
-
trial: {
|
|
56
|
+
trial: {
|
|
57
|
+
id: 'ws-demo',
|
|
58
|
+
view: DEFAULT_VIEW,
|
|
59
|
+
setView: () => {},
|
|
60
|
+
zoom: 1,
|
|
61
|
+
setZoom: () => {},
|
|
62
|
+
activeToolId: null,
|
|
63
|
+
},
|
|
56
64
|
emit: () => {},
|
|
57
65
|
})}
|
|
58
66
|
</TrialChrome>
|
package/src/trial/Trial.test.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { fireEvent, render, screen } from '@testing-library/react';
|
|
1
|
+
import { fireEvent, render, screen, within } from '@testing-library/react';
|
|
2
2
|
import type { ReactNode } from 'react';
|
|
3
3
|
import { describe, expect, it, vi } from 'vitest';
|
|
4
4
|
import type { Instrument } from '../instrument/types';
|
|
@@ -6,92 +6,9 @@ import { Lab } from '../lab/Lab';
|
|
|
6
6
|
import { LabContext, type LabContextValue } from '../lab/LabContext';
|
|
7
7
|
import { LabStoreContext } from '../state/context';
|
|
8
8
|
import { createLabStore } from '../state/store';
|
|
9
|
-
import type { TrialRecord } from '../state/types';
|
|
10
|
-
import { DefaultToolbar } from './DefaultToolbar';
|
|
11
|
-
import type { TrialToolbarContext } from './slotTypes';
|
|
9
|
+
import type { SavedSnapshot, TrialRecord } from '../state/types';
|
|
12
10
|
import { TrialChrome } from './TrialChrome';
|
|
13
11
|
|
|
14
|
-
function makeCtx(overrides: Partial<TrialToolbarContext> = {}): TrialToolbarContext {
|
|
15
|
-
return {
|
|
16
|
-
trialId: 'ws-1',
|
|
17
|
-
instrumentName: 'Stub',
|
|
18
|
-
hasUndo: false,
|
|
19
|
-
canUndo: false,
|
|
20
|
-
canRedo: false,
|
|
21
|
-
undo: vi.fn(),
|
|
22
|
-
redo: vi.fn(),
|
|
23
|
-
zoom: 1,
|
|
24
|
-
setZoom: vi.fn(),
|
|
25
|
-
zoomIn: vi.fn(),
|
|
26
|
-
zoomOut: vi.fn(),
|
|
27
|
-
resetZoom: vi.fn(),
|
|
28
|
-
hasCanvas: false,
|
|
29
|
-
savedSnapshots: [],
|
|
30
|
-
saveSnapshot: vi.fn(),
|
|
31
|
-
loadSnapshot: vi.fn(),
|
|
32
|
-
clone: vi.fn(),
|
|
33
|
-
reset: vi.fn(),
|
|
34
|
-
close: vi.fn(),
|
|
35
|
-
isLastTrial: false,
|
|
36
|
-
...overrides,
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
describe('<DefaultToolbar>', () => {
|
|
41
|
-
it('renders close button', () => {
|
|
42
|
-
render(<DefaultToolbar ctx={makeCtx()} />);
|
|
43
|
-
expect(screen.getByRole('button', { name: /close/i })).toBeInTheDocument();
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it('disables close when isLastTrial is true', () => {
|
|
47
|
-
render(<DefaultToolbar ctx={makeCtx({ isLastTrial: true })} />);
|
|
48
|
-
expect(screen.getByRole('button', { name: /close/i })).toBeDisabled();
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('omits undo/redo buttons when hasUndo is false', () => {
|
|
52
|
-
render(<DefaultToolbar ctx={makeCtx({ hasUndo: false })} />);
|
|
53
|
-
expect(screen.queryByRole('button', { name: /undo/i })).toBeNull();
|
|
54
|
-
expect(screen.queryByRole('button', { name: /redo/i })).toBeNull();
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('shows undo/redo buttons when hasUndo is true', () => {
|
|
58
|
-
render(<DefaultToolbar ctx={makeCtx({ hasUndo: true, canUndo: true, canRedo: false })} />);
|
|
59
|
-
expect(screen.getByRole('button', { name: /undo/i })).not.toBeDisabled();
|
|
60
|
-
expect(screen.getByRole('button', { name: /redo/i })).toBeDisabled();
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('omits zoom buttons when hasCanvas is false', () => {
|
|
64
|
-
render(<DefaultToolbar ctx={makeCtx({ hasCanvas: false })} />);
|
|
65
|
-
expect(screen.queryByTitle('Zoom in')).toBeNull();
|
|
66
|
-
expect(screen.queryByTitle('Zoom out')).toBeNull();
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('shows zoom controls when hasCanvas is true', () => {
|
|
70
|
-
render(<DefaultToolbar ctx={makeCtx({ hasCanvas: true, zoom: 1.5 })} />);
|
|
71
|
-
expect(screen.getByTitle('Zoom in')).toBeInTheDocument();
|
|
72
|
-
expect(screen.getByTitle('Zoom out')).toBeInTheDocument();
|
|
73
|
-
expect(screen.getByText('150%')).toBeInTheDocument();
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('shows load select when snapshots exist', () => {
|
|
77
|
-
const ctx = makeCtx({
|
|
78
|
-
savedSnapshots: [
|
|
79
|
-
{
|
|
80
|
-
id: 's1',
|
|
81
|
-
name: 'First',
|
|
82
|
-
trialId: 'ws-1',
|
|
83
|
-
instrumentName: 'Stub',
|
|
84
|
-
config: {},
|
|
85
|
-
state: {},
|
|
86
|
-
savedAt: 1,
|
|
87
|
-
},
|
|
88
|
-
],
|
|
89
|
-
});
|
|
90
|
-
render(<DefaultToolbar ctx={ctx} />);
|
|
91
|
-
expect(screen.getByRole('combobox', { name: /load snapshot/i })).toBeInTheDocument();
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
|
-
|
|
95
12
|
const stubInstrument: Instrument = {
|
|
96
13
|
name: 'Stub',
|
|
97
14
|
defaultConfig: () => ({}),
|
|
@@ -113,6 +30,7 @@ type ChromeProps = Parameters<typeof TrialChrome>[0];
|
|
|
113
30
|
function ChromeHarness({
|
|
114
31
|
children,
|
|
115
32
|
labOverrides,
|
|
33
|
+
instrument = stubInstrument,
|
|
116
34
|
...props
|
|
117
35
|
}: { children?: ReactNode; labOverrides?: Partial<LabContextValue> } & Partial<ChromeProps>) {
|
|
118
36
|
const store = createLabStore({
|
|
@@ -120,7 +38,7 @@ function ChromeHarness({
|
|
|
120
38
|
storage: { read: () => null, write: () => {} },
|
|
121
39
|
});
|
|
122
40
|
const labCtx: LabContextValue = {
|
|
123
|
-
instruments: [
|
|
41
|
+
instruments: [instrument],
|
|
124
42
|
trials: [stubRecord],
|
|
125
43
|
addTrial: vi.fn(),
|
|
126
44
|
cloneTrial: vi.fn(),
|
|
@@ -141,7 +59,7 @@ function ChromeHarness({
|
|
|
141
59
|
<TrialChrome
|
|
142
60
|
trialId="ws-1"
|
|
143
61
|
record={stubRecord}
|
|
144
|
-
instrument={
|
|
62
|
+
instrument={instrument}
|
|
145
63
|
isLastTrial={false}
|
|
146
64
|
{...props}
|
|
147
65
|
>
|
|
@@ -158,15 +76,47 @@ describe('<TrialChrome>', () => {
|
|
|
158
76
|
expect(screen.getByTestId('content')).toBeInTheDocument();
|
|
159
77
|
});
|
|
160
78
|
|
|
161
|
-
it('renders
|
|
79
|
+
it('renders the built-in trial actions', () => {
|
|
80
|
+
render(<ChromeHarness />);
|
|
81
|
+
expect(screen.getByRole('button', { name: 'Close trial' })).toBeInTheDocument();
|
|
82
|
+
expect(screen.getByRole('button', { name: 'Clone trial' })).toBeInTheDocument();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('disables close on the last trial', () => {
|
|
86
|
+
render(<ChromeHarness isLastTrial />);
|
|
87
|
+
expect(screen.getByRole('button', { name: /close/i })).toBeDisabled();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('omits undo and redo unless the instrument declares undo', () => {
|
|
162
91
|
render(<ChromeHarness />);
|
|
163
|
-
expect(screen.
|
|
92
|
+
expect(screen.queryByRole('button', { name: 'Undo' })).toBeNull();
|
|
93
|
+
render(
|
|
94
|
+
<ChromeHarness
|
|
95
|
+
instrument={{ ...stubInstrument, undo: {} }}
|
|
96
|
+
undoBindings={{ canUndo: true, canRedo: false, undo: vi.fn(), redo: vi.fn() }}
|
|
97
|
+
/>,
|
|
98
|
+
);
|
|
99
|
+
expect(screen.getByRole('button', { name: 'Undo' })).not.toBeDisabled();
|
|
100
|
+
expect(screen.getByRole('button', { name: 'Redo' })).toBeDisabled();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('offers the snapshot loader once a snapshot exists', () => {
|
|
104
|
+
const snapshot: SavedSnapshot = {
|
|
105
|
+
id: 's1',
|
|
106
|
+
name: 'First',
|
|
107
|
+
trialId: 'ws-1',
|
|
108
|
+
instrumentName: 'Stub',
|
|
109
|
+
config: {},
|
|
110
|
+
state: {},
|
|
111
|
+
savedAt: 1,
|
|
112
|
+
};
|
|
113
|
+
render(<ChromeHarness labOverrides={{ savedSnapshots: [snapshot] }} />);
|
|
114
|
+
expect(screen.getByRole('button', { name: /load snapshot/i })).toBeInTheDocument();
|
|
164
115
|
});
|
|
165
116
|
|
|
166
|
-
it('renders
|
|
167
|
-
render(<ChromeHarness
|
|
168
|
-
expect(screen.
|
|
169
|
-
expect(screen.queryByRole('button', { name: /close/i })).toBeNull();
|
|
117
|
+
it('renders a consumer contribution alongside the built-ins', () => {
|
|
118
|
+
render(<ChromeHarness chrome={[{ id: 'mine', region: 'status', item: { text: 'ready' } }]} />);
|
|
119
|
+
expect(screen.getByText('ready')).toBeInTheDocument();
|
|
170
120
|
});
|
|
171
121
|
|
|
172
122
|
it('Cmd+S triggers saveSnapshot', () => {
|
|
@@ -178,10 +128,31 @@ describe('<TrialChrome>', () => {
|
|
|
178
128
|
});
|
|
179
129
|
});
|
|
180
130
|
|
|
181
|
-
describe('
|
|
131
|
+
describe('chrome regions in a mounted lab', () => {
|
|
132
|
+
function renderLabWith(instrument: Instrument) {
|
|
133
|
+
return render(<Lab title="T" instruments={[instrument]} defaultInstrument={instrument.name} />);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
it('puts zoom in the viewport region and not in the toolbar', () => {
|
|
137
|
+
renderLabWith({ ...stubInstrument, canvas: { layers: [] }, undo: {} });
|
|
138
|
+
const toolbar = document.querySelector('.lk-trial__toolbar') as HTMLElement;
|
|
139
|
+
const viewport = document.querySelector('.lk-viewport-controls') as HTMLElement;
|
|
140
|
+
expect(within(viewport).getByRole('button', { name: 'Zoom in' })).toBeInTheDocument();
|
|
141
|
+
expect(within(toolbar).queryByRole('button', { name: 'Zoom in' })).toBeNull();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('renders no undo group for an instrument that does not declare undo', () => {
|
|
145
|
+
renderLabWith({ ...stubInstrument, canvas: { layers: [] } });
|
|
146
|
+
expect(screen.queryByRole('button', { name: 'Undo' })).toBeNull();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('renders no viewport region for an instrument with no canvas', () => {
|
|
150
|
+
renderLabWith(stubInstrument);
|
|
151
|
+
expect(document.querySelector('.lk-viewport-controls')).toBeNull();
|
|
152
|
+
});
|
|
153
|
+
|
|
182
154
|
it('Lab provides context for nested TrialChrome', () => {
|
|
183
|
-
|
|
184
|
-
// Lab seeds a trial; no chrome rendered without children — sanity check Lab mounts.
|
|
155
|
+
renderLabWith(stubInstrument);
|
|
185
156
|
expect(document.querySelector('.lk-lab')).toBeTruthy();
|
|
186
157
|
});
|
|
187
158
|
});
|