@weasel-js/labkit 1.5.1 → 1.6.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 +3 -7
- package/dist/_dts/{CanvasStackContext-CJNMS6mz.d.ts → CanvasStackContext-DQgcH1ny.d.ts} +1 -1
- package/dist/_dts/LayerList.d-2c9yup5Y.d.ts +83 -0
- package/dist/_dts/{PrefsForm.d-DPy9I8It.d.ts → PrefsRow.d-DFpPIeIC.d.ts} +16 -1
- package/dist/_dts/{Select.d-Gwr8fLpK.d.ts → Select.d-BchGMC2Y.d.ts} +129 -12
- package/dist/_dts/{frac-B4okC2lX.d.ts → frac-DTRq7Xpb.d.ts} +31 -13
- package/dist/_dts/inDialog-D5ZKGjiD.d.ts +23 -0
- package/dist/_dts/{index-CSJoj4LC.d.ts → index-B0--8PEM.d.ts} +5 -4
- package/dist/_dts/index-DINmCuLA.d.ts +453 -0
- package/dist/_dts/number.d-OeQYQoUO.d.ts +28 -0
- package/dist/_dts/{types-DzoNY1pO.d.ts → types-B7y8hX1D.d.ts} +6 -6
- package/dist/_dts/{usePanZoom-Pzf9f2jh.d.ts → usePanZoom-Bq73pwFe.d.ts} +4 -47
- package/dist/_dts/{useTrialState-uLlmaccn.d.ts → useTrialState-CP4jfuKl.d.ts} +1 -1
- package/dist/canvas/index.d.ts +6 -5
- package/dist/canvas/index.js +1 -1
- package/dist/chrome/index.d.ts +5 -4
- package/dist/chrome/index.js +7 -7
- package/dist/chunk-2RVCB2PT.js +3 -0
- package/dist/chunk-2RVCB2PT.js.map +1 -0
- package/dist/{chunk-UN4U4P63.js → chunk-3FI7Y4R4.js} +9 -9
- package/dist/chunk-3FI7Y4R4.js.map +1 -0
- package/dist/{chunk-7PFH3CNM.js → chunk-AWH3SRPW.js} +31 -5
- package/dist/chunk-AWH3SRPW.js.map +1 -0
- package/dist/{chunk-ZDHY47EV.js → chunk-BVL7Y574.js} +25 -9
- package/dist/chunk-BVL7Y574.js.map +1 -0
- package/dist/{chunk-IHUVQ3TX.js → chunk-H5VIV7MQ.js} +31 -27
- package/dist/chunk-H5VIV7MQ.js.map +1 -0
- package/dist/chunk-HFRE4QQV.js +400 -0
- package/dist/chunk-HFRE4QQV.js.map +1 -0
- package/dist/{chunk-IXTQI7JX.js → chunk-MOLIUNVH.js} +4 -4
- package/dist/{chunk-IXTQI7JX.js.map → chunk-MOLIUNVH.js.map} +1 -1
- package/dist/{chunk-CEIUBPPW.js → chunk-TT5C5S6R.js} +3 -86
- package/dist/chunk-TT5C5S6R.js.map +1 -0
- package/dist/chunk-USO6TERP.js +90 -0
- package/dist/chunk-USO6TERP.js.map +1 -0
- package/dist/{chunk-PHK4W562.js → chunk-VQ47KWD5.js} +3 -3
- package/dist/{chunk-PHK4W562.js.map → chunk-VQ47KWD5.js.map} +1 -1
- package/dist/chunk-WUJXJC5O.js +14568 -0
- package/dist/chunk-WUJXJC5O.js.map +1 -0
- package/dist/{chunk-EHGVROVV.js → chunk-ZORVNOKX.js} +6 -25
- package/dist/chunk-ZORVNOKX.js.map +1 -0
- package/dist/config/index.d.ts +53 -11
- package/dist/config/index.js +5 -5
- package/dist/controls/index.d.ts +6 -3
- package/dist/controls/index.js +4 -4
- package/dist/dragdrop/index.d.ts +4 -3
- package/dist/index.d.ts +139 -114
- package/dist/index.js +470 -479
- package/dist/index.js.map +1 -1
- package/dist/layers/index.d.ts +6 -35
- package/dist/layers/index.js +2 -3
- package/dist/loupe/index.d.ts +6 -5
- package/dist/passthrough/weasel-ui.d.ts +421 -215
- package/dist/passthrough/weasel-ui.js +1 -1
- package/dist/primitives/index.d.ts +2 -13
- package/dist/primitives/index.js +4 -4
- package/dist/state/index.d.ts +6 -5
- package/dist/state/index.js +5 -5
- package/dist/styles.css +410 -190
- package/dist/undo/index.d.ts +4 -3
- package/package.json +9 -13
- package/src/annotations/frac.test.ts +7 -0
- package/src/annotations/paint.test.ts +21 -0
- package/src/annotations/paint.ts +12 -0
- package/src/annotations/store.test.ts +31 -0
- package/src/annotations/types.ts +3 -2
- package/src/canvas/Stage.test.tsx +18 -0
- 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/SidebarRegion.less +197 -0
- package/src/chrome/regions/SidebarRegion.test.tsx +20 -0
- package/src/chrome/regions/SidebarRegion.tsx +7 -2
- package/src/chrome/regions/TitleBarRegion.tsx +1 -1
- package/src/chrome/types.ts +13 -4
- package/src/config/autoConfig.ts +2 -2
- package/src/config/builder.ts +23 -5
- package/src/config/entry.test.ts +8 -0
- package/src/config/index.ts +2 -0
- package/src/config/rules.ts +2 -0
- package/src/config/sectionTree.test.ts +146 -0
- package/src/config/sectionTree.ts +124 -0
- package/src/config/types.ts +4 -4
- package/src/controls/AutoControls.stories.tsx +6 -6
- package/src/controls/ControlPanel.less +27 -0
- package/src/controls/ControlPanel.stories.tsx +95 -1
- package/src/controls/ControlPanel.test.tsx +82 -112
- package/src/controls/ControlPanel.tsx +159 -396
- package/src/controls/inDialog.test.tsx +82 -0
- package/src/controls/inDialog.tsx +46 -0
- package/src/controls/index.ts +1 -0
- package/src/index.test.ts +0 -2
- package/src/index.ts +14 -35
- package/src/instrument/types.ts +5 -2
- package/src/lab/Lab.nebula.test.tsx +31 -0
- package/src/lab/Lab.tsx +57 -18
- package/src/lab/LabBoundary.test.tsx +66 -0
- package/src/lab/LabBoundary.tsx +56 -0
- package/src/lab/LabHeader.tsx +4 -23
- package/src/lab/LabRoot.tsx +2 -3
- package/src/lab/LabShell.less +0 -1
- package/src/lab/LabShell.stories.tsx +15 -0
- package/src/lab/LabShell.test.tsx +18 -0
- package/src/lab/LabShell.tsx +14 -1
- package/src/lab/LabSwitcher.less +1 -1
- package/src/lab/LabSwitcher.test.tsx +41 -0
- package/src/lab/LabSwitcher.tsx +30 -8
- package/src/lab/index.ts +1 -0
- package/src/layers/AGENTS.md +18 -74
- package/src/layers/index.ts +4 -2
- package/src/passthrough/weasel-ui.ts +25 -10
- 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/primitives/index.ts +0 -2
- package/src/specimen/Specimen.tsx +42 -56
- package/src/state/types.ts +8 -2
- package/src/state/view.test.ts +4 -4
- package/src/styles.less +4 -2
- package/src/theme/Interstellar.stories.tsx +8 -9
- package/src/theme/base.less +3 -20
- package/src/theme/interstellar.test.ts +6 -3
- package/src/theme/interstellar.theme.json +0 -56
- package/src/theme/interstellar.ts +2 -2
- package/src/trial/Trial.canvas.test.tsx +9 -5
- package/src/trial/Trial.config.test.tsx +1 -1
- package/src/trial/Trial.tsx +30 -8
- package/dist/_dts/index-BNdz-tNd.d.ts +0 -423
- package/dist/chunk-7PFH3CNM.js.map +0 -1
- package/dist/chunk-CEIUBPPW.js.map +0 -1
- package/dist/chunk-EH7UEQV7.js +0 -590
- package/dist/chunk-EH7UEQV7.js.map +0 -1
- package/dist/chunk-EHGVROVV.js.map +0 -1
- package/dist/chunk-IHUVQ3TX.js.map +0 -1
- package/dist/chunk-MDVTK5U3.js +0 -170
- package/dist/chunk-MDVTK5U3.js.map +0 -1
- package/dist/chunk-PJ7WEQBJ.js +0 -18
- package/dist/chunk-PJ7WEQBJ.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/dist/ui/layers/index.d.ts +0 -66
- package/dist/ui/layers/index.js +0 -3
- package/dist/ui/layers/index.js.map +0 -1
- package/src/canvas/useOrbit.test.ts +0 -167
- package/src/canvas/useOrbit.ts +0 -156
- package/src/lab/useSystemMode.ts +0 -30
- package/src/layers/LayerList.less +0 -93
- package/src/layers/LayerList.stories.tsx +0 -66
- package/src/layers/LayerList.test.tsx +0 -232
- package/src/layers/LayerList.tsx +0 -268
- package/src/primitives/Sidebar.less +0 -112
- package/src/primitives/Sidebar.stories.tsx +0 -38
- package/src/primitives/Sidebar.test.tsx +0 -74
- package/src/primitives/Sidebar.tsx +0 -35
- package/src/ui/layers/index.ts +0 -5
package/src/config/builder.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { PrefLeaf, PrefNumberFormat, PrefNumberUnit } from '@weasel-js/ui';
|
|
2
|
+
import { type InDialogOptions, inDialog } from '../controls/inDialog';
|
|
2
3
|
import { type Auto, isAuto } from './auto';
|
|
3
4
|
import type {
|
|
4
5
|
Annotations,
|
|
@@ -91,6 +92,12 @@ export abstract class BaseNode<T> implements ConfigNode<T> {
|
|
|
91
92
|
return this.opt({ render: renderer });
|
|
92
93
|
}
|
|
93
94
|
|
|
95
|
+
/** Draw this row as a button that opens a dialog, with `body` as the
|
|
96
|
+
* dialog's content — for an editor too big for the row. */
|
|
97
|
+
dialog(body: ControlRenderer, opts?: InDialogOptions): this {
|
|
98
|
+
return this.render(inDialog(body, opts));
|
|
99
|
+
}
|
|
100
|
+
|
|
94
101
|
/**
|
|
95
102
|
* Compute this leaf's value while it is auto, instead of leaving it
|
|
96
103
|
* `undefined`. The resolver is given the config with every other auto path
|
|
@@ -110,7 +117,7 @@ export abstract class BaseNode<T> implements ConfigNode<T> {
|
|
|
110
117
|
return this.opt({ unpinned: true });
|
|
111
118
|
}
|
|
112
119
|
|
|
113
|
-
/** Never auto. The row
|
|
120
|
+
/** Never auto. The row's label does not toggle. */
|
|
114
121
|
manual(): this {
|
|
115
122
|
return this.opt({ manual: true });
|
|
116
123
|
}
|
|
@@ -134,7 +141,7 @@ export class NumberNode extends BaseNode<number> {
|
|
|
134
141
|
return this.ann({ suffix });
|
|
135
142
|
}
|
|
136
143
|
|
|
137
|
-
/** Show the value in a named format — `'compact'` reads `2.
|
|
144
|
+
/** Show the value in a named format — `'compact'` reads `2.00M`. Presentation
|
|
138
145
|
* only: the stored value stays a plain number, and a typed `2.5m` reads back. */
|
|
139
146
|
format(format: PrefNumberFormat): this {
|
|
140
147
|
return this.ann({ format });
|
|
@@ -187,6 +194,15 @@ export class StringNode extends BaseNode<string> {
|
|
|
187
194
|
}
|
|
188
195
|
}
|
|
189
196
|
|
|
197
|
+
export class ListNode extends BaseNode<string[]> {
|
|
198
|
+
readonly kind = 'list';
|
|
199
|
+
|
|
200
|
+
/** Shown in an empty entry. */
|
|
201
|
+
placeholder(placeholder: string): this {
|
|
202
|
+
return this.ann({ placeholder });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
190
206
|
export class ColorNode extends BaseNode<string> {
|
|
191
207
|
readonly kind = 'color';
|
|
192
208
|
|
|
@@ -254,9 +270,8 @@ export class GroupNode<S extends ConfigShape> implements ConfigBranch<S> {
|
|
|
254
270
|
return this.with({ ...this.annotations, name }, this.options);
|
|
255
271
|
}
|
|
256
272
|
|
|
257
|
-
/** Longer help text. Carried on the resolved `PrefGroup
|
|
258
|
-
* `PrefsForm`
|
|
259
|
-
* it yet and shows the heading alone. */
|
|
273
|
+
/** Longer help text. Carried on the resolved `PrefGroup`; both weasel-ui's
|
|
274
|
+
* `PrefsForm` and `ControlPanel` draw it under the heading. */
|
|
260
275
|
describe(description: string): GroupNode<S> {
|
|
261
276
|
return this.with({ ...this.annotations, description }, this.options);
|
|
262
277
|
}
|
|
@@ -305,6 +320,9 @@ export const f = {
|
|
|
305
320
|
boolean: (def: boolean): BooleanNode => new BooleanNode(def),
|
|
306
321
|
string: (def: string): StringNode => new StringNode(def),
|
|
307
322
|
color: (def: string): ColorNode => new ColorNode(def),
|
|
323
|
+
/** A list of strings. Its row summarizes the list, and opens a dialog to
|
|
324
|
+
* edit it one entry per field. */
|
|
325
|
+
list: (def: readonly string[]): ListNode => new ListNode([...def]),
|
|
308
326
|
|
|
309
327
|
/** A fixed set of choices. The default's literal type flows into the config
|
|
310
328
|
* type, so `f.enum('fast', ['fast', 'accurate'])` gives
|
package/src/config/entry.test.ts
CHANGED
|
@@ -36,6 +36,14 @@ describe('@weasel-js/labkit/config', () => {
|
|
|
36
36
|
expect(files.filter((f) => /\.(less|css)$/.test(f))).toEqual([]);
|
|
37
37
|
});
|
|
38
38
|
|
|
39
|
+
// A forge frame loads this entry for every story, and ui's barrel is 167 modules on the dev server.
|
|
40
|
+
it('loads nothing from @weasel-js/ui at runtime', () => {
|
|
41
|
+
const importers = [...walk(resolve(__dirname, 'index.ts'))].filter((file) =>
|
|
42
|
+
[...readFileSync(file, 'utf8').matchAll(IMPORT)].some((m) => (m[1] ?? m[2] ?? m[3])?.startsWith('@weasel-js/ui')),
|
|
43
|
+
);
|
|
44
|
+
expect(importers).toEqual([]);
|
|
45
|
+
});
|
|
46
|
+
|
|
39
47
|
it('is published as an entry', async () => {
|
|
40
48
|
const pkg = JSON.parse(readFileSync(resolve(__dirname, '../../package.json'), 'utf8'));
|
|
41
49
|
expect(pkg.exports['./config']).toEqual({
|
package/src/config/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ export {
|
|
|
9
9
|
f,
|
|
10
10
|
GroupNode,
|
|
11
11
|
isConfigBranch,
|
|
12
|
+
ListNode,
|
|
12
13
|
NumberNode,
|
|
13
14
|
StringNode,
|
|
14
15
|
ValueNode,
|
|
@@ -22,6 +23,7 @@ export {
|
|
|
22
23
|
withValueAtPath,
|
|
23
24
|
} from './path';
|
|
24
25
|
export { resolveConfigSchema } from './resolve';
|
|
26
|
+
export { type SectionTree, sectionTree } from './sectionTree';
|
|
25
27
|
export { applyRules, builtinRules, titleCase } from './rules';
|
|
26
28
|
export type {
|
|
27
29
|
Annotations,
|
package/src/config/rules.ts
CHANGED
|
@@ -15,6 +15,8 @@ export function titleCase(key: string): string {
|
|
|
15
15
|
const kindFromValue: ConfigRule = (ctx) => {
|
|
16
16
|
const t = typeof ctx.default;
|
|
17
17
|
if (t === 'boolean' || t === 'number' || t === 'string') return { kind: t };
|
|
18
|
+
const d = ctx.default;
|
|
19
|
+
if (Array.isArray(d) && d.every((v) => typeof v === 'string')) return { kind: 'list' };
|
|
18
20
|
return null;
|
|
19
21
|
};
|
|
20
22
|
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { isPrefLeaf, type PrefGroup } from '@weasel-js/ui';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { f } from './builder';
|
|
4
|
+
import { resolveConfigSchema } from './resolve';
|
|
5
|
+
import { sectionTree } from './sectionTree';
|
|
6
|
+
|
|
7
|
+
const groupAt = (g: PrefGroup, key: string) => {
|
|
8
|
+
const child = g.children[key];
|
|
9
|
+
if (!child || isPrefLeaf(child)) throw new Error(`${key} is not a group`);
|
|
10
|
+
return child;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
describe('sectionTree', () => {
|
|
14
|
+
it('makes each root section a top-level group, in schema order', () => {
|
|
15
|
+
const resolved = resolveConfigSchema(
|
|
16
|
+
f.schema({
|
|
17
|
+
lift: f.number(1).section('View'),
|
|
18
|
+
beat: f.number(2).section('Playback'),
|
|
19
|
+
spread: f.number(3).section('View'),
|
|
20
|
+
}),
|
|
21
|
+
);
|
|
22
|
+
const { group } = sectionTree(resolved, { lift: 1, beat: 2, spread: 3 });
|
|
23
|
+
expect(Object.keys(group.children)).toEqual(['view', 'playback']);
|
|
24
|
+
expect(groupAt(group, 'view').name).toBe('View');
|
|
25
|
+
expect(Object.keys(groupAt(group, 'view').children)).toEqual(['lift', 'spread']);
|
|
26
|
+
expect(Object.keys(groupAt(group, 'playback').children)).toEqual(['beat']);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('brings a group a section names in whole, so the rail can indent it', () => {
|
|
30
|
+
const resolved = resolveConfigSchema(
|
|
31
|
+
f.schema({
|
|
32
|
+
cards: f
|
|
33
|
+
.group({ floor: f.number(1), cap: f.number(2) })
|
|
34
|
+
.section('View')
|
|
35
|
+
.label('Cards'),
|
|
36
|
+
}),
|
|
37
|
+
);
|
|
38
|
+
const { group } = sectionTree(resolved, { cards: { floor: 1, cap: 2 } });
|
|
39
|
+
const cards = groupAt(groupAt(group, 'view'), 'cards');
|
|
40
|
+
expect(cards.name).toBe('Cards');
|
|
41
|
+
expect(Object.keys(cards.children)).toEqual(['floor', 'cap']);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('renests the config to mirror the tree', () => {
|
|
45
|
+
const resolved = resolveConfigSchema(
|
|
46
|
+
f.schema({
|
|
47
|
+
cards: f.group({ floor: f.number(1) }).section('View'),
|
|
48
|
+
beat: f.number(2).section('Playback'),
|
|
49
|
+
}),
|
|
50
|
+
);
|
|
51
|
+
const { values } = sectionTree(resolved, { cards: { floor: 7 }, beat: 9 });
|
|
52
|
+
expect(values).toEqual({ view: { cards: { floor: 7 } }, playback: { beat: 9 } });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('cuts what showIf rules out, at any depth', () => {
|
|
56
|
+
const resolved = resolveConfigSchema(
|
|
57
|
+
f.schema({
|
|
58
|
+
lineNumbers: f.boolean(true).section('View'),
|
|
59
|
+
listing: f
|
|
60
|
+
.group({
|
|
61
|
+
dim: f.number(1).showIf((c) => c.lineNumbers === true),
|
|
62
|
+
weight: f.number(0),
|
|
63
|
+
})
|
|
64
|
+
.section('View'),
|
|
65
|
+
}),
|
|
66
|
+
);
|
|
67
|
+
const on = sectionTree(resolved, { lineNumbers: true, listing: { dim: 1, weight: 0 } });
|
|
68
|
+
expect(Object.keys(groupAt(groupAt(on.group, 'view'), 'listing').children)).toEqual([
|
|
69
|
+
'dim',
|
|
70
|
+
'weight',
|
|
71
|
+
]);
|
|
72
|
+
const off = sectionTree(resolved, { lineNumbers: false, listing: { dim: 1, weight: 0 } });
|
|
73
|
+
expect(Object.keys(groupAt(groupAt(off.group, 'view'), 'listing').children)).toEqual(['weight']);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('drops a section nothing in it is showing, rather than opening it onto nothing', () => {
|
|
77
|
+
const resolved = resolveConfigSchema(
|
|
78
|
+
f.schema({
|
|
79
|
+
mode: f.enum<string>('a', ['a', 'b']).section('View'),
|
|
80
|
+
glow: f.number(1).section('Diff').showIf((c) => c.mode === 'b'),
|
|
81
|
+
}),
|
|
82
|
+
);
|
|
83
|
+
expect(Object.keys(sectionTree(resolved, { mode: 'b', glow: 1 }).group.children)).toEqual([
|
|
84
|
+
'view',
|
|
85
|
+
'diff',
|
|
86
|
+
]);
|
|
87
|
+
expect(Object.keys(sectionTree(resolved, { mode: 'a', glow: 1 }).group.children)).toEqual([
|
|
88
|
+
'view',
|
|
89
|
+
]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('keeps a hidden leaf out unless asked for it', () => {
|
|
93
|
+
const resolved = resolveConfigSchema(
|
|
94
|
+
f.schema({ seed: f.number(1).hidden().section('Debug'), route: f.boolean(true).section('Debug') }),
|
|
95
|
+
);
|
|
96
|
+
expect(Object.keys(groupAt(sectionTree(resolved, {}).group, 'debug').children)).toEqual([
|
|
97
|
+
'route',
|
|
98
|
+
]);
|
|
99
|
+
expect(Object.keys(groupAt(sectionTree(resolved, {}, true).group, 'debug').children)).toEqual([
|
|
100
|
+
'seed',
|
|
101
|
+
'route',
|
|
102
|
+
]);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('leaves a node no section claimed at the top level', () => {
|
|
106
|
+
const resolved = resolveConfigSchema(
|
|
107
|
+
f.schema({ lift: f.number(1).section('View'), loose: f.number(2) }),
|
|
108
|
+
);
|
|
109
|
+
const { group, values } = sectionTree(resolved, { lift: 1, loose: 2 });
|
|
110
|
+
expect(Object.keys(group.children)).toEqual(['view', 'loose']);
|
|
111
|
+
expect(values.loose).toBe(2);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('pathAt gives back the config path a rail path stands for', () => {
|
|
115
|
+
const resolved = resolveConfigSchema(
|
|
116
|
+
f.schema({
|
|
117
|
+
cards: f.group({ floor: f.number(1) }).section('View'),
|
|
118
|
+
loose: f.number(2),
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
121
|
+
const { pathAt } = sectionTree(resolved, { cards: { floor: 1 }, loose: 2 });
|
|
122
|
+
expect(pathAt('view.cards.floor')).toBe('cards.floor');
|
|
123
|
+
expect(pathAt('loose')).toBe('loose');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('keys two sections whose labels slug the same apart', () => {
|
|
127
|
+
const resolved = resolveConfigSchema(
|
|
128
|
+
f.schema({ a: f.number(1).section('Hold on'), b: f.number(2).section('Hold-on') }),
|
|
129
|
+
);
|
|
130
|
+
const { group } = sectionTree(resolved, { a: 1, b: 2 });
|
|
131
|
+
expect(Object.keys(group.children)).toEqual(['hold-on', 'hold-on-2']);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('ignores a section nested under a group, which buckets rows in a pane', () => {
|
|
135
|
+
const resolved = resolveConfigSchema(
|
|
136
|
+
f.schema({
|
|
137
|
+
cards: f
|
|
138
|
+
.group({ floor: f.number(1).section('Size') })
|
|
139
|
+
.section('View'),
|
|
140
|
+
}),
|
|
141
|
+
);
|
|
142
|
+
const { group } = sectionTree(resolved, { cards: { floor: 1 } });
|
|
143
|
+
expect(Object.keys(group.children)).toEqual(['view']);
|
|
144
|
+
expect(Object.keys(groupAt(groupAt(group, 'view'), 'cards').children)).toEqual(['floor']);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { isPrefLeaf, type PrefGroup } from '@weasel-js/ui';
|
|
2
|
+
import { valueAtPath } from './path';
|
|
3
|
+
import type { ResolvedConfig } from './types';
|
|
4
|
+
import { isLeafVisible } from './visible';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A resolved schema rearranged so its sections are groups.
|
|
8
|
+
*
|
|
9
|
+
* `PrefsForm`'s rail navigates the schema tree — top-level groups are its
|
|
10
|
+
* items, their group children indent under them. A resolved schema keeps its
|
|
11
|
+
* sections beside the tree instead, so a flat schema's rail comes out as one
|
|
12
|
+
* unnamed item however many headings the panel draws. This puts the sections
|
|
13
|
+
* where the rail looks for them.
|
|
14
|
+
*/
|
|
15
|
+
export interface SectionTree {
|
|
16
|
+
/** The rail's schema. Top-level children are the schema's root sections,
|
|
17
|
+
* followed by any root node no section claimed. */
|
|
18
|
+
group: PrefGroup;
|
|
19
|
+
/** `config` renested to mirror `group` — the value tree `PrefsForm` takes. */
|
|
20
|
+
values: Record<string, unknown>;
|
|
21
|
+
/** The config path a path within `group` stands for. */
|
|
22
|
+
pathAt: (railPath: string) => string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** A section label as an object key: no dots, since a dotted path is how every
|
|
26
|
+
* caller addresses a node, and a label is free to hold one. */
|
|
27
|
+
function slugOf(label: string, taken: ReadonlySet<string>): string {
|
|
28
|
+
const base = label.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'section';
|
|
29
|
+
if (!taken.has(base)) return base;
|
|
30
|
+
for (let n = 2; ; n += 1) if (!taken.has(`${base}-${n}`)) return `${base}-${n}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A subtree with everything `showIf` or `hidden` rules out cut away, or null
|
|
34
|
+
* when that is all of it. A group whose children all went is not a rail item
|
|
35
|
+
* the reader can open onto nothing. */
|
|
36
|
+
function prune(
|
|
37
|
+
resolved: ResolvedConfig,
|
|
38
|
+
group: PrefGroup,
|
|
39
|
+
at: string,
|
|
40
|
+
config: Record<string, unknown>,
|
|
41
|
+
showHidden: boolean,
|
|
42
|
+
): PrefGroup | null {
|
|
43
|
+
const children: PrefGroup['children'] = {};
|
|
44
|
+
for (const [key, child] of Object.entries(group.children)) {
|
|
45
|
+
const path = at === '' ? key : `${at}.${key}`;
|
|
46
|
+
if (!isLeafVisible(resolved, path, config, showHidden)) continue;
|
|
47
|
+
if (isPrefLeaf(child)) {
|
|
48
|
+
children[key] = child;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const kept = prune(resolved, child, path, config, showHidden);
|
|
52
|
+
if (kept) children[key] = kept;
|
|
53
|
+
}
|
|
54
|
+
return Object.keys(children).length === 0 ? null : { ...group, children };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Turn a resolved schema's root sections into the top level of a `PrefGroup`,
|
|
59
|
+
* for `PrefsForm`'s rail layout.
|
|
60
|
+
*
|
|
61
|
+
* A section naming a group brings that group in whole, so it lands as an
|
|
62
|
+
* indented rail item; a section naming a leaf drops it loose into the
|
|
63
|
+
* section's own pane. Sections nested under a group are left alone — they
|
|
64
|
+
* bucket rows inside a pane, which is the panel's job and not the rail's.
|
|
65
|
+
*/
|
|
66
|
+
export function sectionTree(
|
|
67
|
+
resolved: ResolvedConfig,
|
|
68
|
+
config: Record<string, unknown>,
|
|
69
|
+
showHidden = false,
|
|
70
|
+
): SectionTree {
|
|
71
|
+
const children: PrefGroup['children'] = {};
|
|
72
|
+
const values: Record<string, unknown> = {};
|
|
73
|
+
const slugs = new Set<string>();
|
|
74
|
+
const claimed = new Set<string>();
|
|
75
|
+
|
|
76
|
+
for (const section of resolved.sections) {
|
|
77
|
+
if (section.at !== '') continue;
|
|
78
|
+
const kept: PrefGroup['children'] = {};
|
|
79
|
+
const held: Record<string, unknown> = {};
|
|
80
|
+
for (const path of section.paths) {
|
|
81
|
+
claimed.add(path);
|
|
82
|
+
const child = resolved.group.children[path];
|
|
83
|
+
if (!child || !isLeafVisible(resolved, path, config, showHidden)) continue;
|
|
84
|
+
if (isPrefLeaf(child)) {
|
|
85
|
+
kept[path] = child;
|
|
86
|
+
} else {
|
|
87
|
+
const subtree = prune(resolved, child, path, config, showHidden);
|
|
88
|
+
if (!subtree) continue;
|
|
89
|
+
kept[path] = subtree;
|
|
90
|
+
}
|
|
91
|
+
held[path] = valueAtPath(config, path);
|
|
92
|
+
}
|
|
93
|
+
if (Object.keys(kept).length === 0) continue;
|
|
94
|
+
const slug = slugOf(section.label, slugs);
|
|
95
|
+
slugs.add(slug);
|
|
96
|
+
children[slug] = { name: section.label, children: kept };
|
|
97
|
+
values[slug] = held;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Whatever no section claimed stays at the top level, where `prefRailItems`
|
|
101
|
+
// gathers loose leaves into one item named for the root.
|
|
102
|
+
for (const [key, child] of Object.entries(resolved.group.children)) {
|
|
103
|
+
if (claimed.has(key) || !isLeafVisible(resolved, key, config, showHidden)) continue;
|
|
104
|
+
if (isPrefLeaf(child)) {
|
|
105
|
+
children[key] = child;
|
|
106
|
+
} else {
|
|
107
|
+
const subtree = prune(resolved, child, key, config, showHidden);
|
|
108
|
+
if (!subtree) continue;
|
|
109
|
+
children[key] = subtree;
|
|
110
|
+
}
|
|
111
|
+
values[key] = valueAtPath(config, key);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
group: { name: resolved.group.name, children },
|
|
116
|
+
values,
|
|
117
|
+
pathAt: (railPath) => {
|
|
118
|
+
const cut = railPath.indexOf('.');
|
|
119
|
+
const head = cut === -1 ? railPath : railPath.slice(0, cut);
|
|
120
|
+
if (!slugs.has(head)) return railPath;
|
|
121
|
+
return cut === -1 ? '' : railPath.slice(cut + 1);
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
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
|
};
|
|
@@ -2,6 +2,33 @@
|
|
|
2
2
|
// from PropertyPanel.less. This class remains as the consumer-facing hook.
|
|
3
3
|
// It carries no padding: whatever mounts the panel insets it, and a second
|
|
4
4
|
// inset here doubled the gutter inside an already narrow sidebar.
|
|
5
|
+
// A select row aligns to its value's baseline by default, which sits a label
|
|
6
|
+
// lower than the centered rows around it — here those neighbors are sliders and
|
|
7
|
+
// checkboxes, so a column of labels stepped at every dropdown.
|
|
5
8
|
.lk-control-panel {
|
|
6
9
|
padding: 0;
|
|
10
|
+
--wzl-prop-row-align-text: center;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// A cell in a paired row, with the caption naming the knob it holds. Case,
|
|
14
|
+
// tracking, alignment and width come from the --wzl-params-label-* properties,
|
|
15
|
+
// which inherit across the CSS-module boundary that keeps PropertyPanel's row
|
|
16
|
+
// label class out of reach from here.
|
|
17
|
+
.lk-pair-cell {
|
|
18
|
+
display: inline-flex;
|
|
19
|
+
align-items: center;
|
|
20
|
+
gap: var(--wzl-space-3, 6px);
|
|
21
|
+
min-width: 0;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.lk-pair-label {
|
|
25
|
+
font-family: var(--wzl-font-display);
|
|
26
|
+
font-weight: var(--wzl-font-weight-light);
|
|
27
|
+
font-size: var(--wzl-font-size-sm);
|
|
28
|
+
color: var(--wzl-fg-muted);
|
|
29
|
+
text-transform: var(--wzl-params-label-case, uppercase);
|
|
30
|
+
letter-spacing: var(--wzl-params-label-tracking, var(--wzl-tracking-wide));
|
|
31
|
+
text-align: var(--wzl-params-label-align, start);
|
|
32
|
+
inline-size: var(--wzl-params-label-width, auto);
|
|
33
|
+
white-space: nowrap;
|
|
7
34
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ANGLE_RADIANS, prefUnit } from '@weasel-js/core';
|
|
2
2
|
import type { Meta, StoryObj } from '@weasel-js/forge';
|
|
3
|
-
import { PropertyRow } from '@weasel-js/ui';
|
|
3
|
+
import { type PrefNumberUnit, PropertyRow } from '@weasel-js/ui';
|
|
4
4
|
import { useState } from 'react';
|
|
5
5
|
import { f } from '../config/builder';
|
|
6
6
|
import { withValueAtPath } from '../config/path';
|
|
@@ -275,3 +275,97 @@ 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
|
|
289
|
+
.list(['**/node_modules/**', '**/dist/**', 'package-lock.json'])
|
|
290
|
+
.label('Draw as icons'),
|
|
291
|
+
detail: f.number(1200).range(0, 4000).suffix('nodes'),
|
|
292
|
+
showCameraRoute: f.boolean(false).toggle().label('Show camera route'),
|
|
293
|
+
routeColor: f.enum('sweep', ['sweep', 'depth']).label('Route color'),
|
|
294
|
+
minimap: f.boolean(true).toggle(),
|
|
295
|
+
minimapSize: f.number(280).range(80, 600).label('Minimap size'),
|
|
296
|
+
pickHud: f.boolean(true).toggle().label('Pick HUD'),
|
|
297
|
+
pickOutlines: f.boolean(true).toggle().label('Pick outlines'),
|
|
298
|
+
markOverlay: f.boolean(false).toggle().label('Mark overlay'),
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
/** The shape a lab's sidebar draws: `layout="inline"` paired two to a row, in a
|
|
302
|
+
* narrow column. Every row kind meets here — slider, select, switch, text —
|
|
303
|
+
* and they have to share one base height, or the column reads as ragged. */
|
|
304
|
+
export const Inline: Story = {
|
|
305
|
+
render: () => {
|
|
306
|
+
const schema = resolveConfigSchema(inline, []);
|
|
307
|
+
const [config, setConfig] = useState<Record<string, unknown>>(inline.defaults());
|
|
308
|
+
return (
|
|
309
|
+
<div style={{ width: 360 }}>
|
|
310
|
+
<ControlPanel
|
|
311
|
+
schema={schema}
|
|
312
|
+
config={config}
|
|
313
|
+
setConfig={(path, value) => setConfig((prev) => withValueAtPath(prev, path, value))}
|
|
314
|
+
layout="inline"
|
|
315
|
+
align="center"
|
|
316
|
+
/>
|
|
317
|
+
</div>
|
|
318
|
+
);
|
|
319
|
+
},
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const shownIn = (suffix: string): PrefNumberUnit => ({
|
|
323
|
+
toDisplay: (v) => v,
|
|
324
|
+
fromDisplay: (v) => v,
|
|
325
|
+
suffix,
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
/** Every slider in astv's right sidebar, for sizing readouts against real ranges, steps and units. */
|
|
329
|
+
const sidebarSliders = f.schema({
|
|
330
|
+
aimNoise: f.number(0).range(0, 0.6).step(0.02).label('Aim noise'),
|
|
331
|
+
transition: f.number(600).range(0, 1200).step(50).unit(shownIn('ms')).label('Transition'),
|
|
332
|
+
sweep: f.number(12).range(0, 40).step(0.5).label('Text motion'),
|
|
333
|
+
beat: f.number(1600).range(400, 6000).step(100).unit(shownIn('ms')).label('Beat'),
|
|
334
|
+
tourSeed: f.number(1).range(1, 99).step(1).label('Tour seed'),
|
|
335
|
+
hold: f.number(1200).range(0, 4000).step(10).unit(shownIn('ms')).label('Demo hold'),
|
|
336
|
+
scrollRows: f.number(3).range(1, 12).step(1).label('Lines'),
|
|
337
|
+
pad: f.number(0.5).range(0, 4).step(0.25).label('Window pad'),
|
|
338
|
+
folderPad: f.number(1).range(0, 4).step(0.1).label('Folder pad'),
|
|
339
|
+
levels: f.number(2).range(0, 6).step(1).label('Levels'),
|
|
340
|
+
glyphs: f.number(1_000_000).range(0, 2_000_000).step(2000).format('compact').label('Glyphs'),
|
|
341
|
+
names: f.number(40).range(0, 200).step(5).label('Names'),
|
|
342
|
+
labels: f.number(0).range(0, 30).step(1).label('Labels'),
|
|
343
|
+
text: f.number(100).range(50, 250).step(5).unit(shownIn('%')).label('Text'),
|
|
344
|
+
lift: f.number(0.8).range(0, 4).step(0.1).label('Lift'),
|
|
345
|
+
opacity: f.number(0.6).range(0.1, 1).step(0.05).label('Opacity'),
|
|
346
|
+
detail: f.number(300).range(20, 4000).step(20).label('Nodes'),
|
|
347
|
+
reachStep: f.number(600).range(200, 2000).step(100).unit(shownIn('ms')).label('Reach step'),
|
|
348
|
+
width: f.number(2).range(1, 24).step(0.5).unit(shownIn('px')).label('Width'),
|
|
349
|
+
minimapSize: f.number(280).range(120, 600).step(20).label('Minimap size'),
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
/** astv's sidebar sliders paired two to a row, at the narrowest width a two-up sidebar should support:
|
|
353
|
+
* 300px at compact and comfortable. Roomy needs 352px; at 300 its longest labels push readouts out of the row. */
|
|
354
|
+
export const SidebarSliders: Story = {
|
|
355
|
+
render: () => {
|
|
356
|
+
const schema = resolveConfigSchema(sidebarSliders, []);
|
|
357
|
+
const [config, setConfig] = useState<Record<string, unknown>>(sidebarSliders.defaults());
|
|
358
|
+
return (
|
|
359
|
+
<div style={{ width: 300 }}>
|
|
360
|
+
<ControlPanel
|
|
361
|
+
schema={schema}
|
|
362
|
+
config={config}
|
|
363
|
+
setConfig={(path, value) => setConfig((prev) => withValueAtPath(prev, path, value))}
|
|
364
|
+
layout="inline"
|
|
365
|
+
align="center"
|
|
366
|
+
pack="pairs"
|
|
367
|
+
/>
|
|
368
|
+
</div>
|
|
369
|
+
);
|
|
370
|
+
},
|
|
371
|
+
};
|