@weasel-js/labkit 1.4.0 → 1.4.2
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 +8 -0
- package/dist/_dts/{CanvasStackContext-D6M0o8cQ.d.ts → CanvasStackContext-kjILVnPj.d.ts} +1 -1
- package/dist/_dts/PrefsForm.d-CgxUequc.d.ts +20 -0
- package/dist/_dts/{frac-QS7XzvpG.d.ts → frac-A4R6v4ld.d.ts} +36 -13
- package/dist/_dts/{index-B2X21G1c.d.ts → index-CsU9WhjM.d.ts} +90 -30
- package/dist/_dts/{types-ChVJJHvk.d.ts → types-BbXpvQa8.d.ts} +20 -1
- package/dist/_dts/{types-C1Zw7s5P.d.ts → types-lg4TSCb2.d.ts} +2 -1
- package/dist/_dts/{useTrialState-Cdm13fvl.d.ts → useTrialState-DKjqpv20.d.ts} +5 -1
- package/dist/_dts/weasel-canvas-FJTFi3ZZ.d.ts +5041 -0
- package/dist/canvas/index.d.ts +16 -16
- package/dist/canvas/index.js +2 -2
- package/dist/chrome/index.d.ts +15 -9
- package/dist/chrome/index.js +5 -5
- package/dist/{chunk-7VW3S44V.js → chunk-54ZWZ5FQ.js} +908 -301
- package/dist/chunk-54ZWZ5FQ.js.map +1 -0
- package/dist/{chunk-Y2HK44TG.js → chunk-D5KQ5OY6.js} +3 -3
- package/dist/{chunk-Y2HK44TG.js.map → chunk-D5KQ5OY6.js.map} +1 -1
- package/dist/{chunk-VHFQKAUL.js → chunk-FJG4PHTL.js} +83 -75
- package/dist/chunk-FJG4PHTL.js.map +1 -0
- package/dist/{chunk-LN6JDUGB.js → chunk-HXXTULDN.js} +2 -2
- package/dist/{chunk-LN6JDUGB.js.map → chunk-HXXTULDN.js.map} +1 -1
- package/dist/{chunk-L5WBGUVR.js → chunk-ISSVF5PT.js} +2753 -2654
- package/dist/chunk-ISSVF5PT.js.map +1 -0
- package/dist/chunk-NSOVI3AZ.js +170 -0
- package/dist/chunk-NSOVI3AZ.js.map +1 -0
- package/dist/{chunk-YQGQUBKA.js → chunk-TN7YSJVU.js} +62 -60
- package/dist/chunk-TN7YSJVU.js.map +1 -0
- package/dist/{chunk-4AMN4MMB.js → chunk-UDXOYZEC.js} +53 -39
- package/dist/chunk-UDXOYZEC.js.map +1 -0
- package/dist/{chunk-LJSIUFYD.js → chunk-ULDW42CR.js} +23 -2
- package/dist/chunk-ULDW42CR.js.map +1 -0
- package/dist/{chunk-QR5V3AFG.js → chunk-UTOEDPNU.js} +5 -5
- package/dist/{chunk-QR5V3AFG.js.map → chunk-UTOEDPNU.js.map} +1 -1
- package/dist/{chunk-UD7TEHZT.js → chunk-W2FJR5FF.js} +35 -22
- package/dist/chunk-W2FJR5FF.js.map +1 -0
- package/dist/{chunk-B5VWJBWM.js → chunk-WS6ZRV75.js} +3 -3
- package/dist/{chunk-B5VWJBWM.js.map → chunk-WS6ZRV75.js.map} +1 -1
- package/dist/{chunk-W3KSUXXR.js → chunk-XKENZTNE.js} +45 -14
- package/dist/chunk-XKENZTNE.js.map +1 -0
- package/dist/controls/index.d.ts +4 -3
- package/dist/controls/index.js +3 -3
- package/dist/dragdrop/index.d.ts +8 -8
- package/dist/dragdrop/index.js +2 -2
- package/dist/index.d.ts +75 -110
- package/dist/index.js +148 -115
- package/dist/index.js.map +1 -1
- package/dist/job/index.js +1 -1
- package/dist/layers/index.d.ts +26 -11
- package/dist/layers/index.js +3 -3
- package/dist/loupe/index.d.ts +7 -13
- package/dist/loupe/index.js +2 -2
- package/dist/passthrough/weasel-canvas.d.ts +3 -343
- package/dist/passthrough/weasel-canvas.js +1 -1
- package/dist/passthrough/weasel-ui.d.ts +123 -3088
- package/dist/passthrough/weasel-ui.js +2 -2
- package/dist/primitives/index.js +4 -4
- package/dist/state/index.d.ts +3 -3
- package/dist/state/index.js +2 -2
- package/dist/styles.css +30 -3
- package/dist/surface/index.js +2 -2
- package/dist/ui/layers/index.d.ts +22 -9
- package/dist/ui/layers/index.js +2 -2
- package/dist/undo/index.d.ts +6 -5
- package/package.json +8 -8
- package/src/annotations/AnnotationTargets.tsx +6 -1
- package/src/annotations/Annotations.overlay.test.tsx +2 -0
- package/src/annotations/types.ts +4 -1
- package/src/canvas/CanvasStack.tsx +8 -13
- package/src/canvas/useOrbit.test.ts +97 -1
- package/src/canvas/useOrbit.ts +41 -34
- package/src/canvas/usePanZoom.test.ts +106 -1
- package/src/canvas/usePanZoom.ts +54 -36
- package/src/chrome/ChromeRegions.stories.tsx +42 -1
- package/src/chrome/builtins.test.ts +4 -0
- package/src/chrome/builtins.tsx +18 -0
- package/src/chrome/index.ts +1 -1
- package/src/chrome/regions/SidebarRegion.tsx +7 -6
- package/src/chrome/regions/TitleBarRegion.test.tsx +64 -0
- package/src/chrome/regions/TitleBarRegion.tsx +16 -7
- package/src/chrome/regions/ToolbarRegion.test.tsx +14 -0
- package/src/chrome/regions/ToolbarRegion.tsx +2 -2
- package/src/chrome/regions/ViewportRegion.tsx +2 -2
- package/src/chrome/regions/regions.test.tsx +45 -1
- package/src/chrome/types.ts +21 -3
- package/src/controls/ControlPanel.test.tsx +38 -0
- package/src/controls/ControlPanel.tsx +51 -6
- package/src/dragdrop/DragDropRuntime.tsx +74 -61
- package/src/dragdrop/Palette.tsx +4 -3
- package/src/dragdrop/dragDrop.test.tsx +35 -12
- package/src/index.ts +2 -1
- package/src/instrument/types.ts +4 -5
- package/src/job/useJob.ts +1 -1
- package/src/lab/Lab.test.tsx +7 -0
- package/src/lab/Lab.tsx +2 -2
- package/src/lab/LabContext.ts +5 -1
- package/src/lab/LabShell.less +17 -2
- package/src/lab/Workspace.tsx +1 -1
- package/src/layers/AGENTS.md +29 -7
- package/src/layers/LayerList.less +16 -0
- package/src/layers/LayerList.stories.tsx +66 -0
- package/src/layers/LayerList.test.tsx +185 -2
- package/src/layers/LayerList.tsx +221 -74
- package/src/layers/index.ts +1 -1
- package/src/passthrough/weasel-ui.ts +5 -0
- package/src/primitives/FloatingPanel.test.tsx +87 -0
- package/src/primitives/FloatingPanel.tsx +59 -41
- package/src/state/store.test.ts +78 -0
- package/src/state/store.ts +27 -0
- package/src/state/types.ts +20 -0
- package/src/trial/Trial.less +4 -0
- package/src/trial/Trial.targets.test.tsx +96 -0
- package/src/trial/Trial.test.tsx +171 -14
- package/src/trial/Trial.tsx +4 -1
- package/src/trial/TrialChrome.tsx +22 -2
- package/src/trial/TrialTitleBar.tsx +7 -3
- package/src/trial/index.ts +1 -0
- package/src/trial/trialOps.test.ts +49 -1
- package/src/trial/trialOps.ts +28 -6
- package/dist/_dts/DrawCommand-B3bskUsC.d.ts +0 -564
- package/dist/_dts/PrefsForm-BkUJZx0A.d.ts +0 -204
- package/dist/_dts/fitViewToBounds-dZ2UDB6e.d.ts +0 -21
- package/dist/_dts/shapeKinds-Cx_rxwsa.d.ts +0 -87
- package/dist/_dts/types-C-gh9Ap-.d.ts +0 -695
- package/dist/chunk-4AMN4MMB.js.map +0 -1
- package/dist/chunk-7VW3S44V.js.map +0 -1
- package/dist/chunk-L5WBGUVR.js.map +0 -1
- package/dist/chunk-LJSIUFYD.js.map +0 -1
- package/dist/chunk-UD7TEHZT.js.map +0 -1
- package/dist/chunk-VHFQKAUL.js.map +0 -1
- package/dist/chunk-W3KSUXXR.js.map +0 -1
- package/dist/chunk-XCBCZEBO.js +0 -94
- package/dist/chunk-XCBCZEBO.js.map +0 -1
- package/dist/chunk-YQGQUBKA.js.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
-
import { CrosshairIcon, HandIcon, PencilIcon } from '@weasel-js/ui';
|
|
2
|
+
import { CrosshairIcon, HandIcon, PencilIcon, SnapshotIcon } from '@weasel-js/ui';
|
|
3
3
|
import type { ConfigField } from '../controls/types';
|
|
4
4
|
import type { Instrument } from '../instrument/types';
|
|
5
5
|
import { Lab } from '../lab/Lab';
|
|
@@ -104,3 +104,44 @@ export const SuppressedAndReplaced: Story = {
|
|
|
104
104
|
],
|
|
105
105
|
},
|
|
106
106
|
};
|
|
107
|
+
|
|
108
|
+
const SUBJECTS = ['Ash', 'Birch', 'Cedar'];
|
|
109
|
+
|
|
110
|
+
/** A trial titled by its subject rather than its instrument, with the leading
|
|
111
|
+
* end of the title bar carrying the control that renames it — and the
|
|
112
|
+
* snapshot built-in re-declared into the toolbar, which needs the context
|
|
113
|
+
* `onActivate` is handed. */
|
|
114
|
+
export const SubjectTitleAndLead: Story = {
|
|
115
|
+
args: {
|
|
116
|
+
instruments: [FullInstrument],
|
|
117
|
+
defaultInstrument: 'Every Region',
|
|
118
|
+
title: 'Title Bar Lead',
|
|
119
|
+
storage: null,
|
|
120
|
+
suppress: ['snapshot'],
|
|
121
|
+
chrome: [
|
|
122
|
+
{
|
|
123
|
+
id: 'subject',
|
|
124
|
+
region: 'titlebar',
|
|
125
|
+
item: {
|
|
126
|
+
icon: PencilIcon,
|
|
127
|
+
label: 'Next subject',
|
|
128
|
+
onActivate: (ctx) => {
|
|
129
|
+
const i = SUBJECTS.indexOf(ctx.title);
|
|
130
|
+
ctx.setTitle(SUBJECTS[(i + 1) % SUBJECTS.length]);
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
id: 'snapshot-in-toolbar',
|
|
136
|
+
region: 'toolbar',
|
|
137
|
+
group: 'trial',
|
|
138
|
+
item: {
|
|
139
|
+
icon: SnapshotIcon,
|
|
140
|
+
label: 'Snapshot',
|
|
141
|
+
showLabel: true,
|
|
142
|
+
onActivate: (ctx) => ctx.saveSnapshot(),
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
},
|
|
147
|
+
};
|
|
@@ -9,9 +9,13 @@ import type { TrialChromeContext } from './types';
|
|
|
9
9
|
const ctx: TrialChromeContext = {
|
|
10
10
|
trialId: 't1',
|
|
11
11
|
undockedPanels: [],
|
|
12
|
+
collapsedSections: {},
|
|
13
|
+
setSectionCollapsed: () => {},
|
|
12
14
|
undockPanel: () => {},
|
|
13
15
|
dockPanel: () => {},
|
|
14
16
|
instrumentName: 'Stub',
|
|
17
|
+
title: 'Stub',
|
|
18
|
+
setTitle: () => {},
|
|
15
19
|
isLastTrial: false,
|
|
16
20
|
zoom: 1,
|
|
17
21
|
setZoom: () => {},
|
package/src/chrome/builtins.tsx
CHANGED
|
@@ -24,6 +24,22 @@ import type { TrialChromeContext, TrialContribution } from './types';
|
|
|
24
24
|
|
|
25
25
|
const ZOOM_STEP = 1.25;
|
|
26
26
|
|
|
27
|
+
/** The property groups inside a contribution are folded under keys prefixed
|
|
28
|
+
* with that contribution's id, so a group called `marks` cannot fold the
|
|
29
|
+
* sidebar section of the same name. `ControlPanel` keys by bare label, so the
|
|
30
|
+
* prefix goes on here and comes off again. */
|
|
31
|
+
function groupFolds(
|
|
32
|
+
collapsedSections: Readonly<Record<string, boolean>>,
|
|
33
|
+
id: string,
|
|
34
|
+
): Record<string, boolean> {
|
|
35
|
+
const prefix = `${id}/`;
|
|
36
|
+
const out: Record<string, boolean> = {};
|
|
37
|
+
for (const [key, value] of Object.entries(collapsedSections)) {
|
|
38
|
+
if (key.startsWith(prefix)) out[key.slice(prefix.length)] = value;
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
|
|
27
43
|
/**
|
|
28
44
|
* The contributions a trial gets from what its instrument declared. This is
|
|
29
45
|
* the whole "declaring a capability provides the chrome" rule — it replaces
|
|
@@ -180,6 +196,8 @@ export function builtinContributions(
|
|
|
180
196
|
config={ctx.config as Record<string, unknown>}
|
|
181
197
|
setConfig={(key, value) => ctx.setConfig(String(key), value)}
|
|
182
198
|
renderers={controls}
|
|
199
|
+
collapsed={groupFolds(ctx.collapsedSections, 'settings')}
|
|
200
|
+
onCollapse={(label, next) => ctx.setSectionCollapsed(`settings/${label}`, next)}
|
|
183
201
|
/>
|
|
184
202
|
),
|
|
185
203
|
},
|
package/src/chrome/index.ts
CHANGED
|
@@ -6,7 +6,7 @@ export type { SidebarRegionProps } from './regions/SidebarRegion';
|
|
|
6
6
|
export { SidebarRegion } from './regions/SidebarRegion';
|
|
7
7
|
export type { StatusRegionProps } from './regions/StatusRegion';
|
|
8
8
|
export { StatusRegion } from './regions/StatusRegion';
|
|
9
|
-
export type { TitleBarRegionProps } from './regions/TitleBarRegion';
|
|
9
|
+
export type { TitleBarPlacement, TitleBarRegionProps } from './regions/TitleBarRegion';
|
|
10
10
|
export { TitleBarRegion } from './regions/TitleBarRegion';
|
|
11
11
|
export type { ToolbarRegionProps } from './regions/ToolbarRegion';
|
|
12
12
|
export { ToolbarRegion } from './regions/ToolbarRegion';
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
|
-
import { useState } from 'react';
|
|
3
2
|
import type { SidebarSection, TrialChromeContext, TrialContribution } from '../types';
|
|
4
3
|
|
|
5
4
|
/** Props for `<SidebarRegion>`. */
|
|
@@ -10,16 +9,17 @@ export interface SidebarRegionProps {
|
|
|
10
9
|
|
|
11
10
|
function Section({
|
|
12
11
|
title,
|
|
13
|
-
|
|
12
|
+
collapsed,
|
|
13
|
+
onCollapsedChange,
|
|
14
14
|
onUndock,
|
|
15
15
|
children,
|
|
16
16
|
}: {
|
|
17
17
|
title: string;
|
|
18
|
-
|
|
18
|
+
collapsed: boolean;
|
|
19
|
+
onCollapsedChange: (collapsed: boolean) => void;
|
|
19
20
|
onUndock?: () => void;
|
|
20
21
|
children: ReactNode;
|
|
21
22
|
}) {
|
|
22
|
-
const [collapsed, setCollapsed] = useState(defaultCollapsed);
|
|
23
23
|
return (
|
|
24
24
|
<section className="lk-sidebar-section">
|
|
25
25
|
<div className="lk-sidebar-section__bar">
|
|
@@ -27,7 +27,7 @@ function Section({
|
|
|
27
27
|
type="button"
|
|
28
28
|
className="lk-sidebar-section__head"
|
|
29
29
|
aria-expanded={!collapsed}
|
|
30
|
-
onClick={() =>
|
|
30
|
+
onClick={() => onCollapsedChange(!collapsed)}
|
|
31
31
|
>
|
|
32
32
|
{title}
|
|
33
33
|
</button>
|
|
@@ -64,7 +64,8 @@ export function SidebarRegion({ contributions, ctx }: SidebarRegionProps) {
|
|
|
64
64
|
<Section
|
|
65
65
|
key={c.id}
|
|
66
66
|
title={item.title}
|
|
67
|
-
|
|
67
|
+
collapsed={ctx.collapsedSections[c.id] ?? item.defaultCollapsed ?? false}
|
|
68
|
+
onCollapsedChange={(next) => ctx.setSectionCollapsed(c.id, next)}
|
|
68
69
|
onUndock={
|
|
69
70
|
item.undockable === false
|
|
70
71
|
? undefined
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import type { TrialChromeContext, TrialContribution } from '../types';
|
|
4
|
+
import { TitleBarRegion } from './TitleBarRegion';
|
|
5
|
+
|
|
6
|
+
const Glyph = () => <svg />;
|
|
7
|
+
const ctx = { trialId: 't1', saveSnapshot: vi.fn() } as unknown as TrialChromeContext;
|
|
8
|
+
|
|
9
|
+
function item(id: string, over: Partial<TrialContribution> = {}): TrialContribution {
|
|
10
|
+
return {
|
|
11
|
+
id,
|
|
12
|
+
region: 'titlebar',
|
|
13
|
+
item: { icon: Glyph, label: id, onActivate: () => {} },
|
|
14
|
+
...over,
|
|
15
|
+
} as TrialContribution;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('TitleBarRegion', () => {
|
|
19
|
+
it('renders nothing when it has nothing for its placement', () => {
|
|
20
|
+
const { container } = render(<TitleBarRegion contributions={[item('lead')]} ctx={ctx} />);
|
|
21
|
+
expect(container).toBeEmptyDOMElement();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('takes the contributions that set `end` into the actions span', () => {
|
|
25
|
+
const { container } = render(
|
|
26
|
+
<TitleBarRegion contributions={[item('lead'), item('close', { end: true })]} ctx={ctx} />,
|
|
27
|
+
);
|
|
28
|
+
expect(container.querySelector('.lk-trial__titlebar-actions')).toBeTruthy();
|
|
29
|
+
expect(screen.getByRole('button', { name: 'close' })).toBeInTheDocument();
|
|
30
|
+
expect(screen.queryByRole('button', { name: 'lead' })).toBeNull();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('takes the rest into the lead span', () => {
|
|
34
|
+
const { container } = render(
|
|
35
|
+
<TitleBarRegion
|
|
36
|
+
placement="lead"
|
|
37
|
+
contributions={[item('lead'), item('close', { end: true })]}
|
|
38
|
+
ctx={ctx}
|
|
39
|
+
/>,
|
|
40
|
+
);
|
|
41
|
+
expect(container.querySelector('.lk-trial__titlebar-lead')).toBeTruthy();
|
|
42
|
+
expect(screen.getByRole('button', { name: 'lead' })).toBeInTheDocument();
|
|
43
|
+
expect(screen.queryByRole('button', { name: 'close' })).toBeNull();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('hands onActivate the chrome context', () => {
|
|
47
|
+
const onActivate = vi.fn();
|
|
48
|
+
render(
|
|
49
|
+
<TitleBarRegion
|
|
50
|
+
contributions={[
|
|
51
|
+
{
|
|
52
|
+
id: 'go',
|
|
53
|
+
region: 'titlebar',
|
|
54
|
+
end: true,
|
|
55
|
+
item: { icon: Glyph, label: 'Go', onActivate },
|
|
56
|
+
},
|
|
57
|
+
]}
|
|
58
|
+
ctx={ctx}
|
|
59
|
+
/>,
|
|
60
|
+
);
|
|
61
|
+
screen.getByRole('button', { name: 'Go' }).click();
|
|
62
|
+
expect(onActivate).toHaveBeenCalledWith(ctx);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
2
|
import type { TrialChromeContext, TrialContribution } from '../types';
|
|
3
3
|
|
|
4
|
+
/** Which end of the title bar a `<TitleBarRegion>` lays out: `'lead'` renders
|
|
5
|
+
* the contributions that leave `end` unset, before the title; `'actions'`
|
|
6
|
+
* renders the ones that set it, after everything else. */
|
|
7
|
+
export type TitleBarPlacement = 'lead' | 'actions';
|
|
8
|
+
|
|
4
9
|
/** Props for `<TitleBarRegion>`. */
|
|
5
10
|
export interface TitleBarRegionProps {
|
|
6
11
|
contributions: readonly TrialContribution[];
|
|
7
12
|
ctx: TrialChromeContext;
|
|
13
|
+
/** Defaults to `'actions'`, where the built-ins live. */
|
|
14
|
+
placement?: TitleBarPlacement;
|
|
8
15
|
}
|
|
9
16
|
|
|
10
17
|
function renderEntry(c: TrialContribution, ctx: TrialChromeContext): ReactNode {
|
|
11
18
|
if (c.render) return <span key={c.id}>{c.render(ctx)}</span>;
|
|
12
19
|
if (c.region !== 'titlebar' || !c.item) return null;
|
|
13
|
-
const { icon: Icon, label, shortcut, disabled, danger } = c.item;
|
|
20
|
+
const { icon: Icon, label, shortcut, disabled, danger, onActivate } = c.item;
|
|
14
21
|
return (
|
|
15
22
|
<button
|
|
16
23
|
key={c.id}
|
|
@@ -19,7 +26,7 @@ function renderEntry(c: TrialContribution, ctx: TrialChromeContext): ReactNode {
|
|
|
19
26
|
disabled={disabled}
|
|
20
27
|
aria-label={label}
|
|
21
28
|
title={shortcut ? `${label} (${shortcut})` : label}
|
|
22
|
-
onClick={
|
|
29
|
+
onClick={() => onActivate(ctx)}
|
|
23
30
|
// The bar behind these buttons is the window drag surface, so a press
|
|
24
31
|
// that lands here must not also start a drag.
|
|
25
32
|
onPointerDown={(e) => e.stopPropagation()}
|
|
@@ -30,12 +37,14 @@ function renderEntry(c: TrialContribution, ctx: TrialChromeContext): ReactNode {
|
|
|
30
37
|
);
|
|
31
38
|
}
|
|
32
39
|
|
|
33
|
-
/** Lays a trial's `titlebar` contributions out
|
|
34
|
-
export function TitleBarRegion({ contributions, ctx }: TitleBarRegionProps) {
|
|
35
|
-
|
|
40
|
+
/** Lays a trial's `titlebar` contributions out at one end of the bar. */
|
|
41
|
+
export function TitleBarRegion({ contributions, ctx, placement = 'actions' }: TitleBarRegionProps) {
|
|
42
|
+
const wanted = placement === 'actions';
|
|
43
|
+
const mine = contributions.filter((c) => (c.end ?? false) === wanted);
|
|
44
|
+
if (mine.length === 0) return null;
|
|
36
45
|
return (
|
|
37
|
-
<span className=
|
|
38
|
-
{
|
|
46
|
+
<span className={wanted ? 'lk-trial__titlebar-actions' : 'lk-trial__titlebar-lead'}>
|
|
47
|
+
{mine.map((c) => renderEntry(c, ctx))}
|
|
39
48
|
</span>
|
|
40
49
|
);
|
|
41
50
|
}
|
|
@@ -42,6 +42,20 @@ describe('ToolbarRegion', () => {
|
|
|
42
42
|
expect(onActivate).toHaveBeenCalledOnce();
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
+
it('hands onActivate the chrome context', () => {
|
|
46
|
+
const onActivate = vi.fn();
|
|
47
|
+
render(
|
|
48
|
+
<ToolbarRegion
|
|
49
|
+
contributions={[
|
|
50
|
+
{ id: 'snap', region: 'toolbar', item: { icon: Glyph, label: 'Snap', onActivate } },
|
|
51
|
+
]}
|
|
52
|
+
ctx={ctx}
|
|
53
|
+
/>,
|
|
54
|
+
);
|
|
55
|
+
screen.getByRole('button', { name: 'Snap' }).click();
|
|
56
|
+
expect(onActivate).toHaveBeenCalledWith(ctx);
|
|
57
|
+
});
|
|
58
|
+
|
|
45
59
|
it('groups contributions sharing a group into one group element', () => {
|
|
46
60
|
render(
|
|
47
61
|
<ToolbarRegion
|
|
@@ -38,7 +38,7 @@ function groupsOf(contributions: readonly TrialContribution[]): Group[] {
|
|
|
38
38
|
function renderEntry(c: TrialContribution, ctx: TrialChromeContext): ReactNode {
|
|
39
39
|
if (c.render) return <span key={c.id}>{c.render(ctx)}</span>;
|
|
40
40
|
if (c.region !== 'toolbar' || !c.item) return null;
|
|
41
|
-
const { icon: Icon, label, shortcut, disabled, danger, showLabel, pressed } = c.item;
|
|
41
|
+
const { icon: Icon, label, shortcut, disabled, danger, showLabel, pressed, onActivate } = c.item;
|
|
42
42
|
return (
|
|
43
43
|
<Toolbar.Button
|
|
44
44
|
key={c.id}
|
|
@@ -48,7 +48,7 @@ function renderEntry(c: TrialContribution, ctx: TrialChromeContext): ReactNode {
|
|
|
48
48
|
pressed={pressed}
|
|
49
49
|
aria-label={label}
|
|
50
50
|
title={shortcut ? `${label} (${shortcut})` : label}
|
|
51
|
-
onClick={
|
|
51
|
+
onClick={() => onActivate(ctx)}
|
|
52
52
|
>
|
|
53
53
|
<Icon size={16} />
|
|
54
54
|
{showLabel ? <span>{label}</span> : null}
|
|
@@ -25,7 +25,7 @@ export function ViewportRegion({ contributions, ctx }: ViewportRegionProps) {
|
|
|
25
25
|
{contributions.map((c) => {
|
|
26
26
|
if (c.render) return <span key={c.id}>{c.render(ctx)}</span>;
|
|
27
27
|
if (c.region !== 'viewport' || !c.item) return null;
|
|
28
|
-
const { icon: Icon, label, disabled } = c.item;
|
|
28
|
+
const { icon: Icon, label, disabled, onActivate } = c.item;
|
|
29
29
|
return (
|
|
30
30
|
<button
|
|
31
31
|
key={c.id}
|
|
@@ -34,7 +34,7 @@ export function ViewportRegion({ contributions, ctx }: ViewportRegionProps) {
|
|
|
34
34
|
aria-label={label}
|
|
35
35
|
title={label}
|
|
36
36
|
disabled={disabled}
|
|
37
|
-
onClick={
|
|
37
|
+
onClick={() => onActivate(ctx)}
|
|
38
38
|
>
|
|
39
39
|
<Icon size={16} />
|
|
40
40
|
</button>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { render, screen } from '@testing-library/react';
|
|
1
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
|
2
2
|
import { describe, expect, it, vi } from 'vitest';
|
|
3
3
|
import type { TrialChromeContext, TrialContribution } from '../types';
|
|
4
4
|
import { SidebarRegion } from './SidebarRegion';
|
|
@@ -9,6 +9,8 @@ const Glyph = () => <svg />;
|
|
|
9
9
|
const ctx = {
|
|
10
10
|
trialId: 't1',
|
|
11
11
|
undockedPanels: [],
|
|
12
|
+
collapsedSections: {},
|
|
13
|
+
setSectionCollapsed: () => {},
|
|
12
14
|
undockPanel: () => {},
|
|
13
15
|
dockPanel: () => {},
|
|
14
16
|
} as unknown as TrialChromeContext;
|
|
@@ -45,6 +47,34 @@ describe('SidebarRegion', () => {
|
|
|
45
47
|
);
|
|
46
48
|
expect(screen.queryByText('hidden')).not.toBeInTheDocument();
|
|
47
49
|
});
|
|
50
|
+
|
|
51
|
+
it('takes the trial’s remembered fold over the section’s default', () => {
|
|
52
|
+
render(
|
|
53
|
+
<SidebarRegion
|
|
54
|
+
contributions={[
|
|
55
|
+
{
|
|
56
|
+
id: 's',
|
|
57
|
+
region: 'sidebar',
|
|
58
|
+
item: { title: 'S', defaultCollapsed: true, body: <p>shown</p> },
|
|
59
|
+
},
|
|
60
|
+
]}
|
|
61
|
+
ctx={{ ...ctx, collapsedSections: { s: false } }}
|
|
62
|
+
/>,
|
|
63
|
+
);
|
|
64
|
+
expect(screen.getByText('shown')).toBeInTheDocument();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('reports a toggle rather than keeping the fold itself', () => {
|
|
68
|
+
const setSectionCollapsed = vi.fn();
|
|
69
|
+
render(
|
|
70
|
+
<SidebarRegion
|
|
71
|
+
contributions={[{ id: 's', region: 'sidebar', item: { title: 'S', body: <p>body</p> } }]}
|
|
72
|
+
ctx={{ ...ctx, setSectionCollapsed }}
|
|
73
|
+
/>,
|
|
74
|
+
);
|
|
75
|
+
fireEvent.click(screen.getByRole('button', { name: 'S' }));
|
|
76
|
+
expect(setSectionCollapsed).toHaveBeenCalledWith('s', true);
|
|
77
|
+
});
|
|
48
78
|
});
|
|
49
79
|
|
|
50
80
|
describe('StatusRegion', () => {
|
|
@@ -83,4 +113,18 @@ describe('ViewportRegion', () => {
|
|
|
83
113
|
screen.getByRole('button', { name: 'Fit' }).click();
|
|
84
114
|
expect(onActivate).toHaveBeenCalledOnce();
|
|
85
115
|
});
|
|
116
|
+
|
|
117
|
+
it('hands onActivate the chrome context', () => {
|
|
118
|
+
const onActivate = vi.fn();
|
|
119
|
+
render(
|
|
120
|
+
<ViewportRegion
|
|
121
|
+
contributions={[
|
|
122
|
+
{ id: 'fit', region: 'viewport', item: { icon: Glyph, label: 'Fit', onActivate } },
|
|
123
|
+
]}
|
|
124
|
+
ctx={ctx}
|
|
125
|
+
/>,
|
|
126
|
+
);
|
|
127
|
+
screen.getByRole('button', { name: 'Fit' }).click();
|
|
128
|
+
expect(onActivate).toHaveBeenCalledWith(ctx);
|
|
129
|
+
});
|
|
86
130
|
});
|
package/src/chrome/types.ts
CHANGED
|
@@ -24,7 +24,10 @@ export interface ToolbarItem {
|
|
|
24
24
|
pressed?: boolean;
|
|
25
25
|
/** Render the label beside the glyph rather than only in the tooltip. */
|
|
26
26
|
showLabel?: boolean;
|
|
27
|
-
|
|
27
|
+
/** Handed the trial's chrome context, so a contribution declared from the
|
|
28
|
+
* lab can reach `ctx.saveSnapshot()` and the rest without the `render`
|
|
29
|
+
* escape. A zero-argument handler stays valid. */
|
|
30
|
+
onActivate: (ctx: TrialChromeContext) => void;
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
/** A selectable tool in the palette region. */
|
|
@@ -38,7 +41,7 @@ export interface ToolItem {
|
|
|
38
41
|
/** A titled block in the sidebar. */
|
|
39
42
|
export interface SidebarSection {
|
|
40
43
|
title: string;
|
|
41
|
-
/** Starts collapsed
|
|
44
|
+
/** Starts collapsed, until the trial remembers a fold of its own. */
|
|
42
45
|
defaultCollapsed?: boolean;
|
|
43
46
|
/** Offer the tear-out control. On by default; a section that only makes
|
|
44
47
|
* sense beside its trial sets this false. */
|
|
@@ -53,7 +56,8 @@ export interface ViewportControl {
|
|
|
53
56
|
icon: IconComponent;
|
|
54
57
|
label: string;
|
|
55
58
|
disabled?: boolean;
|
|
56
|
-
onActivate
|
|
59
|
+
/** Handed the trial's chrome context, as `ToolbarItem.onActivate` is. */
|
|
60
|
+
onActivate: (ctx: TrialChromeContext) => void;
|
|
57
61
|
}
|
|
58
62
|
|
|
59
63
|
/** A readout in the status bar. */
|
|
@@ -100,6 +104,12 @@ export type TrialContribution =
|
|
|
100
104
|
export interface TrialChromeContext {
|
|
101
105
|
trialId: string;
|
|
102
106
|
instrumentName: string;
|
|
107
|
+
/** What the title bar reads, which is the instrument's name until something
|
|
108
|
+
* calls `setTitle`. */
|
|
109
|
+
title: string;
|
|
110
|
+
/** Retitle this trial; `null` restores the instrument name. Persisted with
|
|
111
|
+
* the trial, so a title survives a reload. */
|
|
112
|
+
setTitle: (title: string | null) => void;
|
|
103
113
|
isLastTrial: boolean;
|
|
104
114
|
|
|
105
115
|
/** Null when the trial holds a view that is not the 2D one. */
|
|
@@ -125,6 +135,14 @@ export interface TrialChromeContext {
|
|
|
125
135
|
config: unknown;
|
|
126
136
|
setConfig: (key: string, value: unknown) => void;
|
|
127
137
|
|
|
138
|
+
/** Which of this trial's collapsible sections are folded. A sidebar
|
|
139
|
+
* section's key is its contribution id; a section *inside* a contribution —
|
|
140
|
+
* a control panel's property group — is keyed `<contribution id>/<label>`.
|
|
141
|
+
* A section absent here is at its own default. Persisted with the trial. */
|
|
142
|
+
collapsedSections: Readonly<Record<string, boolean>>;
|
|
143
|
+
/** Fold or unfold one section, keyed as `collapsedSections` is. */
|
|
144
|
+
setSectionCollapsed: (key: string, collapsed: boolean) => void;
|
|
145
|
+
|
|
128
146
|
/** Section ids this trial currently has torn out of its sidebar. */
|
|
129
147
|
undockedPanels: readonly string[];
|
|
130
148
|
/** Tear a sidebar section out into the workspace. */
|
|
@@ -443,3 +443,41 @@ describe('<ControlPanel> suffix', () => {
|
|
|
443
443
|
expect(container.textContent).toContain('px');
|
|
444
444
|
});
|
|
445
445
|
});
|
|
446
|
+
|
|
447
|
+
describe('<ControlPanel> collapsible sections', () => {
|
|
448
|
+
const schema = resolveConfigSchema(
|
|
449
|
+
f.schema({ showGrid: f.boolean(true), seed: f.number(0).section('Advanced') }),
|
|
450
|
+
[],
|
|
451
|
+
);
|
|
452
|
+
const config = { showGrid: true, seed: 0 };
|
|
453
|
+
|
|
454
|
+
it('draws no twisty by default', () => {
|
|
455
|
+
render(<ControlPanel schema={schema} config={config} setConfig={vi.fn()} />);
|
|
456
|
+
expect(screen.queryByRole('button', { name: 'Advanced' })).toBeNull();
|
|
457
|
+
expect(screen.getByLabelText('Seed')).toBeVisible();
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it('folds a section away when the panel starts closed', () => {
|
|
461
|
+
render(<ControlPanel schema={schema} config={config} setConfig={vi.fn()} collapse="closed" />);
|
|
462
|
+
expect(screen.getByLabelText('Seed')).not.toBeVisible();
|
|
463
|
+
fireEvent.click(screen.getByRole('button', { name: 'Advanced' }));
|
|
464
|
+
expect(screen.getByLabelText('Seed')).toBeVisible();
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it('hands a controlled panel its toggles instead of acting on them', () => {
|
|
468
|
+
const onCollapse = vi.fn();
|
|
469
|
+
render(
|
|
470
|
+
<ControlPanel
|
|
471
|
+
schema={schema}
|
|
472
|
+
config={config}
|
|
473
|
+
setConfig={vi.fn()}
|
|
474
|
+
collapsed={{ Advanced: true }}
|
|
475
|
+
onCollapse={onCollapse}
|
|
476
|
+
/>,
|
|
477
|
+
);
|
|
478
|
+
expect(screen.getByLabelText('Seed')).not.toBeVisible();
|
|
479
|
+
fireEvent.click(screen.getByRole('button', { name: 'Advanced' }));
|
|
480
|
+
expect(onCollapse).toHaveBeenCalledWith('Advanced', false);
|
|
481
|
+
expect(screen.getByLabelText('Seed')).not.toBeVisible();
|
|
482
|
+
});
|
|
483
|
+
});
|
|
@@ -4,6 +4,8 @@ import {
|
|
|
4
4
|
ColorRow,
|
|
5
5
|
NumberRow,
|
|
6
6
|
type PrefLeaf,
|
|
7
|
+
type PropertyAlign,
|
|
8
|
+
type PropertyDensity,
|
|
7
9
|
PropertyGroup,
|
|
8
10
|
PropertyList,
|
|
9
11
|
PropertyRow,
|
|
@@ -44,9 +46,28 @@ export interface ControlPanelProps<TC extends Record<string, unknown>> {
|
|
|
44
46
|
renderers?: Record<string, ControlRenderer>;
|
|
45
47
|
/** How rows pack into the two-column grid. Defaults to `'pairs'`. */
|
|
46
48
|
pack?: ControlPack;
|
|
47
|
-
/**
|
|
48
|
-
*
|
|
49
|
+
/**
|
|
50
|
+
* Where a row's label sits relative to its control. Unset takes each row's
|
|
51
|
+
* own orientation — `block` (label above, which is what leaves a paired row
|
|
52
|
+
* room for its value) for most, `inline` for colors and checkboxes.
|
|
53
|
+
*/
|
|
49
54
|
layout?: PropertyRowLayout;
|
|
55
|
+
/** Room the rows get. Passed straight to the property list. */
|
|
56
|
+
density?: PropertyDensity;
|
|
57
|
+
/** Cross-axis alignment of an inline row's label and control. */
|
|
58
|
+
align?: PropertyAlign;
|
|
59
|
+
/**
|
|
60
|
+
* Fold each section away behind a twisty, starting `'open'` or `'closed'`.
|
|
61
|
+
* Unset draws the heading alone, as the panel always has.
|
|
62
|
+
*/
|
|
63
|
+
collapse?: 'open' | 'closed';
|
|
64
|
+
/**
|
|
65
|
+
* Which sections are folded, keyed by section label. Given, the panel keeps
|
|
66
|
+
* no state of its own: every toggle arrives at `onCollapse` instead, which
|
|
67
|
+
* is where a lab that remembers a trial's sections writes them.
|
|
68
|
+
*/
|
|
69
|
+
collapsed?: Readonly<Record<string, boolean>>;
|
|
70
|
+
onCollapse?: (label: string, collapsed: boolean) => void;
|
|
50
71
|
/** Draw leaves marked `hidden`. */
|
|
51
72
|
showHidden?: boolean;
|
|
52
73
|
className?: string;
|
|
@@ -62,7 +83,12 @@ export function ControlPanel<TC extends Record<string, unknown>>({
|
|
|
62
83
|
setConfig,
|
|
63
84
|
renderers,
|
|
64
85
|
pack = 'pairs',
|
|
65
|
-
layout
|
|
86
|
+
layout,
|
|
87
|
+
density,
|
|
88
|
+
align,
|
|
89
|
+
collapse,
|
|
90
|
+
collapsed,
|
|
91
|
+
onCollapse,
|
|
66
92
|
showHidden = false,
|
|
67
93
|
className,
|
|
68
94
|
}: ControlPanelProps<TC>) {
|
|
@@ -87,14 +113,26 @@ export function ControlPanel<TC extends Record<string, unknown>>({
|
|
|
87
113
|
);
|
|
88
114
|
|
|
89
115
|
const gridPack = pack === 'one-up' ? 'auto-color' : 'pairs';
|
|
116
|
+
const folds = collapse !== undefined || collapsed !== undefined || onCollapse !== undefined;
|
|
117
|
+
const startsFolded = collapse === 'closed';
|
|
90
118
|
return (
|
|
91
119
|
<PropertyList
|
|
92
120
|
pack={gridPack}
|
|
121
|
+
density={density}
|
|
122
|
+
align={align}
|
|
93
123
|
className={className ? `lk-control-panel ${className}` : 'lk-control-panel'}
|
|
94
124
|
>
|
|
95
125
|
{loose.map(row)}
|
|
96
126
|
{resolved.sections.map((section) => (
|
|
97
|
-
<PropertyGroup
|
|
127
|
+
<PropertyGroup
|
|
128
|
+
key={section.label}
|
|
129
|
+
title={section.label}
|
|
130
|
+
pack={gridPack}
|
|
131
|
+
collapsible={folds}
|
|
132
|
+
defaultCollapsed={startsFolded}
|
|
133
|
+
collapsed={collapsed ? (collapsed[section.label] ?? startsFolded) : undefined}
|
|
134
|
+
onCollapsedChange={onCollapse ? (next) => onCollapse(section.label, next) : undefined}
|
|
135
|
+
>
|
|
98
136
|
{section.paths.map(row)}
|
|
99
137
|
</PropertyGroup>
|
|
100
138
|
))}
|
|
@@ -109,7 +147,7 @@ interface ControlRowProps<TC extends Record<string, unknown>> {
|
|
|
109
147
|
setConfig: (key: keyof TC, value: unknown) => void;
|
|
110
148
|
renderers?: Record<string, ControlRenderer>;
|
|
111
149
|
pack: ControlPack;
|
|
112
|
-
layout
|
|
150
|
+
layout?: PropertyRowLayout;
|
|
113
151
|
showHidden: boolean;
|
|
114
152
|
}
|
|
115
153
|
|
|
@@ -201,6 +239,7 @@ function ControlRow<TC extends Record<string, unknown>>({
|
|
|
201
239
|
label={label}
|
|
202
240
|
value={read<boolean>()}
|
|
203
241
|
onChange={write}
|
|
242
|
+
layout={layout}
|
|
204
243
|
description={description}
|
|
205
244
|
/>
|
|
206
245
|
);
|
|
@@ -234,7 +273,13 @@ function ControlRow<TC extends Record<string, unknown>>({
|
|
|
234
273
|
);
|
|
235
274
|
case 'color':
|
|
236
275
|
return (
|
|
237
|
-
<ColorRow
|
|
276
|
+
<ColorRow
|
|
277
|
+
label={label}
|
|
278
|
+
value={read<string>()}
|
|
279
|
+
onChange={write}
|
|
280
|
+
layout={layout}
|
|
281
|
+
description={description}
|
|
282
|
+
/>
|
|
238
283
|
);
|
|
239
284
|
case 'paint':
|
|
240
285
|
case 'object':
|