@weasel-js/labkit 1.5.0 → 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/README.md +41 -4
- package/dist/_dts/{CanvasStackContext-CqkxkFJW.d.ts → CanvasStackContext-CMMo180l.d.ts} +1 -1
- package/dist/_dts/{PrefsForm.d-DDHFANkc.d.ts → PrefsForm.d-DPy9I8It.d.ts} +4 -0
- package/dist/_dts/Select.d-ZTiwzYyT.d.ts +479 -0
- package/dist/_dts/{frac-8R6UmlvN.d.ts → frac-B4hFJMiN.d.ts} +54 -6
- package/dist/_dts/{index-retnAut7.d.ts → index-8LiKe3A-.d.ts} +77 -10
- package/dist/_dts/{index-B8uu2ba7.d.ts → index-DlKolEIZ.d.ts} +20 -8
- package/dist/_dts/{types-Ca2LCnPq.d.ts → types-Db-3NNk1.d.ts} +28 -4
- package/dist/_dts/{usePanZoom-nS798rOF.d.ts → usePanZoom-SSTZe410.d.ts} +4 -47
- package/dist/_dts/{useTrialState-jcx_TdU6.d.ts → useTrialState-pnAE-UBD.d.ts} +1 -1
- package/dist/canvas/index.d.ts +5 -5
- package/dist/canvas/index.js +1 -1
- package/dist/chrome/index.d.ts +4 -4
- package/dist/chrome/index.js +7 -7
- package/dist/chunk-6BUQ77ZV.js +18 -0
- package/dist/chunk-6BUQ77ZV.js.map +1 -0
- package/dist/{chunk-KJALCDWE.js → chunk-753Y6HE6.js} +9 -9
- package/dist/chunk-753Y6HE6.js.map +1 -0
- package/dist/{chunk-6ZDGOZQV.js → chunk-A2ACAGW6.js} +72 -20
- package/dist/chunk-A2ACAGW6.js.map +1 -0
- package/dist/{chunk-NS64DXMN.js → chunk-CKUKF72R.js} +44 -21
- package/dist/chunk-CKUKF72R.js.map +1 -0
- package/dist/{chunk-MUKOW3TC.js → chunk-D35PFTS7.js} +4 -4
- package/dist/{chunk-MUKOW3TC.js.map → chunk-D35PFTS7.js.map} +1 -1
- package/dist/{chunk-UU3NJO6N.js → chunk-EMXAIRUJ.js} +3 -3
- package/dist/{chunk-UU3NJO6N.js.map → chunk-EMXAIRUJ.js.map} +1 -1
- package/dist/{chunk-ZPH5WTWF.js → chunk-ERFMC6WU.js} +142 -66
- package/dist/chunk-ERFMC6WU.js.map +1 -0
- package/dist/chunk-FKLT3ITC.js +590 -0
- package/dist/chunk-FKLT3ITC.js.map +1 -0
- package/dist/{chunk-QLSV2N3G.js → chunk-IJJUK4FU.js} +25 -13
- package/dist/chunk-IJJUK4FU.js.map +1 -0
- package/dist/{chunk-EKIICY6X.js → chunk-KJCUERQF.js} +37 -25
- package/dist/chunk-KJCUERQF.js.map +1 -0
- package/dist/chunk-LASCBWW7.js +13456 -0
- package/dist/chunk-LASCBWW7.js.map +1 -0
- package/dist/{chunk-67SJLMC7.js → chunk-LRI3MBGR.js} +45 -12
- package/dist/chunk-LRI3MBGR.js.map +1 -0
- package/dist/chunk-REOBGYMC.js +150 -0
- package/dist/chunk-REOBGYMC.js.map +1 -0
- package/dist/{chunk-PGETSEDK.js → chunk-TT5C5S6R.js} +11 -90
- package/dist/chunk-TT5C5S6R.js.map +1 -0
- package/dist/{chunk-NDRYLVOW.js → chunk-Z76KEZAX.js} +2 -2
- package/dist/chunk-Z76KEZAX.js.map +1 -0
- package/dist/config/index.d.ts +62 -19
- package/dist/config/index.js +6 -4
- package/dist/controls/index.d.ts +3 -3
- package/dist/controls/index.js +4 -4
- package/dist/dragdrop/index.d.ts +3 -3
- package/dist/index.d.ts +230 -62
- package/dist/index.js +1367 -1698
- package/dist/index.js.map +1 -1
- package/dist/layers/index.d.ts +4 -4
- package/dist/layers/index.js +2 -2
- package/dist/loupe/index.d.ts +67 -16
- package/dist/loupe/index.js +1 -1
- package/dist/passthrough/weasel-ui.d.ts +57 -230
- package/dist/passthrough/weasel-ui.js +1 -1
- package/dist/primitives/index.d.ts +3 -1
- package/dist/primitives/index.js +3 -3
- package/dist/state/index.d.ts +5 -5
- package/dist/state/index.js +5 -3
- package/dist/state/index.js.map +1 -1
- package/dist/styles.css +266 -57
- package/dist/surface/index.js +1 -1
- package/dist/ui/layers/index.js +1 -1
- package/dist/undo/index.d.ts +3 -3
- package/package.json +9 -9
- package/src/annotations/AnnotationOverlay.tsx +3 -0
- package/src/annotations/Annotations.less +4 -0
- package/src/annotations/Annotations.overlay.test.tsx +20 -0
- package/src/annotations/svgNodes.ts +1 -0
- package/src/annotations/toolMap.test.ts +9 -5
- package/src/annotations/toolMap.ts +3 -0
- package/src/canvas/CanvasStack.stories.tsx +1 -1
- package/src/canvas/index.ts +0 -9
- package/src/canvas/usePanZoom.test.ts +28 -0
- package/src/canvas/usePanZoom.ts +11 -4
- package/src/chrome/ChromeRegions.stories.tsx +1 -1
- package/src/chrome/LabChrome.tsx +21 -5
- package/src/chrome/builtins.test.ts +1 -0
- package/src/chrome/builtins.tsx +1 -0
- package/src/chrome/index.ts +1 -0
- package/src/chrome/labTypes.ts +3 -3
- package/src/chrome/regions/PaletteRegion.test.tsx +18 -0
- package/src/chrome/regions/PaletteRegion.tsx +6 -10
- package/src/chrome/regions/SidebarRegion.stories.tsx +44 -0
- package/src/chrome/regions/TitleBarRegion.tsx +1 -1
- package/src/chrome/regions/ToolbarRegion.tsx +11 -7
- package/src/chrome/types.ts +11 -2
- package/src/config/auto.test.ts +12 -0
- package/src/config/auto.ts +15 -0
- package/src/config/autoConfig.test.ts +107 -0
- package/src/config/autoConfig.ts +153 -0
- package/src/config/builder.test.ts +38 -0
- package/src/config/builder.ts +55 -11
- package/src/config/index.ts +3 -0
- package/src/config/path.ts +7 -2
- package/src/config/resolve.test.ts +25 -0
- package/src/config/resolve.ts +17 -1
- package/src/config/types.ts +32 -2
- package/src/config/useResolvedConfig.ts +26 -0
- package/src/controls/AutoControls.stories.tsx +88 -0
- package/src/controls/ControlPanel.less +23 -0
- package/src/controls/ControlPanel.stories.tsx +83 -1
- package/src/controls/ControlPanel.test.tsx +457 -4
- package/src/controls/ControlPanel.tsx +534 -73
- package/src/index.test.ts +0 -2
- package/src/index.ts +33 -11
- package/src/instrument/serializers.ts +29 -1
- package/src/lab/Lab.chrome.test.tsx +16 -0
- package/src/lab/Lab.stories.tsx +1 -1
- package/src/lab/Lab.surface.test.tsx +7 -2
- package/src/lab/Lab.test.tsx +79 -1
- package/src/lab/Lab.tsx +124 -16
- package/src/lab/LabContext.ts +8 -0
- package/src/lab/LabFit.stories.tsx +4 -4
- package/src/lab/LabFullChrome.stories.tsx +1 -1
- package/src/lab/LabHeader.tsx +0 -1
- package/src/lab/LabRoot.test.tsx +56 -0
- package/src/lab/LabRoot.tsx +47 -0
- package/src/lab/LabShell.less +24 -3
- package/src/lab/LabShell.stories.tsx +50 -1
- package/src/lab/LabShell.tsx +18 -27
- package/src/lab/LabSwitcher.less +10 -10
- package/src/lab/LabSwitcher.test.tsx +14 -9
- package/src/lab/LabSwitcher.tsx +8 -4
- package/src/lab/Workspace.less +1 -1
- package/src/lab/Workspace.stories.tsx +1 -1
- package/src/lab/Workspace.surface.test.tsx +1 -1
- package/src/lab/index.ts +2 -0
- package/src/layers/LayerList.less +1 -1
- package/src/layers/LayerList.stories.tsx +2 -2
- package/src/loupe/AGENTS.md +16 -4
- package/src/loupe/LoupeGestures.tsx +67 -0
- package/src/loupe/TrialLoupe.tsx +43 -28
- package/src/loupe/index.ts +4 -0
- package/src/loupe/loupeActions.ts +98 -0
- package/src/loupe/useLoupe.test.tsx +59 -2
- package/src/loupe/useLoupe.ts +28 -48
- package/src/passthrough/weasel-ui.ts +3 -3
- package/src/primitives/FloatingPanel.stories.tsx +1 -1
- package/src/primitives/FpsMeter.less +6 -1
- package/src/primitives/FpsMeter.stories.tsx +1 -1
- package/src/primitives/FpsMeter.test.tsx +12 -4
- package/src/primitives/FpsMeter.tsx +6 -1
- package/src/primitives/JobProgress.stories.tsx +2 -2
- package/src/primitives/Legend.stories.tsx +1 -1
- package/src/primitives/ScaleIndicator.stories.tsx +1 -1
- package/src/primitives/Sidebar.less +7 -0
- package/src/primitives/Sidebar.stories.tsx +1 -1
- package/src/primitives/Split.test.tsx +25 -0
- package/src/primitives/Split.tsx +36 -20
- package/src/primitives/StatusBar.less +6 -0
- package/src/primitives/StatusBar.stories.tsx +1 -1
- package/src/primitives/Toolbar.stories.tsx +1 -1
- package/src/specimen/Specimen.less +95 -0
- package/src/specimen/Specimen.stories.tsx +13 -0
- package/src/specimen/Specimen.test.tsx +13 -0
- package/src/specimen/Specimen.tsx +888 -0
- package/src/state/Persistence.stories.tsx +4 -4
- package/src/state/store.test.ts +65 -0
- package/src/state/store.ts +32 -8
- package/src/state/types.ts +31 -2
- package/src/state/useTrialState.test.tsx +85 -0
- package/src/state/useTrialState.ts +9 -1
- package/src/state/view.test.ts +4 -4
- package/src/styles.less +1 -0
- package/src/surface/index.ts +1 -1
- package/src/surface/useSurfaceTile.test.tsx +1 -1
- package/src/surface/useTiledSurface.test.tsx +65 -10
- package/src/surface/useTiledSurface.ts +23 -11
- package/src/theme/Interstellar.stories.tsx +2 -2
- package/src/theme/interstellar.test.ts +16 -5
- package/src/theme/interstellar.theme.json +180 -0
- package/src/theme/interstellar.ts +8 -5
- package/src/tools/labTool.ts +3 -3
- package/src/trial/Trial.annotations.test.tsx +13 -4
- package/src/trial/Trial.canvas.test.tsx +36 -0
- package/src/trial/Trial.config.test.tsx +75 -2
- package/src/trial/Trial.less +4 -0
- package/src/trial/Trial.stories.tsx +4 -1
- package/src/trial/Trial.test.tsx +3 -0
- package/src/trial/Trial.tsx +21 -14
- package/src/trial/TrialChrome.tsx +17 -3
- package/src/trial/trialOps.test.ts +101 -1
- package/src/trial/trialOps.ts +62 -4
- package/dist/_dts/ToggleBar.d-DbHoWKIA.d.ts +0 -116
- package/dist/chunk-67SJLMC7.js.map +0 -1
- package/dist/chunk-6ZDGOZQV.js.map +0 -1
- package/dist/chunk-E44SU6XS.js +0 -50
- package/dist/chunk-E44SU6XS.js.map +0 -1
- package/dist/chunk-EKIICY6X.js.map +0 -1
- package/dist/chunk-KJALCDWE.js.map +0 -1
- package/dist/chunk-NDRYLVOW.js.map +0 -1
- package/dist/chunk-NS64DXMN.js.map +0 -1
- package/dist/chunk-PGETSEDK.js.map +0 -1
- package/dist/chunk-QLSV2N3G.js.map +0 -1
- package/dist/chunk-S2SHZRD7.js +0 -7681
- package/dist/chunk-S2SHZRD7.js.map +0 -1
- package/dist/chunk-TQBBYTAF.js +0 -280
- package/dist/chunk-TQBBYTAF.js.map +0 -1
- package/dist/chunk-ZPH5WTWF.js.map +0 -1
- package/src/canvas/useOrbit.test.ts +0 -167
- package/src/canvas/useOrbit.ts +0 -156
- package/src/theme/interstellar.tokens.json +0 -61
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { auto } from './auto';
|
|
3
|
+
import { autoPathsOf, resolveAutoConfig } from './autoConfig';
|
|
4
|
+
import { f } from './builder';
|
|
5
|
+
import { resolveConfigSchema } from './resolve';
|
|
6
|
+
|
|
7
|
+
const schema = f.schema({
|
|
8
|
+
width: f.number(432),
|
|
9
|
+
gap: f.number(12).auto((c) => (c.width as number) / 24),
|
|
10
|
+
cols: f.number(3).initial(auto),
|
|
11
|
+
grid: f.group({ size: f.number(8).auto(() => 99) }),
|
|
12
|
+
});
|
|
13
|
+
const resolved = resolveConfigSchema(schema);
|
|
14
|
+
const raw = { width: 432, gap: 12, cols: 3, grid: { size: 8 } };
|
|
15
|
+
|
|
16
|
+
describe('autoPathsOf', () => {
|
|
17
|
+
it('lists the dotted paths a schema starts auto', () => {
|
|
18
|
+
expect(autoPathsOf(resolved)).toEqual(['cols']);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('reaches leaves nested under a group', () => {
|
|
22
|
+
const nested = resolveConfigSchema(
|
|
23
|
+
f.schema({ grid: f.group({ size: f.number(8).initial(auto) }) }),
|
|
24
|
+
);
|
|
25
|
+
expect(autoPathsOf(nested)).toEqual(['grid.size']);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('resolveAutoConfig', () => {
|
|
30
|
+
it('leaves a config with nothing auto exactly as it was', () => {
|
|
31
|
+
expect(resolveAutoConfig(resolved, raw, new Set())).toEqual(raw);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('replaces an auto path that has a resolver with the computed value', () => {
|
|
35
|
+
const out = resolveAutoConfig(resolved, raw, new Set(['gap']));
|
|
36
|
+
expect(out.gap).toBe(18);
|
|
37
|
+
expect(out.width).toBe(432);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('deletes an auto path that has no resolver', () => {
|
|
41
|
+
const out = resolveAutoConfig(resolved, raw, new Set(['cols'])) as Record<string, unknown>;
|
|
42
|
+
expect('cols' in out).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('resolves a leaf nested under a group', () => {
|
|
46
|
+
const out = resolveAutoConfig(resolved, raw, new Set(['grid.size'])) as {
|
|
47
|
+
grid: { size: number };
|
|
48
|
+
};
|
|
49
|
+
expect(out.grid.size).toBe(99);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('lets one resolver read another auto path, already resolved', () => {
|
|
53
|
+
const chained = resolveConfigSchema(
|
|
54
|
+
f.schema({
|
|
55
|
+
a: f.number(1).auto(() => 10),
|
|
56
|
+
b: f.number(2).auto((c) => (c.a as number) * 3),
|
|
57
|
+
}),
|
|
58
|
+
);
|
|
59
|
+
const out = resolveAutoConfig(chained, { a: 1, b: 2 }, new Set(['a', 'b'])) as {
|
|
60
|
+
a: number;
|
|
61
|
+
b: number;
|
|
62
|
+
};
|
|
63
|
+
expect(out).toEqual({ a: 10, b: 30 });
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('resolves a dependency declared after its dependent', () => {
|
|
67
|
+
const backward = resolveConfigSchema(
|
|
68
|
+
f.schema({
|
|
69
|
+
b: f.number(2).auto((c) => (c.a as number) * 3),
|
|
70
|
+
a: f.number(1).auto(() => 10),
|
|
71
|
+
}),
|
|
72
|
+
);
|
|
73
|
+
const out = resolveAutoConfig(backward, { a: 1, b: 2 }, new Set(['a', 'b'])) as {
|
|
74
|
+
a: number;
|
|
75
|
+
b: number;
|
|
76
|
+
};
|
|
77
|
+
expect(out).toEqual({ a: 10, b: 30 });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('throws naming the path when resolvers cycle', () => {
|
|
81
|
+
const cyclic = resolveConfigSchema(
|
|
82
|
+
f.schema({
|
|
83
|
+
a: f.number(1).auto((c) => (c.b as number) + 1),
|
|
84
|
+
b: f.number(2).auto((c) => (c.a as number) + 1),
|
|
85
|
+
}),
|
|
86
|
+
);
|
|
87
|
+
expect(() => resolveAutoConfig(cyclic, { a: 1, b: 2 }, new Set(['a', 'b']))).toThrow(/cycle/i);
|
|
88
|
+
expect(() => resolveAutoConfig(cyclic, { a: 1, b: 2 }, new Set(['a', 'b']))).toThrow(/"a"/);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('throws on a resolver that reads its own path', () => {
|
|
92
|
+
const selfish = resolveConfigSchema(
|
|
93
|
+
f.schema({ a: f.number(1).auto((c) => (c.a as number) + 1) }),
|
|
94
|
+
);
|
|
95
|
+
expect(() => resolveAutoConfig(selfish, { a: 1 }, new Set(['a']))).toThrow(/cycle/i);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('ignores an auto path the schema does not have', () => {
|
|
99
|
+
expect(resolveAutoConfig(resolved, raw, new Set(['gone']))).toEqual(raw);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('does not mutate the config it was given', () => {
|
|
103
|
+
const before = structuredClone(raw);
|
|
104
|
+
resolveAutoConfig(resolved, raw, new Set(['gap', 'grid.size']));
|
|
105
|
+
expect(raw).toEqual(before);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { isPrefLeaf, type PrefGroup, type PrefLeaf } from '@weasel-js/ui';
|
|
2
|
+
import { isAuto } from './auto';
|
|
3
|
+
import { isRecord, withValueAtPath } from './path';
|
|
4
|
+
import type { ResolvedConfig } from './types';
|
|
5
|
+
|
|
6
|
+
type Resolver = (config: Record<string, unknown>) => unknown;
|
|
7
|
+
|
|
8
|
+
/** Reads a labkit-only extra off a resolved leaf. `PrefLeaf` has no field for
|
|
9
|
+
* these; they ride as extra keys and survive the resolve walk. */
|
|
10
|
+
function extra<T>(leaf: PrefLeaf, key: string): T | undefined {
|
|
11
|
+
return (leaf as unknown as Record<string, T | undefined>)[key];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Every leaf in a resolved schema, in schema order, with its dotted path. */
|
|
15
|
+
function* leaves(group: PrefGroup, at = ''): Generator<[string, PrefLeaf]> {
|
|
16
|
+
for (const [key, child] of Object.entries(group.children)) {
|
|
17
|
+
const path = at === '' ? key : `${at}.${key}`;
|
|
18
|
+
if (isPrefLeaf(child)) yield [path, child];
|
|
19
|
+
else yield* leaves(child, path);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The dotted paths a schema declares as starting auto — every leaf that said
|
|
24
|
+
* `.initial(auto)`. This is what seeds a new trial's unpinned set. */
|
|
25
|
+
export function autoPathsOf(resolved: ResolvedConfig): string[] {
|
|
26
|
+
const out: string[] = [];
|
|
27
|
+
for (const [path, leaf] of leaves(resolved.group)) {
|
|
28
|
+
if (extra<boolean>(leaf, 'unpinned')) out.push(path);
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The config an instrument reads: the stored one, with every unpinned path
|
|
35
|
+
* either computed by its resolver or removed.
|
|
36
|
+
*
|
|
37
|
+
* Resolution is demand-driven — a resolver reading another unpinned path
|
|
38
|
+
* forces that one first, whatever order the schema declared them in — so a
|
|
39
|
+
* cycle is a real cycle and not an ordering accident.
|
|
40
|
+
*/
|
|
41
|
+
export function resolveAutoConfig<TC>(
|
|
42
|
+
resolved: ResolvedConfig,
|
|
43
|
+
config: TC,
|
|
44
|
+
autoPaths: ReadonlySet<string>,
|
|
45
|
+
): TC {
|
|
46
|
+
if (autoPaths.size === 0) return config;
|
|
47
|
+
|
|
48
|
+
const resolvers = new Map<string, Resolver | undefined>();
|
|
49
|
+
for (const [path, leaf] of leaves(resolved.group)) {
|
|
50
|
+
if (autoPaths.has(path)) resolvers.set(path, extra<Resolver>(leaf, 'autoResolve'));
|
|
51
|
+
}
|
|
52
|
+
if (resolvers.size === 0) return config;
|
|
53
|
+
|
|
54
|
+
// Drop first, so a resolver reading a still-unresolved auto path sees
|
|
55
|
+
// `undefined` rather than a stale pinned value it would silently believe.
|
|
56
|
+
let out = config as unknown as Record<string, unknown>;
|
|
57
|
+
for (const path of resolvers.keys()) out = dropAtPath(out, path);
|
|
58
|
+
|
|
59
|
+
const inFlight = new Set<string>();
|
|
60
|
+
const done = new Set<string>();
|
|
61
|
+
|
|
62
|
+
const need = (path: string): void => {
|
|
63
|
+
if (done.has(path)) return;
|
|
64
|
+
if (inFlight.has(path)) {
|
|
65
|
+
throw new Error(`[labkit] auto resolver cycle at "${path}"`);
|
|
66
|
+
}
|
|
67
|
+
const resolve = resolvers.get(path);
|
|
68
|
+
if (!resolve) {
|
|
69
|
+
done.add(path);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
inFlight.add(path);
|
|
73
|
+
try {
|
|
74
|
+
// Read `out` only after the resolver has run: it forces its own
|
|
75
|
+
// dependencies, and each of those replaced `out`.
|
|
76
|
+
const value = resolve(demand(() => out, resolvers, need));
|
|
77
|
+
out = withValueAtPath(out, path, value);
|
|
78
|
+
} finally {
|
|
79
|
+
inFlight.delete(path);
|
|
80
|
+
}
|
|
81
|
+
done.add(path);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
for (const path of resolvers.keys()) need(path);
|
|
85
|
+
return out as unknown as TC;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The config a resolver reads. A top-level key that is itself unpinned is
|
|
90
|
+
* forced before it is handed back, which is what makes declaration order
|
|
91
|
+
* irrelevant and turns a genuine loop into a named error.
|
|
92
|
+
*
|
|
93
|
+
* Only top-level keys are trapped: a resolver reaching `c.grid.size` gets the
|
|
94
|
+
* already-dropped branch, and a nested auto path it depends on resolves by
|
|
95
|
+
* schema order alone.
|
|
96
|
+
*/
|
|
97
|
+
function demand(
|
|
98
|
+
read: () => Record<string, unknown>,
|
|
99
|
+
resolvers: ReadonlyMap<string, Resolver | undefined>,
|
|
100
|
+
need: (path: string) => void,
|
|
101
|
+
): Record<string, unknown> {
|
|
102
|
+
return new Proxy(read(), {
|
|
103
|
+
get(_target, key) {
|
|
104
|
+
if (typeof key === 'string' && resolvers.has(key)) need(key);
|
|
105
|
+
return Reflect.get(read(), key);
|
|
106
|
+
},
|
|
107
|
+
has(_target, key) {
|
|
108
|
+
return Reflect.has(read(), key);
|
|
109
|
+
},
|
|
110
|
+
ownKeys() {
|
|
111
|
+
return Reflect.ownKeys(read());
|
|
112
|
+
},
|
|
113
|
+
getOwnPropertyDescriptor(_target, key) {
|
|
114
|
+
return Reflect.getOwnPropertyDescriptor(read(), key);
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** A copy of `config` with the dotted path removed, copying every record on
|
|
120
|
+
* the way down. A path that is not there comes back unchanged. */
|
|
121
|
+
function dropAtPath(config: Record<string, unknown>, path: string): Record<string, unknown> {
|
|
122
|
+
const [head, ...rest] = path.split('.');
|
|
123
|
+
if (head === undefined || !(head in config)) return config;
|
|
124
|
+
if (rest.length === 0) {
|
|
125
|
+
const { [head]: _dropped, ...kept } = config;
|
|
126
|
+
return kept;
|
|
127
|
+
}
|
|
128
|
+
const child = config[head];
|
|
129
|
+
if (!isRecord(child)) return config;
|
|
130
|
+
return { ...config, [head]: dropAtPath(child, rest.join('.')) };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* What writing `value` at `path` makes of a trial's raw config and its set of
|
|
135
|
+
* unpinned paths. `auto` pins nothing and joins the set; any other value is
|
|
136
|
+
* written and leaves it. Both the store and the chrome go through this, so
|
|
137
|
+
* they cannot disagree about what a write meant.
|
|
138
|
+
*/
|
|
139
|
+
export function applyConfigWrite<TC>(
|
|
140
|
+
config: TC,
|
|
141
|
+
autoPaths: readonly string[],
|
|
142
|
+
path: string,
|
|
143
|
+
value: unknown,
|
|
144
|
+
): { config: TC; autoPaths: readonly string[] } {
|
|
145
|
+
if (isAuto(value)) {
|
|
146
|
+
if (autoPaths.includes(path)) return { config, autoPaths };
|
|
147
|
+
return { config, autoPaths: [...autoPaths, path] };
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
config: withValueAtPath(config, path, value),
|
|
151
|
+
autoPaths: autoPaths.includes(path) ? autoPaths.filter((p) => p !== path) : autoPaths,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { auto } from './auto';
|
|
2
3
|
import { f } from './builder';
|
|
3
4
|
import type { ConfigOf, ConfigPath, ValueAtPath } from './types';
|
|
4
5
|
|
|
@@ -154,3 +155,40 @@ describe('builder / nested types', () => {
|
|
|
154
155
|
expect([size, path, value]).toEqual([20, 'grid.size', 40]);
|
|
155
156
|
});
|
|
156
157
|
});
|
|
158
|
+
|
|
159
|
+
describe('auto on a node', () => {
|
|
160
|
+
it('attaches a resolver without changing the default', () => {
|
|
161
|
+
const n = f.number(12).auto((c) => (c.width as number) / 24);
|
|
162
|
+
expect(n.default).toBe(12);
|
|
163
|
+
expect(n.options.autoResolve?.({ width: 432 })).toBe(18);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('.initial(auto) marks the node as starting unpinned', () => {
|
|
167
|
+
expect(f.number(3).initial(auto).options.unpinned).toBe(true);
|
|
168
|
+
expect(f.number(3).options.unpinned).toBeUndefined();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('.manual() opts the node out of ever being auto', () => {
|
|
172
|
+
expect(f.number(1).manual().options.manual).toBe(true);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('clones rather than mutating, like every other chaining method', () => {
|
|
176
|
+
const base = f.number(3);
|
|
177
|
+
const unpinned = base.initial(auto);
|
|
178
|
+
expect(base.options.unpinned).toBeUndefined();
|
|
179
|
+
expect(unpinned).not.toBe(base);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('keeps the subclass, so kind-specific methods still chain after', () => {
|
|
183
|
+
const n = f.number(3).initial(auto).range(0, 10).suffix('px');
|
|
184
|
+
expect(n.annotations.min).toBe(0);
|
|
185
|
+
expect(n.annotations.suffix).toBe('px');
|
|
186
|
+
expect(n.options.unpinned).toBe(true);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('rejects anything but the sentinel, for a caller TypeScript is not checking', () => {
|
|
190
|
+
const n = f.number(3) as unknown as { initial: (value: unknown) => unknown };
|
|
191
|
+
expect(() => n.initial(5)).toThrow(/takes `auto`/);
|
|
192
|
+
expect(() => n.initial(Symbol('auto'))).toThrow(/takes `auto`/);
|
|
193
|
+
});
|
|
194
|
+
});
|
package/src/config/builder.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { PrefLeaf, PrefNumberFormat } from '@weasel-js/ui';
|
|
1
|
+
import type { PrefLeaf, PrefNumberFormat, PrefNumberUnit } from '@weasel-js/ui';
|
|
2
|
+
import { type Auto, isAuto } from './auto';
|
|
2
3
|
import type {
|
|
3
4
|
Annotations,
|
|
4
5
|
BranchAnnotations,
|
|
@@ -12,6 +13,7 @@ import type {
|
|
|
12
13
|
ControlRenderer,
|
|
13
14
|
InferConfig,
|
|
14
15
|
NodeOptions,
|
|
16
|
+
SectionOption,
|
|
15
17
|
} from './types';
|
|
16
18
|
|
|
17
19
|
/** Whether a schema entry is a branch rather than a leaf. */
|
|
@@ -72,8 +74,9 @@ export abstract class BaseNode<T> implements ConfigNode<T> {
|
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
/** Render under a named section heading. `collapsed` opens the section
|
|
75
|
-
* folded — say
|
|
76
|
-
section
|
|
77
|
+
* folded, and `layout` and `pack` lay out its rows — say each on any one of
|
|
78
|
+
* the section's leaves. */
|
|
79
|
+
section(label: string, opts: Omit<SectionOption, 'label'> = {}): this {
|
|
77
80
|
return this.opt({ section: { label, ...opts } });
|
|
78
81
|
}
|
|
79
82
|
|
|
@@ -87,6 +90,30 @@ export abstract class BaseNode<T> implements ConfigNode<T> {
|
|
|
87
90
|
render(renderer: ControlRenderer): this {
|
|
88
91
|
return this.opt({ render: renderer });
|
|
89
92
|
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Compute this leaf's value while it is auto, instead of leaving it
|
|
96
|
+
* `undefined`. The resolver is given the config with every other auto path
|
|
97
|
+
* already resolved.
|
|
98
|
+
*/
|
|
99
|
+
auto(resolve: (config: Record<string, unknown>) => T): this {
|
|
100
|
+
return this.opt({ autoResolve: resolve as (c: Record<string, unknown>) => unknown });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Start this leaf auto rather than pinned at its default. Takes `auto` and
|
|
105
|
+
* nothing else — the constructor argument already declares the value, and a
|
|
106
|
+
* second way to say it would fight with the first.
|
|
107
|
+
*/
|
|
108
|
+
initial(value: Auto): this {
|
|
109
|
+
if (!isAuto(value)) throw new Error('[labkit] .initial() takes `auto` and nothing else');
|
|
110
|
+
return this.opt({ unpinned: true });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Never auto. The row's label does not toggle. */
|
|
114
|
+
manual(): this {
|
|
115
|
+
return this.opt({ manual: true });
|
|
116
|
+
}
|
|
90
117
|
}
|
|
91
118
|
|
|
92
119
|
export class NumberNode extends BaseNode<number> {
|
|
@@ -113,6 +140,16 @@ export class NumberNode extends BaseNode<number> {
|
|
|
113
140
|
return this.ann({ format });
|
|
114
141
|
}
|
|
115
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Store the value in one unit and edit it in another — radians stored,
|
|
145
|
+
* degrees typed. `min`, `max` and `step` are declared in the stored unit
|
|
146
|
+
* alongside the value, and convert with it. `prefUnit` builds one from a
|
|
147
|
+
* `UnitSystem`.
|
|
148
|
+
*/
|
|
149
|
+
unit(unit: PrefNumberUnit): this {
|
|
150
|
+
return this.ann({ unit });
|
|
151
|
+
}
|
|
152
|
+
|
|
116
153
|
/** Force a slider even without both bounds. */
|
|
117
154
|
slider(): this {
|
|
118
155
|
return this.ann({ control: 'slider' });
|
|
@@ -127,8 +164,7 @@ export class NumberNode extends BaseNode<number> {
|
|
|
127
164
|
export class BooleanNode extends BaseNode<boolean> {
|
|
128
165
|
readonly kind = 'boolean';
|
|
129
166
|
|
|
130
|
-
/** Ask for a switch
|
|
131
|
-
* `PrefsForm` honors the distinction. */
|
|
167
|
+
/** Ask for a switch rather than a checkbox. */
|
|
132
168
|
toggle(): this {
|
|
133
169
|
return this.ann({ control: 'switch' });
|
|
134
170
|
}
|
|
@@ -153,6 +189,12 @@ export class StringNode extends BaseNode<string> {
|
|
|
153
189
|
|
|
154
190
|
export class ColorNode extends BaseNode<string> {
|
|
155
191
|
readonly kind = 'color';
|
|
192
|
+
|
|
193
|
+
/** The value carries alpha as `#rrggbbaa`, and the row gets an opacity
|
|
194
|
+
* track beside the swatch. */
|
|
195
|
+
alpha(): this {
|
|
196
|
+
return this.ann({ alpha: true });
|
|
197
|
+
}
|
|
156
198
|
}
|
|
157
199
|
|
|
158
200
|
export class EnumNode<T extends string> extends BaseNode<T> {
|
|
@@ -212,15 +254,14 @@ export class GroupNode<S extends ConfigShape> implements ConfigBranch<S> {
|
|
|
212
254
|
return this.with({ ...this.annotations, name }, this.options);
|
|
213
255
|
}
|
|
214
256
|
|
|
215
|
-
/** Longer help text. Carried on the resolved `PrefGroup
|
|
216
|
-
* `PrefsForm`
|
|
217
|
-
* 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. */
|
|
218
259
|
describe(description: string): GroupNode<S> {
|
|
219
260
|
return this.with({ ...this.annotations, description }, this.options);
|
|
220
261
|
}
|
|
221
262
|
|
|
222
263
|
/** Render this whole group under a named section heading. */
|
|
223
|
-
section(label: string, opts:
|
|
264
|
+
section(label: string, opts: Omit<SectionOption, 'label'> = {}): GroupNode<S> {
|
|
224
265
|
return this.with(this.annotations, { ...this.options, section: { label, ...opts } });
|
|
225
266
|
}
|
|
226
267
|
|
|
@@ -282,8 +323,11 @@ export const f = {
|
|
|
282
323
|
group: <const S extends ConfigShape>(children: S): GroupNode<S> => new GroupNode(children),
|
|
283
324
|
|
|
284
325
|
/** A leaf of a kind a lab supplies the control for, through `controls`. */
|
|
285
|
-
custom: <T>(
|
|
286
|
-
|
|
326
|
+
custom: <T>(
|
|
327
|
+
kind: string,
|
|
328
|
+
def: T,
|
|
329
|
+
validate?: (leaf: PrefLeaf, config: Record<string, unknown>) => string[],
|
|
330
|
+
): CustomNode<T> => new CustomNode(kind, def, {}, validate ? { validate } : {}),
|
|
287
331
|
|
|
288
332
|
/** Collect leaves and groups into an instrument's config. */
|
|
289
333
|
schema<S extends ConfigShape>(nodes: S): ConfigSchema<InferConfig<S>> {
|
package/src/config/index.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export { type Auto, auto, isAuto } from './auto';
|
|
2
|
+
export { autoPathsOf, resolveAutoConfig } from './autoConfig';
|
|
1
3
|
export {
|
|
2
4
|
BaseNode,
|
|
3
5
|
BooleanNode,
|
|
@@ -46,4 +48,5 @@ export type {
|
|
|
46
48
|
ValueAtPath,
|
|
47
49
|
} from './types';
|
|
48
50
|
export { useConfigSchema } from './useConfigSchema';
|
|
51
|
+
export { useResolvedConfig } from './useResolvedConfig';
|
|
49
52
|
export { isLeafVisible } from './visible';
|
package/src/config/path.ts
CHANGED
|
@@ -6,8 +6,13 @@ import type { PrefGroup, PrefLeaf } from '@weasel-js/ui';
|
|
|
6
6
|
* reached by exactly the same call.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
/** A plain object a config tree can be walked into. An array, a `Date`, a
|
|
10
|
+
* `Map` or a class instance is a leaf value: a walk that descended into one
|
|
11
|
+
* would rebuild it as a bare `{}`. */
|
|
12
|
+
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
13
|
+
if (typeof value !== 'object' || value === null) return false;
|
|
14
|
+
const proto = Object.getPrototypeOf(value);
|
|
15
|
+
return proto === Object.prototype || proto === null;
|
|
11
16
|
}
|
|
12
17
|
|
|
13
18
|
/** The value at a dotted path. `undefined` when a segment is missing or the
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { PrefGroup } from '@weasel-js/ui';
|
|
2
2
|
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { auto } from './auto';
|
|
3
4
|
import { f } from './builder';
|
|
4
5
|
import { resolveConfigSchema } from './resolve';
|
|
5
6
|
import { titleCase } from './rules';
|
|
@@ -207,4 +208,28 @@ describe('resolveConfigSchema / nested groups', () => {
|
|
|
207
208
|
);
|
|
208
209
|
expect(r.sections).toEqual([{ at: '', label: 'Advanced', paths: ['grid'] }]);
|
|
209
210
|
});
|
|
211
|
+
|
|
212
|
+
it('rejects a leaf that is both manual and starts auto', () => {
|
|
213
|
+
const schema = f.schema({ seed: f.number(1).manual().initial(auto) });
|
|
214
|
+
expect(() => resolveConfigSchema(schema, [])).toThrow(/seed/);
|
|
215
|
+
expect(() => resolveConfigSchema(schema, [])).toThrow(/manual/);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('carries autoResolve, unpinned and manual onto the resolved leaf', () => {
|
|
219
|
+
const r = resolveConfigSchema(
|
|
220
|
+
f.schema({
|
|
221
|
+
gap: f.number(12).auto(() => 18),
|
|
222
|
+
cols: f.number(3).initial(auto),
|
|
223
|
+
seed: f.number(1).manual(),
|
|
224
|
+
plain: f.number(0),
|
|
225
|
+
}),
|
|
226
|
+
[],
|
|
227
|
+
);
|
|
228
|
+
expect(typeof leafAt(r, 'gap').autoResolve).toBe('function');
|
|
229
|
+
expect(leafAt(r, 'cols').unpinned).toBe(true);
|
|
230
|
+
expect(leafAt(r, 'seed').manual).toBe(true);
|
|
231
|
+
expect(leafAt(r, 'plain').autoResolve).toBeUndefined();
|
|
232
|
+
expect(leafAt(r, 'plain').unpinned).toBeUndefined();
|
|
233
|
+
expect(leafAt(r, 'plain').manual).toBeUndefined();
|
|
234
|
+
});
|
|
210
235
|
});
|
package/src/config/resolve.ts
CHANGED
|
@@ -76,6 +76,8 @@ function resolveShape(shape: ConfigShape, at: string, name: string, sink: Sink):
|
|
|
76
76
|
if (section.collapsed !== undefined) {
|
|
77
77
|
spec.collapsed = (spec.collapsed ?? false) || section.collapsed;
|
|
78
78
|
}
|
|
79
|
+
if (section.layout !== undefined && spec.layout === undefined) spec.layout = section.layout;
|
|
80
|
+
if (section.pack !== undefined && spec.pack === undefined) spec.pack = section.pack;
|
|
79
81
|
}
|
|
80
82
|
if (predicate) sink.showIf.set(path, predicate);
|
|
81
83
|
if (!isConfigBranch(entry) && entry.options.render) sink.renderers[path] = entry.options.render;
|
|
@@ -101,10 +103,24 @@ function resolveEntry(
|
|
|
101
103
|
? group
|
|
102
104
|
: { ...group, description: entry.annotations.description };
|
|
103
105
|
}
|
|
106
|
+
if (entry.options.manual && entry.options.unpinned) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
`[labkit] "${path}" is both .manual() and .initial(auto) — a field that can never be auto cannot start auto`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
104
111
|
const seed: LeafPatch = defined({
|
|
105
112
|
...entry.annotations,
|
|
106
113
|
...(entry.kind === null ? {} : { kind: entry.kind }),
|
|
107
114
|
});
|
|
108
115
|
const patch = applyRules(seed, { key, path, default: entry.default }, sink.chain);
|
|
109
|
-
|
|
116
|
+
const { autoResolve, unpinned, manual } = entry.options;
|
|
117
|
+
return {
|
|
118
|
+
...patch,
|
|
119
|
+
default: entry.default,
|
|
120
|
+
// Extra keys survive onto the leaf at runtime; `ControlPanel` reads them
|
|
121
|
+
// back with its `extra<T>` helper, the same as `min`, `step` and the rest.
|
|
122
|
+
...(autoResolve === undefined ? {} : { autoResolve }),
|
|
123
|
+
...(unpinned === undefined ? {} : { unpinned }),
|
|
124
|
+
...(manual === undefined ? {} : { manual }),
|
|
125
|
+
} as PrefLeaf;
|
|
110
126
|
}
|
package/src/config/types.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
PrefGroup,
|
|
3
|
+
PrefLeaf,
|
|
4
|
+
PrefNumberFormat,
|
|
5
|
+
PrefNumberUnit,
|
|
6
|
+
PrefRenderer,
|
|
7
|
+
} from '@weasel-js/ui';
|
|
2
8
|
|
|
3
9
|
/**
|
|
4
10
|
* Renders the control cell for one config leaf. Identical to weasel-ui's
|
|
@@ -27,6 +33,11 @@ export interface Annotations {
|
|
|
27
33
|
suffix?: string;
|
|
28
34
|
/** How a number's value is shown. `compact` abbreviates from a thousand up. */
|
|
29
35
|
format?: PrefNumberFormat;
|
|
36
|
+
/** Display-unit conversion for a number stored in a canonical unit — the
|
|
37
|
+
* value, its bounds and its step all convert at the control's edge. */
|
|
38
|
+
unit?: PrefNumberUnit;
|
|
39
|
+
/** A color leaf's value carries alpha (`#rrggbbaa`) and its row edits it. */
|
|
40
|
+
alpha?: boolean;
|
|
30
41
|
control?: string;
|
|
31
42
|
options?: readonly ConfigOption[];
|
|
32
43
|
placeholder?: string;
|
|
@@ -76,6 +87,10 @@ export interface SectionSpec {
|
|
|
76
87
|
* whether or not it was given a panel-wide `collapse`; a fold the reader
|
|
77
88
|
* has since toggled outranks it. */
|
|
78
89
|
collapsed?: boolean;
|
|
90
|
+
/** Where the section's rows put their labels, over the panel's `layout`. */
|
|
91
|
+
layout?: 'block' | 'inline';
|
|
92
|
+
/** How the section packs its rows, over the panel's `pack`. */
|
|
93
|
+
pack?: 'auto' | 'pairs' | 'one-up';
|
|
79
94
|
}
|
|
80
95
|
|
|
81
96
|
/** A schema resolved against a set of rules: the vocabulary weasel-ui renders,
|
|
@@ -97,12 +112,27 @@ export interface ResolvedConfig {
|
|
|
97
112
|
export interface SectionOption {
|
|
98
113
|
label: string;
|
|
99
114
|
collapsed?: boolean;
|
|
115
|
+
/** Where the section's rows put their labels, over the panel's `layout`. */
|
|
116
|
+
layout?: 'block' | 'inline';
|
|
117
|
+
/** How the section packs its rows, over the panel's `pack`. */
|
|
118
|
+
pack?: 'auto' | 'pairs' | 'one-up';
|
|
100
119
|
}
|
|
101
120
|
|
|
102
121
|
/** Per-node extras that do not belong on a `PrefLeaf`. */
|
|
103
122
|
export interface NodeOptions extends BranchOptions {
|
|
104
123
|
render?: ControlRenderer;
|
|
105
|
-
|
|
124
|
+
/** Errors for this leaf under `config`, the instrument's whole config. */
|
|
125
|
+
validate?: (leaf: PrefLeaf, config: Record<string, unknown>) => string[];
|
|
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
|
+
* `?? compute()`. */
|
|
129
|
+
autoResolve?: (config: Record<string, unknown>) => unknown;
|
|
130
|
+
/** The leaf starts auto rather than pinned at its default. Set by
|
|
131
|
+
* `.initial(auto)`. */
|
|
132
|
+
unpinned?: boolean;
|
|
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
|
+
manual?: boolean;
|
|
106
136
|
}
|
|
107
137
|
|
|
108
138
|
/** What a branch can say about itself, beyond its children. */
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { resolveAutoConfig } from './autoConfig';
|
|
3
|
+
import type { ResolvedConfig } from './types';
|
|
4
|
+
|
|
5
|
+
const NONE: ReadonlySet<string> = new Set();
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The config an instrument reads: the stored one with every unpinned path
|
|
9
|
+
* resolved. The single place resolution happens, so `Trial` and
|
|
10
|
+
* `useTrialState` cannot disagree about what an instrument is looking at.
|
|
11
|
+
*/
|
|
12
|
+
export function useResolvedConfig<TC>(
|
|
13
|
+
schema: ResolvedConfig | undefined,
|
|
14
|
+
config: TC,
|
|
15
|
+
autoPaths: readonly string[] | undefined,
|
|
16
|
+
): { config: TC; auto: ReadonlySet<string> } {
|
|
17
|
+
const auto = useMemo(
|
|
18
|
+
() => (autoPaths && autoPaths.length > 0 ? new Set(autoPaths) : NONE),
|
|
19
|
+
[autoPaths],
|
|
20
|
+
);
|
|
21
|
+
const resolvedConfig = useMemo(
|
|
22
|
+
() => (schema ? resolveAutoConfig(schema, config, auto) : config),
|
|
23
|
+
[schema, config, auto],
|
|
24
|
+
);
|
|
25
|
+
return { config: resolvedConfig, auto };
|
|
26
|
+
}
|