@weasel-js/labkit 1.5.2 → 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-CMMo180l.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-ZTiwzYyT.d.ts → Select.d-BchGMC2Y.d.ts} +102 -11
- package/dist/_dts/{frac-B4hFJMiN.d.ts → frac-DTRq7Xpb.d.ts} +18 -10
- package/dist/_dts/inDialog-D5ZKGjiD.d.ts +23 -0
- package/dist/_dts/{index-DlKolEIZ.d.ts → index-B0--8PEM.d.ts} +2 -2
- package/dist/_dts/index-DINmCuLA.d.ts +453 -0
- package/dist/_dts/number.d-OeQYQoUO.d.ts +28 -0
- package/dist/_dts/{types-Db-3NNk1.d.ts → types-B7y8hX1D.d.ts} +2 -2
- package/dist/_dts/{usePanZoom-SSTZe410.d.ts → usePanZoom-Bq73pwFe.d.ts} +2 -2
- package/dist/_dts/{useTrialState-pnAE-UBD.d.ts → useTrialState-CP4jfuKl.d.ts} +1 -1
- package/dist/canvas/index.d.ts +6 -5
- 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-753Y6HE6.js → chunk-3FI7Y4R4.js} +4 -4
- package/dist/chunk-3FI7Y4R4.js.map +1 -0
- package/dist/{chunk-REOBGYMC.js → chunk-AWH3SRPW.js} +31 -5
- package/dist/chunk-AWH3SRPW.js.map +1 -0
- package/dist/{chunk-A2ACAGW6.js → chunk-BVL7Y574.js} +22 -5
- package/dist/chunk-BVL7Y574.js.map +1 -0
- package/dist/{chunk-CKUKF72R.js → chunk-H5VIV7MQ.js} +26 -22
- 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-LRI3MBGR.js → chunk-MOLIUNVH.js} +4 -4
- package/dist/{chunk-LRI3MBGR.js.map → chunk-MOLIUNVH.js.map} +1 -1
- package/dist/chunk-USO6TERP.js +90 -0
- package/dist/chunk-USO6TERP.js.map +1 -0
- package/dist/{chunk-EMXAIRUJ.js → chunk-VQ47KWD5.js} +3 -3
- package/dist/{chunk-EMXAIRUJ.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-KJCUERQF.js → chunk-ZORVNOKX.js} +6 -25
- package/dist/chunk-ZORVNOKX.js.map +1 -0
- package/dist/config/index.d.ts +50 -7
- 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 +83 -102
- package/dist/index.js +430 -458
- 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 +407 -204
- 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 +298 -174
- 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/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/types.ts +5 -2
- package/src/config/autoConfig.ts +2 -2
- package/src/config/builder.ts +20 -1
- 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/controls/ControlPanel.less +4 -0
- package/src/controls/ControlPanel.stories.tsx +55 -2
- package/src/controls/ControlPanel.test.tsx +30 -3
- package/src/controls/ControlPanel.tsx +116 -337
- 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.ts +11 -26
- package/src/instrument/types.ts +5 -2
- package/src/lab/Lab.nebula.test.tsx +31 -0
- package/src/lab/Lab.tsx +38 -15
- 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/index.ts +0 -2
- package/src/specimen/Specimen.tsx +42 -56
- package/src/state/types.ts +2 -1
- 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 +1 -1
- package/src/trial/Trial.tsx +30 -8
- package/dist/_dts/index-8LiKe3A-.d.ts +0 -410
- package/dist/chunk-6BUQ77ZV.js +0 -18
- package/dist/chunk-6BUQ77ZV.js.map +0 -1
- package/dist/chunk-753Y6HE6.js.map +0 -1
- package/dist/chunk-A2ACAGW6.js.map +0 -1
- package/dist/chunk-CKUKF72R.js.map +0 -1
- package/dist/chunk-D35PFTS7.js +0 -170
- package/dist/chunk-D35PFTS7.js.map +0 -1
- package/dist/chunk-FKLT3ITC.js +0 -590
- package/dist/chunk-FKLT3ITC.js.map +0 -1
- package/dist/chunk-KJCUERQF.js.map +0 -1
- package/dist/chunk-LASCBWW7.js +0 -13456
- package/dist/chunk-LASCBWW7.js.map +0 -1
- package/dist/chunk-REOBGYMC.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/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
|
@@ -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
|
+
}
|
|
@@ -2,8 +2,12 @@
|
|
|
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;
|
|
7
11
|
}
|
|
8
12
|
|
|
9
13
|
// A cell in a paired row, with the caption naming the knob it holds. Case,
|
|
@@ -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';
|
|
@@ -285,7 +285,9 @@ const inline = f.schema({
|
|
|
285
285
|
lift: f.number(0.2).range(0, 1).step(0.05),
|
|
286
286
|
opacity: f.number(0.45).range(0, 1).step(0.05),
|
|
287
287
|
iconSet: f.enum('small', ['small', 'large']).label('Icon set'),
|
|
288
|
-
drawAsIcons: f
|
|
288
|
+
drawAsIcons: f
|
|
289
|
+
.list(['**/node_modules/**', '**/dist/**', 'package-lock.json'])
|
|
290
|
+
.label('Draw as icons'),
|
|
289
291
|
detail: f.number(1200).range(0, 4000).suffix('nodes'),
|
|
290
292
|
showCameraRoute: f.boolean(false).toggle().label('Show camera route'),
|
|
291
293
|
routeColor: f.enum('sweep', ['sweep', 'depth']).label('Route color'),
|
|
@@ -316,3 +318,54 @@ export const Inline: Story = {
|
|
|
316
318
|
);
|
|
317
319
|
},
|
|
318
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
|
+
};
|
|
@@ -187,7 +187,11 @@ describe('<ControlPanel> schema', () => {
|
|
|
187
187
|
[],
|
|
188
188
|
);
|
|
189
189
|
render(
|
|
190
|
-
<ControlPanel
|
|
190
|
+
<ControlPanel
|
|
191
|
+
schema={schema}
|
|
192
|
+
config={{ showGrid: true, snap: false }}
|
|
193
|
+
setConfig={setConfig}
|
|
194
|
+
/>,
|
|
191
195
|
);
|
|
192
196
|
expect(screen.getByRole('checkbox', { name: 'Show grid' })).toBeInTheDocument();
|
|
193
197
|
const sw = screen.getByRole('switch', { name: 'Snap' });
|
|
@@ -478,7 +482,7 @@ describe('<ControlPanel> format', () => {
|
|
|
478
482
|
setConfig={vi.fn()}
|
|
479
483
|
/>,
|
|
480
484
|
);
|
|
481
|
-
expect(screen.getByDisplayValue('2.
|
|
485
|
+
expect(screen.getByDisplayValue('2.00M')).toBeInTheDocument();
|
|
482
486
|
});
|
|
483
487
|
});
|
|
484
488
|
|
|
@@ -493,7 +497,9 @@ describe('<ControlPanel> group descriptions', () => {
|
|
|
493
497
|
}),
|
|
494
498
|
[],
|
|
495
499
|
);
|
|
496
|
-
render(
|
|
500
|
+
render(
|
|
501
|
+
<ControlPanel schema={schema} config={{ grid: { cellSize: 20 } }} setConfig={vi.fn()} />,
|
|
502
|
+
);
|
|
497
503
|
const note = screen.getByText('How the background lattice is spaced.');
|
|
498
504
|
const field = screen.getByLabelText('Cell size');
|
|
499
505
|
expect(note.compareDocumentPosition(field) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
|
@@ -1064,3 +1070,24 @@ describe('<ControlPanel> color alpha', () => {
|
|
|
1064
1070
|
expect(screen.queryByLabelText('Tint opacity')).toBeNull();
|
|
1065
1071
|
});
|
|
1066
1072
|
});
|
|
1073
|
+
|
|
1074
|
+
describe('<ControlPanel> title, stance and tone', () => {
|
|
1075
|
+
const fields: ConfigField[] = [{ key: 'on', label: 'On', type: 'checkbox', default: true }];
|
|
1076
|
+
|
|
1077
|
+
it('renders the bare list when given none of them', () => {
|
|
1078
|
+
const { container } = render(<ControlPanel fields={fields} config={{ on: true }} setConfig={vi.fn()} />);
|
|
1079
|
+
expect(container.firstElementChild?.classList.contains('lk-control-panel')).toBe(true);
|
|
1080
|
+
expect(container.querySelector('[data-stance], h2')).toBeNull();
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1083
|
+
it('wraps its rows in a panel carrying them when asked', () => {
|
|
1084
|
+
const { container } = render(
|
|
1085
|
+
<ControlPanel title="View" stance="scope" tone={1} fields={fields} config={{ on: true }} setConfig={vi.fn()} />,
|
|
1086
|
+
);
|
|
1087
|
+
const panel = container.firstElementChild as HTMLElement;
|
|
1088
|
+
expect(panel.dataset.stance).toBe('scope');
|
|
1089
|
+
expect(panel.dataset.tone).toBe('1');
|
|
1090
|
+
expect(screen.getByRole('heading', { name: 'View' })).toBeTruthy();
|
|
1091
|
+
expect(panel.querySelector('.lk-control-panel')).not.toBeNull();
|
|
1092
|
+
});
|
|
1093
|
+
});
|