@weasel-js/labkit 1.5.1 → 1.5.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/dist/_dts/{CanvasStackContext-CJNMS6mz.d.ts → CanvasStackContext-CMMo180l.d.ts} +1 -1
- package/dist/_dts/{Select.d-Gwr8fLpK.d.ts → Select.d-ZTiwzYyT.d.ts} +27 -1
- package/dist/_dts/{frac-B4okC2lX.d.ts → frac-B4hFJMiN.d.ts} +16 -6
- package/dist/_dts/{index-BNdz-tNd.d.ts → index-8LiKe3A-.d.ts} +30 -43
- package/dist/_dts/{index-CSJoj4LC.d.ts → index-DlKolEIZ.d.ts} +5 -4
- package/dist/_dts/{types-DzoNY1pO.d.ts → types-Db-3NNk1.d.ts} +4 -4
- package/dist/_dts/{usePanZoom-Pzf9f2jh.d.ts → usePanZoom-SSTZe410.d.ts} +4 -47
- package/dist/_dts/{useTrialState-uLlmaccn.d.ts → useTrialState-pnAE-UBD.d.ts} +1 -1
- package/dist/canvas/index.d.ts +4 -4
- package/dist/canvas/index.js +1 -1
- package/dist/chrome/index.d.ts +3 -3
- package/dist/chrome/index.js +6 -6
- package/dist/{chunk-PJ7WEQBJ.js → chunk-6BUQ77ZV.js} +4 -4
- package/dist/{chunk-PJ7WEQBJ.js.map → chunk-6BUQ77ZV.js.map} +1 -1
- package/dist/{chunk-UN4U4P63.js → chunk-753Y6HE6.js} +9 -9
- package/dist/chunk-753Y6HE6.js.map +1 -0
- package/dist/{chunk-ZDHY47EV.js → chunk-A2ACAGW6.js} +6 -7
- package/dist/chunk-A2ACAGW6.js.map +1 -0
- package/dist/{chunk-IHUVQ3TX.js → chunk-CKUKF72R.js} +10 -10
- package/dist/chunk-CKUKF72R.js.map +1 -0
- package/dist/{chunk-MDVTK5U3.js → chunk-D35PFTS7.js} +3 -3
- package/dist/{chunk-MDVTK5U3.js.map → chunk-D35PFTS7.js.map} +1 -1
- package/dist/{chunk-PHK4W562.js → chunk-EMXAIRUJ.js} +3 -3
- package/dist/{chunk-PHK4W562.js.map → chunk-EMXAIRUJ.js.map} +1 -1
- package/dist/{chunk-EH7UEQV7.js → chunk-FKLT3ITC.js} +69 -69
- package/dist/chunk-FKLT3ITC.js.map +1 -0
- package/dist/{chunk-EHGVROVV.js → chunk-KJCUERQF.js} +3 -3
- package/dist/{chunk-EHGVROVV.js.map → chunk-KJCUERQF.js.map} +1 -1
- package/dist/chunk-LASCBWW7.js +13456 -0
- package/dist/chunk-LASCBWW7.js.map +1 -0
- package/dist/{chunk-IXTQI7JX.js → chunk-LRI3MBGR.js} +4 -4
- package/dist/{chunk-IXTQI7JX.js.map → chunk-LRI3MBGR.js.map} +1 -1
- package/dist/{chunk-7PFH3CNM.js → chunk-REOBGYMC.js} +4 -4
- package/dist/{chunk-7PFH3CNM.js.map → chunk-REOBGYMC.js.map} +1 -1
- package/dist/{chunk-CEIUBPPW.js → chunk-TT5C5S6R.js} +3 -86
- package/dist/chunk-TT5C5S6R.js.map +1 -0
- package/dist/config/index.d.ts +7 -8
- package/dist/config/index.js +5 -5
- package/dist/controls/index.d.ts +2 -2
- package/dist/controls/index.js +4 -4
- package/dist/dragdrop/index.d.ts +2 -2
- package/dist/index.d.ts +72 -28
- package/dist/index.js +142 -123
- package/dist/index.js.map +1 -1
- package/dist/layers/index.d.ts +3 -3
- package/dist/layers/index.js +2 -2
- package/dist/loupe/index.d.ts +4 -4
- package/dist/passthrough/weasel-ui.d.ts +33 -30
- package/dist/passthrough/weasel-ui.js +1 -1
- package/dist/primitives/index.js +3 -3
- package/dist/state/index.d.ts +4 -4
- package/dist/state/index.js +5 -5
- package/dist/styles.css +114 -18
- package/dist/ui/layers/index.js +1 -1
- package/dist/undo/index.d.ts +2 -2
- package/package.json +9 -9
- package/src/canvas/index.ts +0 -9
- package/src/chrome/labTypes.ts +1 -1
- package/src/chrome/regions/PaletteRegion.test.tsx +18 -0
- package/src/chrome/regions/PaletteRegion.tsx +5 -4
- package/src/chrome/regions/TitleBarRegion.tsx +1 -1
- package/src/chrome/types.ts +8 -2
- package/src/config/builder.ts +3 -4
- package/src/config/types.ts +4 -4
- package/src/controls/AutoControls.stories.tsx +6 -6
- package/src/controls/ControlPanel.less +23 -0
- package/src/controls/ControlPanel.stories.tsx +41 -0
- package/src/controls/ControlPanel.test.tsx +53 -110
- package/src/controls/ControlPanel.tsx +61 -77
- package/src/index.test.ts +0 -2
- package/src/index.ts +4 -10
- package/src/lab/Lab.tsx +21 -5
- package/src/layers/LayerList.less +1 -1
- package/src/layers/LayerList.stories.tsx +1 -1
- package/src/primitives/FpsMeter.less +6 -1
- package/src/primitives/FpsMeter.test.tsx +12 -4
- package/src/primitives/FpsMeter.tsx +6 -1
- package/src/primitives/JobProgress.stories.tsx +1 -1
- package/src/primitives/StatusBar.less +6 -0
- package/src/state/types.ts +6 -1
- package/src/state/view.test.ts +4 -4
- package/src/trial/Trial.canvas.test.tsx +8 -4
- package/src/trial/Trial.config.test.tsx +1 -1
- package/dist/chunk-CEIUBPPW.js.map +0 -1
- package/dist/chunk-EH7UEQV7.js.map +0 -1
- package/dist/chunk-IHUVQ3TX.js.map +0 -1
- package/dist/chunk-T3EC2DXD.js +0 -12628
- package/dist/chunk-T3EC2DXD.js.map +0 -1
- package/dist/chunk-UN4U4P63.js.map +0 -1
- package/dist/chunk-ZDHY47EV.js.map +0 -1
- package/src/canvas/useOrbit.test.ts +0 -167
- package/src/canvas/useOrbit.ts +0 -156
package/dist/ui/layers/index.js
CHANGED
package/dist/undo/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { au as UndoCapability } from '../_dts/frac-B4hFJMiN.js';
|
|
2
2
|
import 'react';
|
|
3
|
-
import '../_dts/types-
|
|
3
|
+
import '../_dts/types-Db-3NNk1.js';
|
|
4
4
|
import '@weasel-js/core';
|
|
5
5
|
import '../_dts/PrefsForm.d-DPy9I8It.js';
|
|
6
6
|
import '../_dts/types-DJ79Tg5J.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weasel-js/labkit",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.2",
|
|
4
4
|
"description": "React widgets for building self-contained interactive lab pages",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "orochi235",
|
|
@@ -115,17 +115,17 @@
|
|
|
115
115
|
"dev:3d": "LABKIT_EXAMPLE=3d-lab vite"
|
|
116
116
|
},
|
|
117
117
|
"peerDependencies": {
|
|
118
|
-
"@weasel-js/core": "1.5.
|
|
119
|
-
"@weasel-js/theme": "1.5.
|
|
118
|
+
"@weasel-js/core": "1.5.2",
|
|
119
|
+
"@weasel-js/theme": "1.5.2",
|
|
120
120
|
"react": "^19.0.0",
|
|
121
121
|
"react-dom": "^19.0.0"
|
|
122
122
|
},
|
|
123
123
|
"dependencies": {
|
|
124
|
-
"@weasel-js/geom": "1.5.
|
|
125
|
-
"@weasel-js/kernel3d": "1.5.
|
|
126
|
-
"@weasel-js/loupe": "1.5.
|
|
127
|
-
"@weasel-js/svg": "1.5.
|
|
128
|
-
"@weasel-js/ui": "1.5.
|
|
124
|
+
"@weasel-js/geom": "1.5.2",
|
|
125
|
+
"@weasel-js/kernel3d": "1.5.2",
|
|
126
|
+
"@weasel-js/loupe": "1.5.2",
|
|
127
|
+
"@weasel-js/svg": "1.5.2",
|
|
128
|
+
"@weasel-js/ui": "1.5.2",
|
|
129
129
|
"earcut": "2.2.4",
|
|
130
130
|
"polygon-clipping": "^0.15.7",
|
|
131
131
|
"react-aria-components": "^1.5.0",
|
|
@@ -136,7 +136,7 @@
|
|
|
136
136
|
"@biomejs/biome": "^2.4.12",
|
|
137
137
|
"@rollup/plugin-alias": "^6.0.0",
|
|
138
138
|
"@testing-library/jest-dom": "^6.6.0",
|
|
139
|
-
"@weasel-js/modes": "1.5.
|
|
139
|
+
"@weasel-js/modes": "1.5.2",
|
|
140
140
|
"less": "^4.2.0",
|
|
141
141
|
"rollup-plugin-dts": "^6.4.1",
|
|
142
142
|
"tsx": "^4.19.0",
|
package/src/canvas/index.ts
CHANGED
|
@@ -10,15 +10,6 @@ export { screenToWorld, worldToScreen } from './canvasCoords';
|
|
|
10
10
|
export type { StageProps } from './Stage';
|
|
11
11
|
export { fitStage, Stage } from './Stage';
|
|
12
12
|
export type { CanvasLayerDescriptor } from './useLayerScheduler';
|
|
13
|
-
export type { OrbitHandlers, OrbitView, UseOrbitOptions, Vec3 } from './useOrbit';
|
|
14
|
-
export {
|
|
15
|
-
clampPitch,
|
|
16
|
-
orbitAfterDrag,
|
|
17
|
-
orbitAfterWheel,
|
|
18
|
-
PITCH_LIMIT,
|
|
19
|
-
useOrbit,
|
|
20
|
-
wrapYaw,
|
|
21
|
-
} from './useOrbit';
|
|
22
13
|
export type { PanZoomHandlers, UsePanZoomOptions } from './usePanZoom';
|
|
23
14
|
export { usePanZoom } from './usePanZoom';
|
|
24
15
|
export type { ViewportSize, WorldFrame, WorldSpec } from './worldSpec';
|
package/src/chrome/labTypes.ts
CHANGED
|
@@ -40,7 +40,7 @@ export type LabContribution<TCtx = LabChromeContext> =
|
|
|
40
40
|
item: ToolbarItem<TCtx>;
|
|
41
41
|
render?: never;
|
|
42
42
|
})
|
|
43
|
-
| (ContributionBase & { region: 'palette'; item: ToolItem
|
|
43
|
+
| (ContributionBase & { region: 'palette'; item: ToolItem<TCtx>; render?: never })
|
|
44
44
|
| (ContributionBase & { region: 'sidebar' | 'aside'; item: SidebarSection; render?: never })
|
|
45
45
|
| (ContributionBase & { region: 'footer'; item: StatusReadout; render?: never })
|
|
46
46
|
| (ContributionBase & {
|
|
@@ -36,6 +36,24 @@ describe('PaletteRegion', () => {
|
|
|
36
36
|
expect(setActiveTool).toHaveBeenCalledWith('brush');
|
|
37
37
|
});
|
|
38
38
|
|
|
39
|
+
it('runs a command tool instead of selecting it, and never marks it current', () => {
|
|
40
|
+
const onActivate = vi.fn();
|
|
41
|
+
const setActiveTool = vi.fn();
|
|
42
|
+
const ctx = ctxWith('info', setActiveTool);
|
|
43
|
+
const info: RegionContribution<ToolSlotContext> = {
|
|
44
|
+
id: 'info',
|
|
45
|
+
region: 'palette',
|
|
46
|
+
item: { icon: Glyph, label: 'Info', onActivate },
|
|
47
|
+
};
|
|
48
|
+
render(<PaletteRegion contributions={[info]} ctx={ctx} />);
|
|
49
|
+
const button = screen.getByRole('button', { name: 'Info' });
|
|
50
|
+
button.click();
|
|
51
|
+
expect(onActivate).toHaveBeenCalledWith(ctx);
|
|
52
|
+
expect(setActiveTool).not.toHaveBeenCalled();
|
|
53
|
+
// The slot names this id, and a command still must not read as held down.
|
|
54
|
+
expect(button).not.toHaveAttribute('aria-current');
|
|
55
|
+
});
|
|
56
|
+
|
|
39
57
|
it('puts exactly one tool in the tab order', () => {
|
|
40
58
|
render(<PaletteRegion contributions={[tool('brush'), tool('eraser')]} ctx={ctxWith(null)} />);
|
|
41
59
|
expect(screen.getByRole('button', { name: 'brush' })).toHaveAttribute('tabindex', '0');
|
|
@@ -12,7 +12,8 @@ export interface PaletteRegionProps<TCtx extends ToolSlotContext = TrialChromeCo
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
/** A tool strip: a trial's, or the lab's. Selection lives in whichever tool
|
|
15
|
-
* slot the context carries; this region only reflects it.
|
|
15
|
+
* slot the context carries; this region only reflects it. An item carrying
|
|
16
|
+
* `onActivate` is a command instead — it presses, and never latches. */
|
|
16
17
|
export function PaletteRegion<TCtx extends ToolSlotContext = TrialChromeContext>({
|
|
17
18
|
contributions,
|
|
18
19
|
ctx,
|
|
@@ -33,16 +34,16 @@ export function PaletteRegion<TCtx extends ToolSlotContext = TrialChromeContext>
|
|
|
33
34
|
{contributions.map((c) => {
|
|
34
35
|
if (c.render) return <span key={c.id}>{c.render(ctx)}</span>;
|
|
35
36
|
if (c.region !== region || !c.item) return null;
|
|
36
|
-
const { icon: Icon, label, shortcut, disabled } = c.item as ToolItem
|
|
37
|
+
const { icon: Icon, label, shortcut, disabled, onActivate } = c.item as ToolItem<TCtx>;
|
|
37
38
|
return (
|
|
38
39
|
<ToolButton
|
|
39
40
|
key={c.id}
|
|
40
41
|
icon={<Icon size={16} />}
|
|
41
42
|
label={label}
|
|
42
43
|
shortcut={shortcut}
|
|
43
|
-
active={ctx.activeToolId === c.id}
|
|
44
|
+
active={onActivate ? false : ctx.activeToolId === c.id}
|
|
44
45
|
disabled={disabled}
|
|
45
|
-
onClick={() => ctx.setActiveTool(c.id)}
|
|
46
|
+
onClick={onActivate ? () => onActivate(ctx) : () => ctx.setActiveTool(c.id)}
|
|
46
47
|
/>
|
|
47
48
|
);
|
|
48
49
|
})}
|
package/src/chrome/types.ts
CHANGED
|
@@ -31,12 +31,18 @@ export interface ToolbarItem<TCtx = TrialChromeContext> {
|
|
|
31
31
|
onActivate: (ctx: TCtx) => void;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
/** A
|
|
35
|
-
|
|
34
|
+
/** A tool in the palette region: a mode by default, a command when it carries
|
|
35
|
+
* an `onActivate`. `TCtx` is the chrome context that handler is handed — a
|
|
36
|
+
* trial's, or the lab's for a lab-level rail. */
|
|
37
|
+
export interface ToolItem<TCtx = TrialChromeContext> {
|
|
36
38
|
icon: IconComponent;
|
|
37
39
|
label: string;
|
|
40
|
+
/** Shown in the tooltip. Not bound here — the trial owns its keymap. */
|
|
38
41
|
shortcut?: string;
|
|
39
42
|
disabled?: boolean;
|
|
43
|
+
/** Runs on press instead of selecting. An item with one is a command: it
|
|
44
|
+
* never writes the tool slot, and never reads as the current tool. */
|
|
45
|
+
onActivate?: (ctx: TCtx) => void;
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
/** A titled block in the sidebar. */
|
package/src/config/builder.ts
CHANGED
|
@@ -110,7 +110,7 @@ export abstract class BaseNode<T> implements ConfigNode<T> {
|
|
|
110
110
|
return this.opt({ unpinned: true });
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
/** Never auto. The row
|
|
113
|
+
/** Never auto. The row's label does not toggle. */
|
|
114
114
|
manual(): this {
|
|
115
115
|
return this.opt({ manual: true });
|
|
116
116
|
}
|
|
@@ -254,9 +254,8 @@ export class GroupNode<S extends ConfigShape> implements ConfigBranch<S> {
|
|
|
254
254
|
return this.with({ ...this.annotations, name }, this.options);
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
-
/** Longer help text. Carried on the resolved `PrefGroup
|
|
258
|
-
* `PrefsForm`
|
|
259
|
-
* it yet and shows the heading alone. */
|
|
257
|
+
/** Longer help text. Carried on the resolved `PrefGroup`; both weasel-ui's
|
|
258
|
+
* `PrefsForm` and `ControlPanel` draw it under the heading. */
|
|
260
259
|
describe(description: string): GroupNode<S> {
|
|
261
260
|
return this.with({ ...this.annotations, description }, this.options);
|
|
262
261
|
}
|
package/src/config/types.ts
CHANGED
|
@@ -123,15 +123,15 @@ export interface NodeOptions extends BranchOptions {
|
|
|
123
123
|
render?: ControlRenderer;
|
|
124
124
|
/** Errors for this leaf under `config`, the instrument's whole config. */
|
|
125
125
|
validate?: (leaf: PrefLeaf, config: Record<string, unknown>) => string[];
|
|
126
|
-
/** Computes this leaf's value while it is auto. Attaching one is what
|
|
127
|
-
* the
|
|
126
|
+
/** Computes this leaf's value while it is auto. Attaching one is what gives
|
|
127
|
+
* the row a real value to pin back to and keeps the instrument off
|
|
128
128
|
* `?? compute()`. */
|
|
129
129
|
autoResolve?: (config: Record<string, unknown>) => unknown;
|
|
130
130
|
/** The leaf starts auto rather than pinned at its default. Set by
|
|
131
131
|
* `.initial(auto)`. */
|
|
132
132
|
unpinned?: boolean;
|
|
133
|
-
/** The leaf can never be auto: the row
|
|
134
|
-
*
|
|
133
|
+
/** The leaf can never be auto: the row's label does not toggle. For a value
|
|
134
|
+
* the instrument cannot receive as `undefined`. */
|
|
135
135
|
manual?: boolean;
|
|
136
136
|
}
|
|
137
137
|
|
|
@@ -37,7 +37,7 @@ const schema = f.schema({
|
|
|
37
37
|
|
|
38
38
|
const resolved = resolveConfigSchema(schema, []);
|
|
39
39
|
|
|
40
|
-
/** Every path the panel will draw
|
|
40
|
+
/** Every path the panel will draw as auto, except the `.manual()` one. */
|
|
41
41
|
const EVERY_AUTO = ['gap', 'cols', 'wave', 'mode', 'showGrid', 'title', 'tint'];
|
|
42
42
|
|
|
43
43
|
function Panel({ start }: { start: readonly string[] }) {
|
|
@@ -73,16 +73,16 @@ function Pair() {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
/**
|
|
76
|
-
* Both states of every row kind, side by side. What to look at: an auto row
|
|
77
|
-
* control
|
|
78
|
-
*
|
|
79
|
-
*
|
|
76
|
+
* Both states of every row kind, side by side. What to look at: an auto row
|
|
77
|
+
* shows no control at all — only its label and the word `auto` — and still
|
|
78
|
+
* takes exactly the room it took while pinned, so nothing below it moves on
|
|
79
|
+
* the toggle. `Seed` is `.manual()` and does not toggle.
|
|
80
80
|
*/
|
|
81
81
|
export const BothStates: Story = {
|
|
82
82
|
render: () => <Pair />,
|
|
83
83
|
};
|
|
84
84
|
|
|
85
|
-
/** One panel to
|
|
85
|
+
/** One panel to click labels around in: the row must not resize as it toggles. */
|
|
86
86
|
export const Interactive: Story = {
|
|
87
87
|
render: () => <Panel start={['gap']} />,
|
|
88
88
|
};
|
|
@@ -5,3 +5,26 @@
|
|
|
5
5
|
.lk-control-panel {
|
|
6
6
|
padding: 0;
|
|
7
7
|
}
|
|
8
|
+
|
|
9
|
+
// A cell in a paired row, with the caption naming the knob it holds. Case,
|
|
10
|
+
// tracking, alignment and width come from the --wzl-params-label-* properties,
|
|
11
|
+
// which inherit across the CSS-module boundary that keeps PropertyPanel's row
|
|
12
|
+
// label class out of reach from here.
|
|
13
|
+
.lk-pair-cell {
|
|
14
|
+
display: inline-flex;
|
|
15
|
+
align-items: center;
|
|
16
|
+
gap: var(--wzl-space-3, 6px);
|
|
17
|
+
min-width: 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.lk-pair-label {
|
|
21
|
+
font-family: var(--wzl-font-display);
|
|
22
|
+
font-weight: var(--wzl-font-weight-light);
|
|
23
|
+
font-size: var(--wzl-font-size-sm);
|
|
24
|
+
color: var(--wzl-fg-muted);
|
|
25
|
+
text-transform: var(--wzl-params-label-case, uppercase);
|
|
26
|
+
letter-spacing: var(--wzl-params-label-tracking, var(--wzl-tracking-wide));
|
|
27
|
+
text-align: var(--wzl-params-label-align, start);
|
|
28
|
+
inline-size: var(--wzl-params-label-width, auto);
|
|
29
|
+
white-space: nowrap;
|
|
30
|
+
}
|
|
@@ -275,3 +275,44 @@ export const Presentation: Story = {
|
|
|
275
275
|
);
|
|
276
276
|
},
|
|
277
277
|
};
|
|
278
|
+
|
|
279
|
+
const inline = f.schema({
|
|
280
|
+
folderPad: f.number(1.1).range(0, 4).step(0.1).label('Folder pad'),
|
|
281
|
+
glyphs: f.number(1_000_000).range(0, 2_000_000).format('compact'),
|
|
282
|
+
names: f.number(60).range(0, 100),
|
|
283
|
+
labels: f.number(0).range(0, 100),
|
|
284
|
+
placement: f.enum('spread', ['spread', 'stack', 'ring']),
|
|
285
|
+
lift: f.number(0.2).range(0, 1).step(0.05),
|
|
286
|
+
opacity: f.number(0.45).range(0, 1).step(0.05),
|
|
287
|
+
iconSet: f.enum('small', ['small', 'large']).label('Icon set'),
|
|
288
|
+
drawAsIcons: f.string('**/node_modules/**').label('Draw as icons'),
|
|
289
|
+
detail: f.number(1200).range(0, 4000).suffix('nodes'),
|
|
290
|
+
showCameraRoute: f.boolean(false).toggle().label('Show camera route'),
|
|
291
|
+
routeColor: f.enum('sweep', ['sweep', 'depth']).label('Route color'),
|
|
292
|
+
minimap: f.boolean(true).toggle(),
|
|
293
|
+
minimapSize: f.number(280).range(80, 600).label('Minimap size'),
|
|
294
|
+
pickHud: f.boolean(true).toggle().label('Pick HUD'),
|
|
295
|
+
pickOutlines: f.boolean(true).toggle().label('Pick outlines'),
|
|
296
|
+
markOverlay: f.boolean(false).toggle().label('Mark overlay'),
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
/** The shape a lab's sidebar draws: `layout="inline"` paired two to a row, in a
|
|
300
|
+
* narrow column. Every row kind meets here — slider, select, switch, text —
|
|
301
|
+
* and they have to share one base height, or the column reads as ragged. */
|
|
302
|
+
export const Inline: Story = {
|
|
303
|
+
render: () => {
|
|
304
|
+
const schema = resolveConfigSchema(inline, []);
|
|
305
|
+
const [config, setConfig] = useState<Record<string, unknown>>(inline.defaults());
|
|
306
|
+
return (
|
|
307
|
+
<div style={{ width: 360 }}>
|
|
308
|
+
<ControlPanel
|
|
309
|
+
schema={schema}
|
|
310
|
+
config={config}
|
|
311
|
+
setConfig={(path, value) => setConfig((prev) => withValueAtPath(prev, path, value))}
|
|
312
|
+
layout="inline"
|
|
313
|
+
align="center"
|
|
314
|
+
/>
|
|
315
|
+
</div>
|
|
316
|
+
);
|
|
317
|
+
},
|
|
318
|
+
};
|
|
@@ -482,6 +482,24 @@ describe('<ControlPanel> format', () => {
|
|
|
482
482
|
});
|
|
483
483
|
});
|
|
484
484
|
|
|
485
|
+
describe('<ControlPanel> group descriptions', () => {
|
|
486
|
+
it('draws a named group’s describe() above its rows', () => {
|
|
487
|
+
const schema = resolveConfigSchema(
|
|
488
|
+
f.schema({
|
|
489
|
+
grid: f
|
|
490
|
+
.group({ cellSize: f.number(20) })
|
|
491
|
+
.label('Grid')
|
|
492
|
+
.describe('How the background lattice is spaced.'),
|
|
493
|
+
}),
|
|
494
|
+
[],
|
|
495
|
+
);
|
|
496
|
+
render(<ControlPanel schema={schema} config={{ grid: { cellSize: 20 } }} setConfig={vi.fn()} />);
|
|
497
|
+
const note = screen.getByText('How the background lattice is spaced.');
|
|
498
|
+
const field = screen.getByLabelText('Cell size');
|
|
499
|
+
expect(note.compareDocumentPosition(field) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
|
|
485
503
|
describe('<ControlPanel> section layout', () => {
|
|
486
504
|
const schema = resolveConfigSchema(
|
|
487
505
|
f.schema({
|
|
@@ -675,7 +693,7 @@ describe('<ControlPanel> nested groups', () => {
|
|
|
675
693
|
});
|
|
676
694
|
|
|
677
695
|
describe('<ControlPanel> auto', () => {
|
|
678
|
-
it('renders a
|
|
696
|
+
it('renders a label toggle whether or not it was given a set of auto paths', () => {
|
|
679
697
|
render(
|
|
680
698
|
<ControlPanel
|
|
681
699
|
schema={resolveConfigSchema(f.schema({ gap: f.number(12).range(0, 48) }), [])}
|
|
@@ -708,7 +726,7 @@ describe('<ControlPanel> auto', () => {
|
|
|
708
726
|
expect(setConfig).toHaveBeenCalledWith('gap', 12);
|
|
709
727
|
});
|
|
710
728
|
|
|
711
|
-
it(
|
|
729
|
+
it('reads out the bare word, resolver or not', () => {
|
|
712
730
|
render(
|
|
713
731
|
<ControlPanel
|
|
714
732
|
schema={resolveConfigSchema(
|
|
@@ -725,19 +743,8 @@ describe('<ControlPanel> auto', () => {
|
|
|
725
743
|
setConfig={() => {}}
|
|
726
744
|
/>,
|
|
727
745
|
);
|
|
728
|
-
expect(screen.getByText('auto · 18')).toBeInTheDocument();
|
|
729
|
-
});
|
|
730
|
-
|
|
731
|
-
it('reads "auto" alone for a path with no resolver', () => {
|
|
732
|
-
render(
|
|
733
|
-
<ControlPanel
|
|
734
|
-
schema={resolveConfigSchema(f.schema({ cols: f.number(3).range(0, 8) }), [])}
|
|
735
|
-
config={{ cols: 3 }}
|
|
736
|
-
auto={new Set(['cols'])}
|
|
737
|
-
setConfig={() => {}}
|
|
738
|
-
/>,
|
|
739
|
-
);
|
|
740
746
|
expect(screen.getByText('auto')).toBeInTheDocument();
|
|
747
|
+
expect(screen.queryByText(/auto · /)).toBeNull();
|
|
741
748
|
});
|
|
742
749
|
|
|
743
750
|
it('writes the sentinel when the dot turns a row auto', async () => {
|
|
@@ -794,11 +801,11 @@ describe('<ControlPanel> auto', () => {
|
|
|
794
801
|
expect(screen.queryByRole('button', { name: /Seed/ })).toBeNull();
|
|
795
802
|
});
|
|
796
803
|
|
|
797
|
-
it('
|
|
804
|
+
it('draws what the instrument reads when a resolver depends on another auto path', () => {
|
|
798
805
|
// `width` is auto with no resolver, so the instrument sees `undefined` and
|
|
799
806
|
// `gap` resolves from that. Re-running gap's resolver against the raw
|
|
800
|
-
// config would instead read the pinned 640 and
|
|
801
|
-
render(
|
|
807
|
+
// config would instead read the pinned 640 and draw a number nobody got.
|
|
808
|
+
const { container } = render(
|
|
802
809
|
<ControlPanel
|
|
803
810
|
schema={resolveConfigSchema(
|
|
804
811
|
f.schema({
|
|
@@ -806,7 +813,7 @@ describe('<ControlPanel> auto', () => {
|
|
|
806
813
|
gap: f
|
|
807
814
|
.number(12)
|
|
808
815
|
.range(0, 48)
|
|
809
|
-
.auto((c) => (c.width === undefined ?
|
|
816
|
+
.auto((c) => (c.width === undefined ? 5 : (c.width as number) / 64)),
|
|
810
817
|
}),
|
|
811
818
|
[],
|
|
812
819
|
)}
|
|
@@ -815,27 +822,7 @@ describe('<ControlPanel> auto', () => {
|
|
|
815
822
|
setConfig={() => {}}
|
|
816
823
|
/>,
|
|
817
824
|
);
|
|
818
|
-
expect(
|
|
819
|
-
expect(screen.queryByText('auto · 10')).toBeNull();
|
|
820
|
-
});
|
|
821
|
-
|
|
822
|
-
it('does not repeat a resolved value the control itself already shows', () => {
|
|
823
|
-
render(
|
|
824
|
-
<ControlPanel
|
|
825
|
-
schema={resolveConfigSchema(
|
|
826
|
-
f.schema({ caption: f.string('typed').auto(() => 'eight across') }),
|
|
827
|
-
[],
|
|
828
|
-
)}
|
|
829
|
-
config={{ caption: 'typed' }}
|
|
830
|
-
auto={new Set(['caption'])}
|
|
831
|
-
setConfig={() => {}}
|
|
832
|
-
/>,
|
|
833
|
-
);
|
|
834
|
-
// The field below the readout is already showing it, and the readout slot
|
|
835
|
-
// is one narrow column — a long string wraps it onto two lines.
|
|
836
|
-
expect(screen.getByText('auto')).toBeInTheDocument();
|
|
837
|
-
expect(screen.queryByText(/auto · /)).toBeNull();
|
|
838
|
-
expect(screen.getByDisplayValue('eight across')).toBeInTheDocument();
|
|
825
|
+
expect(container.querySelector('input[type=range]')).toHaveValue('5');
|
|
839
826
|
});
|
|
840
827
|
|
|
841
828
|
it('draws an auto control at the resolved value, not the value underneath it', () => {
|
|
@@ -881,7 +868,7 @@ describe('<ControlPanel> auto', () => {
|
|
|
881
868
|
});
|
|
882
869
|
|
|
883
870
|
// Every kind, because the panel wraps some rows before rendering them and a
|
|
884
|
-
// wrapper that drops the auto props leaves that kind silently
|
|
871
|
+
// wrapper that drops the auto props leaves that kind silently pinned.
|
|
885
872
|
it.each([
|
|
886
873
|
['number', f.number(3).range(0, 8)],
|
|
887
874
|
['slider', f.number(3).range(0, 8).slider()],
|
|
@@ -890,7 +877,7 @@ describe('<ControlPanel> auto', () => {
|
|
|
890
877
|
['radio', f.enum('a', ['a', 'b']).radio()],
|
|
891
878
|
['string', f.string('hi')],
|
|
892
879
|
['color', f.color('#3a86ff')],
|
|
893
|
-
])('marks a %s row auto and gives it a
|
|
880
|
+
])('marks a %s row auto and gives it a label toggle', (_kind, node) => {
|
|
894
881
|
const { container } = render(
|
|
895
882
|
<ControlPanel
|
|
896
883
|
schema={resolveConfigSchema(f.schema({ thing: node.label('Thing') }), [])}
|
|
@@ -899,76 +886,11 @@ describe('<ControlPanel> auto', () => {
|
|
|
899
886
|
setConfig={() => {}}
|
|
900
887
|
/>,
|
|
901
888
|
);
|
|
902
|
-
|
|
903
|
-
expect(row).not.toBeNull();
|
|
904
|
-
expect(row?.className).toMatch(/rowAuto/);
|
|
889
|
+
expect(container.querySelector('[class*=rowAuto]')).not.toBeNull();
|
|
905
890
|
expect(screen.getByRole('button', { name: 'Pin Thing' })).toBeInTheDocument();
|
|
906
891
|
});
|
|
907
892
|
});
|
|
908
893
|
|
|
909
|
-
describe('<ControlPanel> shift-click', () => {
|
|
910
|
-
const panel = (props = {}) =>
|
|
911
|
-
render(
|
|
912
|
-
<ControlPanel
|
|
913
|
-
schema={resolveConfigSchema(f.schema({ gap: f.number(12).range(0, 48) }), [])}
|
|
914
|
-
config={{ gap: 12 }}
|
|
915
|
-
auto={new Set()}
|
|
916
|
-
setConfig={() => {}}
|
|
917
|
-
{...props}
|
|
918
|
-
/>,
|
|
919
|
-
);
|
|
920
|
-
|
|
921
|
-
it('toggles a row on shift-pointerdown and suppresses the control it landed on', () => {
|
|
922
|
-
const setConfig = vi.fn();
|
|
923
|
-
const { container } = panel({ setConfig });
|
|
924
|
-
const track = container.querySelector('input[type=range]') as HTMLInputElement;
|
|
925
|
-
const ev = new PointerEvent('pointerdown', { bubbles: true, cancelable: true, shiftKey: true });
|
|
926
|
-
track.dispatchEvent(ev);
|
|
927
|
-
|
|
928
|
-
expect(setConfig).toHaveBeenCalledWith('gap', auto);
|
|
929
|
-
// A PROXY, not the real claim. jsdom synthesizes neither a label's click
|
|
930
|
-
// retargeting nor a range input's drag, so "the slider did not move" cannot
|
|
931
|
-
// fail here. What is assertable is that the default was suppressed; the
|
|
932
|
-
// browser half is covered by the story.
|
|
933
|
-
expect(ev.defaultPrevented).toBe(true);
|
|
934
|
-
});
|
|
935
|
-
|
|
936
|
-
it('leaves an ordinary pointerdown alone', () => {
|
|
937
|
-
const setConfig = vi.fn();
|
|
938
|
-
const { container } = panel({ setConfig });
|
|
939
|
-
container
|
|
940
|
-
.querySelector('input[type=range]')
|
|
941
|
-
?.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, cancelable: true }));
|
|
942
|
-
expect(setConfig).not.toHaveBeenCalled();
|
|
943
|
-
});
|
|
944
|
-
|
|
945
|
-
it('ignores shift-pointerdown on a manual row', () => {
|
|
946
|
-
const setConfig = vi.fn();
|
|
947
|
-
const { container } = panel({
|
|
948
|
-
schema: resolveConfigSchema(f.schema({ seed: f.number(1).manual().range(0, 8) }), []),
|
|
949
|
-
config: { seed: 1 },
|
|
950
|
-
setConfig,
|
|
951
|
-
});
|
|
952
|
-
container
|
|
953
|
-
.querySelector('input')
|
|
954
|
-
?.dispatchEvent(
|
|
955
|
-
new PointerEvent('pointerdown', { bubbles: true, cancelable: true, shiftKey: true }),
|
|
956
|
-
);
|
|
957
|
-
expect(setConfig).not.toHaveBeenCalled();
|
|
958
|
-
});
|
|
959
|
-
|
|
960
|
-
it('pins a shift-clicked auto row back at its stored value', () => {
|
|
961
|
-
const setConfig = vi.fn();
|
|
962
|
-
const { container } = panel({ config: { gap: 24 }, auto: new Set(['gap']), setConfig });
|
|
963
|
-
container
|
|
964
|
-
.querySelector('input[type=range]')
|
|
965
|
-
?.dispatchEvent(
|
|
966
|
-
new PointerEvent('pointerdown', { bubbles: true, cancelable: true, shiftKey: true }),
|
|
967
|
-
);
|
|
968
|
-
expect(setConfig).toHaveBeenCalledWith('gap', 24);
|
|
969
|
-
});
|
|
970
|
-
});
|
|
971
|
-
|
|
972
894
|
describe('<ControlPanel> pair', () => {
|
|
973
895
|
const paired = () =>
|
|
974
896
|
resolveConfigSchema(
|
|
@@ -986,9 +908,13 @@ describe('<ControlPanel> pair', () => {
|
|
|
986
908
|
const row = screen.getByText('Offset').closest('div') as HTMLElement;
|
|
987
909
|
expect(within(row).getByLabelText('X')).toHaveValue(10);
|
|
988
910
|
expect(within(row).getByLabelText('Y')).toHaveValue(20);
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
it('captions each cell of a pair with its own name, so a paired switch is not left unnamed', () => {
|
|
914
|
+
render(<ControlPanel schema={paired()} config={config} setConfig={vi.fn()} />);
|
|
915
|
+
const row = screen.getByText('Offset').closest('div') as HTMLElement;
|
|
916
|
+
expect(within(row).getByText('X')).toHaveClass('lk-pair-label');
|
|
917
|
+
expect(within(row).getByText('Y')).toHaveClass('lk-pair-label');
|
|
992
918
|
});
|
|
993
919
|
|
|
994
920
|
it('writes each cell of a paired row to its own path', () => {
|
|
@@ -998,6 +924,23 @@ describe('<ControlPanel> pair', () => {
|
|
|
998
924
|
expect(setConfig).toHaveBeenCalledWith('y', 30);
|
|
999
925
|
});
|
|
1000
926
|
|
|
927
|
+
it('keeps a segmented leaf segmented when it shares a row, and gives that row the width', () => {
|
|
928
|
+
const schema = resolveConfigSchema(
|
|
929
|
+
f.schema({
|
|
930
|
+
camera: f.enum<'2d' | '3d'>('3d', ['2d', '3d']).radio().label('').pair('Camera'),
|
|
931
|
+
hold: f.boolean(true).toggle().label('Hold front').pair('Camera'),
|
|
932
|
+
}),
|
|
933
|
+
[],
|
|
934
|
+
);
|
|
935
|
+
render(
|
|
936
|
+
<ControlPanel schema={schema} config={{ camera: '3d', hold: true }} setConfig={vi.fn()} />,
|
|
937
|
+
);
|
|
938
|
+
const row = screen.getByText('Camera').closest('div') as HTMLElement;
|
|
939
|
+
expect(within(row).getByRole('radio', { name: '2d' })).toBeInTheDocument();
|
|
940
|
+
expect(within(row).queryByRole('combobox')).toBeNull();
|
|
941
|
+
expect(row.className).toMatch(/span/);
|
|
942
|
+
});
|
|
943
|
+
|
|
1001
944
|
it('leaves an unpaired sibling on its own row', () => {
|
|
1002
945
|
render(<ControlPanel schema={paired()} config={config} setConfig={vi.fn()} />);
|
|
1003
946
|
const row = screen.getByText('Offset').closest('div') as HTMLElement;
|