@weasel-js/labkit 1.4.3 → 1.5.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 +13 -2
- package/dist/_dts/{CanvasStackContext-Dde8_1my.d.ts → CanvasStackContext-CqkxkFJW.d.ts} +1 -1
- package/dist/_dts/ToggleBar.d-DbHoWKIA.d.ts +116 -0
- package/dist/_dts/{frac-B5mB8LsJ.d.ts → frac-8R6UmlvN.d.ts} +256 -33
- package/dist/_dts/index-B8uu2ba7.d.ts +237 -0
- package/dist/_dts/{index-DelFfeV0.d.ts → index-retnAut7.d.ts} +45 -12
- package/dist/_dts/{types-B9_zrHmb.d.ts → types-Ca2LCnPq.d.ts} +65 -2
- package/dist/_dts/usePanZoom-nS798rOF.d.ts +160 -0
- package/dist/_dts/useTrialState-jcx_TdU6.d.ts +212 -0
- package/dist/canvas/index.d.ts +17 -128
- package/dist/canvas/index.js +4 -2
- package/dist/chrome/index.d.ts +4 -92
- package/dist/chrome/index.js +9 -5
- package/dist/chunk-67SJLMC7.js +704 -0
- package/dist/chunk-67SJLMC7.js.map +1 -0
- package/dist/chunk-6ZDGOZQV.js +310 -0
- package/dist/chunk-6ZDGOZQV.js.map +1 -0
- package/dist/{chunk-QAGAHJKZ.js → chunk-EKIICY6X.js} +111 -25
- package/dist/chunk-EKIICY6X.js.map +1 -0
- package/dist/{chunk-CTRKTLYZ.js → chunk-H6ZAOWNE.js} +11 -11
- package/dist/chunk-H6ZAOWNE.js.map +1 -0
- package/dist/{chunk-2RQPGOQF.js → chunk-KJALCDWE.js} +9 -7
- package/dist/chunk-KJALCDWE.js.map +1 -0
- package/dist/{chunk-ISCOFAUO.js → chunk-MUKOW3TC.js} +3 -3
- package/dist/{chunk-ISCOFAUO.js.map → chunk-MUKOW3TC.js.map} +1 -1
- package/dist/chunk-NDRYLVOW.js +15 -0
- package/dist/chunk-NDRYLVOW.js.map +1 -0
- package/dist/{chunk-C42T6DJT.js → chunk-NS64DXMN.js} +226 -159
- package/dist/chunk-NS64DXMN.js.map +1 -0
- package/dist/{chunk-I6JCVE24.js → chunk-PGETSEDK.js} +135 -13
- package/dist/chunk-PGETSEDK.js.map +1 -0
- package/dist/{chunk-FQMJUVHJ.js → chunk-QLSV2N3G.js} +32 -37
- package/dist/chunk-QLSV2N3G.js.map +1 -0
- package/dist/chunk-RWNDDKOH.js +682 -0
- package/dist/chunk-RWNDDKOH.js.map +1 -0
- package/dist/chunk-S2SHZRD7.js +7681 -0
- package/dist/chunk-S2SHZRD7.js.map +1 -0
- package/dist/{chunk-ASWKLRKJ.js → chunk-TQBBYTAF.js} +19 -82
- package/dist/chunk-TQBBYTAF.js.map +1 -0
- package/dist/chunk-UU3NJO6N.js +72 -0
- package/dist/chunk-UU3NJO6N.js.map +1 -0
- package/dist/chunk-WIG6XHJ7.js +35 -0
- package/dist/chunk-WIG6XHJ7.js.map +1 -0
- package/dist/{chunk-TJ7QY3OC.js → chunk-ZPH5WTWF.js} +21 -15
- package/dist/chunk-ZPH5WTWF.js.map +1 -0
- package/dist/config/index.d.ts +232 -0
- package/dist/config/index.js +6 -0
- package/dist/config/index.js.map +1 -0
- package/dist/controls/index.d.ts +2 -3
- package/dist/controls/index.js +3 -2
- package/dist/dragdrop/index.d.ts +2 -4
- package/dist/index.d.ts +123 -348
- package/dist/index.js +794 -837
- package/dist/index.js.map +1 -1
- package/dist/layers/index.d.ts +3 -5
- package/dist/layers/index.js +2 -2
- package/dist/loupe/index.d.ts +6 -8
- package/dist/loupe/index.js +2 -2
- package/dist/passthrough/weasel-ui.d.ts +169 -117
- package/dist/passthrough/weasel-ui.js +1 -1
- package/dist/primitives/index.d.ts +53 -6
- package/dist/primitives/index.js +6 -4
- package/dist/state/index.d.ts +12 -7
- package/dist/state/index.js +16 -13
- package/dist/state/index.js.map +1 -1
- package/dist/styles.css +163 -62
- package/dist/surface/index.d.ts +44 -7
- package/dist/surface/index.js +2 -1
- package/dist/ui/layers/index.js +1 -1
- package/dist/undo/index.d.ts +2 -4
- package/package.json +15 -8
- package/src/annotations/AnnotationOverlay.tsx +53 -5
- package/src/annotations/Annotations.overlay.test.tsx +85 -6
- package/src/annotations/drawOne.test.ts +1 -1
- package/src/annotations/drawOne.ts +2 -2
- package/src/annotations/frac.test.ts +4 -4
- package/src/annotations/frac.ts +3 -2
- package/src/annotations/index.ts +1 -1
- package/src/annotations/paint.test.ts +2 -2
- package/src/annotations/paint.ts +1 -1
- package/src/annotations/preload.ts +6 -0
- package/src/annotations/store.test.ts +18 -0
- package/src/annotations/store.ts +7 -6
- package/src/annotations/svgNodes.test.ts +2 -2
- package/src/annotations/svgNodes.ts +1 -2
- package/src/annotations/toolMap.ts +3 -3
- package/src/annotations/types.ts +6 -3
- package/src/canvas/CameraWheelContext.ts +12 -0
- package/src/canvas/CanvasStack.test.tsx +25 -0
- package/src/canvas/CanvasStack.tsx +31 -4
- package/src/canvas/Stage.less +18 -0
- package/src/canvas/Stage.test.tsx +186 -0
- package/src/canvas/Stage.tsx +134 -0
- package/src/canvas/camera.test.ts +17 -2
- package/src/canvas/camera.ts +9 -9
- package/src/canvas/index.ts +4 -0
- package/src/canvas/usePanZoom.ts +10 -5
- package/src/chrome/ChromeRegions.stories.tsx +0 -3
- package/src/chrome/LabChrome.tsx +114 -0
- package/src/chrome/builtins.tsx +20 -31
- package/src/chrome/index.ts +15 -0
- package/src/chrome/labRegions.test.tsx +208 -0
- package/src/chrome/labTypes.ts +50 -0
- package/src/chrome/merge.ts +7 -9
- package/src/chrome/regions/PaletteRegion.test.tsx +6 -4
- package/src/chrome/regions/PaletteRegion.tsx +21 -9
- package/src/chrome/regions/SidebarRegion.tsx +25 -13
- package/src/chrome/regions/StatusRegion.tsx +15 -8
- package/src/chrome/regions/ToolbarRegion.tsx +30 -16
- package/src/chrome/types.ts +47 -20
- package/src/config/builder.test.ts +4 -0
- package/src/config/builder.ts +16 -10
- package/src/config/declarationEmit.fixture.ts +11 -0
- package/src/config/declarationEmit.test.ts +51 -0
- package/src/config/entry.test.ts +46 -0
- package/src/config/index.ts +13 -1
- package/src/config/nodeClasses.test.ts +18 -0
- package/src/config/types.ts +3 -1
- package/src/controls/ControlPanel.test.tsx +16 -0
- package/src/controls/ControlPanel.tsx +5 -2
- package/src/fake-indexeddb-auto.d.ts +5 -0
- package/src/index.test.ts +7 -0
- package/src/index.ts +39 -6
- package/src/instrument/serializers.test.ts +22 -0
- package/src/instrument/serializers.ts +11 -0
- package/src/instrument/types.ts +25 -1
- package/src/lab/Lab.less +4 -13
- package/src/lab/Lab.persist.test.tsx +147 -0
- package/src/lab/Lab.stories.tsx +0 -2
- package/src/lab/Lab.surface.test.tsx +66 -3
- package/src/lab/Lab.test.tsx +91 -17
- package/src/lab/Lab.tsx +337 -124
- package/src/lab/LabFit.stories.less +18 -0
- package/src/lab/LabFit.stories.tsx +219 -0
- package/src/lab/LabFullChrome.stories.tsx +0 -3
- package/src/lab/LabHeader.test.tsx +40 -9
- package/src/lab/LabHeader.tsx +7 -13
- package/src/lab/LabPalette.tsx +13 -26
- package/src/lab/LabShell.less +49 -12
- package/src/lab/LabShell.tsx +14 -2
- package/src/lab/LabSwitcher.less +92 -0
- package/src/lab/LabSwitcher.test.tsx +96 -0
- package/src/lab/LabSwitcher.tsx +112 -0
- package/src/lab/Workspace.surface.test.tsx +2 -1
- package/src/lab/fitCheck.test.ts +57 -0
- package/src/lab/fitCheck.ts +125 -0
- package/src/lab/index.ts +2 -0
- package/src/loupe/AGENTS.md +1 -1
- package/src/loupe/Loupe.less +1 -1
- package/src/loupe/canvasLens.ts +1 -1
- package/src/loupe/types.ts +1 -1
- package/src/loupe/useLoupe.test.tsx +1 -1
- package/src/loupe/useLoupe.ts +1 -1
- package/src/passthrough/weasel-ui.ts +9 -0
- package/src/primitives/FloatingPanel.stories.tsx +13 -1
- package/src/primitives/FloatingPanel.test.tsx +47 -16
- package/src/primitives/FloatingPanel.tsx +11 -25
- package/src/primitives/Split.less +14 -0
- package/src/primitives/Split.test.tsx +108 -0
- package/src/primitives/Split.tsx +159 -0
- package/src/primitives/ZoomControl.tsx +5 -2
- package/src/primitives/index.ts +2 -0
- package/src/state/Persistence.stories.tsx +75 -0
- package/src/state/Persistence.tsx +33 -0
- package/src/state/SingletonExperiment.test.tsx +47 -53
- package/src/state/SingletonExperiment.tsx +24 -13
- package/src/state/adapterContract.ts +116 -0
- package/src/state/adapters.test.ts +113 -61
- package/src/state/adapters.ts +328 -69
- package/src/state/document.test.ts +71 -96
- package/src/state/document.ts +66 -52
- package/src/state/helpers.test.ts +20 -0
- package/src/state/helpers.ts +16 -0
- package/src/state/index.ts +8 -0
- package/src/state/labRecords.test.ts +105 -0
- package/src/state/labRecords.ts +160 -0
- package/src/state/openLabStore.test.ts +658 -0
- package/src/state/openLabStore.ts +403 -0
- package/src/state/records.test.ts +234 -0
- package/src/state/records.ts +219 -0
- package/src/state/store.test.ts +104 -635
- package/src/state/store.ts +117 -158
- package/src/state/toolSlot.test.ts +1 -1
- package/src/state/types.ts +39 -15
- package/src/state/useOpenOnce.ts +68 -0
- package/src/state/usePersistedState.test.tsx +125 -0
- package/src/state/usePersistedState.ts +66 -0
- package/src/state/useTrialState.test.tsx +2 -3
- package/src/state/view.test.ts +49 -15
- package/src/state/view.ts +18 -1
- package/src/styles.less +3 -0
- package/src/surface/AGENTS.md +39 -7
- package/src/surface/SurfaceContext.ts +26 -3
- package/src/surface/index.ts +2 -0
- package/src/surface/useSurfaceTile.test.tsx +2 -1
- package/src/surface/useSurfaceTile.ts +6 -5
- package/src/surface/useTiledSurface.test.tsx +175 -0
- package/src/surface/useTiledSurface.ts +54 -5
- package/src/theme/Interstellar.stories.tsx +7 -7
- package/src/theme/base.less +4 -4
- package/src/theme/interstellar.test.ts +1 -1
- package/src/theme/interstellar.tokens.json +2 -2
- package/src/tools/labTool.ts +11 -0
- package/src/trial/Trial.annotations.persist.test.tsx +100 -58
- package/src/trial/Trial.annotations.test.tsx +47 -4
- package/src/trial/Trial.job.test.tsx +3 -3
- package/src/trial/Trial.less +1 -26
- package/src/trial/Trial.stories.tsx +1 -2
- package/src/trial/Trial.test.tsx +28 -4
- package/src/trial/Trial.trialId.test.tsx +23 -1
- package/src/trial/Trial.tsx +118 -38
- package/src/trial/TrialBody.tsx +17 -141
- package/src/trial/TrialChrome.tsx +7 -4
- package/src/trial/trialOps.ts +7 -4
- package/dist/_dts/types-BP2OCcpg.d.ts +0 -155
- package/dist/_dts/types-x92Kfeme.d.ts +0 -62
- package/dist/_dts/useTrialState-D6Vb3T-g.d.ts +0 -99
- package/dist/chunk-2RQPGOQF.js.map +0 -1
- package/dist/chunk-5TROQVQ2.js +0 -547
- package/dist/chunk-5TROQVQ2.js.map +0 -1
- package/dist/chunk-ASWKLRKJ.js.map +0 -1
- package/dist/chunk-C42T6DJT.js.map +0 -1
- package/dist/chunk-CTRKTLYZ.js.map +0 -1
- package/dist/chunk-FQMJUVHJ.js.map +0 -1
- package/dist/chunk-I6JCVE24.js.map +0 -1
- package/dist/chunk-MHAGC6VD.js +0 -7320
- package/dist/chunk-MHAGC6VD.js.map +0 -1
- package/dist/chunk-QAGAHJKZ.js.map +0 -1
- package/dist/chunk-TJ7QY3OC.js.map +0 -1
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import { StatusBar } from '../../primitives/StatusBar';
|
|
2
|
-
import type {
|
|
2
|
+
import type { RegionContribution, StatusReadout, TrialChromeContext } from '../types';
|
|
3
3
|
|
|
4
4
|
/** Props for `<StatusRegion>`. */
|
|
5
|
-
export interface StatusRegionProps {
|
|
6
|
-
contributions: readonly
|
|
7
|
-
ctx:
|
|
5
|
+
export interface StatusRegionProps<TCtx = TrialChromeContext> {
|
|
6
|
+
contributions: readonly RegionContribution<NoInfer<TCtx>>[];
|
|
7
|
+
ctx: TCtx;
|
|
8
|
+
/** Which region's readouts to lay out; the lab's footer is `footer`. */
|
|
9
|
+
region?: string;
|
|
8
10
|
}
|
|
9
11
|
|
|
10
|
-
/** Lays a
|
|
11
|
-
export function StatusRegion
|
|
12
|
+
/** Lays a bar's readouts out. */
|
|
13
|
+
export function StatusRegion<TCtx = TrialChromeContext>({
|
|
14
|
+
contributions,
|
|
15
|
+
ctx,
|
|
16
|
+
region = 'status',
|
|
17
|
+
}: StatusRegionProps<TCtx>) {
|
|
12
18
|
if (contributions.length === 0) return null;
|
|
13
19
|
return (
|
|
14
20
|
<StatusBar>
|
|
@@ -19,10 +25,11 @@ export function StatusRegion({ contributions, ctx }: StatusRegionProps) {
|
|
|
19
25
|
{c.render(ctx)}
|
|
20
26
|
</StatusBar.Section>
|
|
21
27
|
);
|
|
22
|
-
if (c.region !==
|
|
28
|
+
if (c.region !== region || !c.item) return null;
|
|
29
|
+
const item = c.item as StatusReadout;
|
|
23
30
|
return (
|
|
24
31
|
<StatusBar.Section key={c.id} end={c.end}>
|
|
25
|
-
<span title={
|
|
32
|
+
<span title={item.title}>{item.text}</span>
|
|
26
33
|
</StatusBar.Section>
|
|
27
34
|
);
|
|
28
35
|
})}
|
|
@@ -1,24 +1,28 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
2
|
import { Toolbar } from '../../primitives/Toolbar';
|
|
3
|
-
import type {
|
|
3
|
+
import type { RegionContribution, ToolbarItem, TrialChromeContext } from '../types';
|
|
4
4
|
|
|
5
5
|
/** Props for `<ToolbarRegion>`. */
|
|
6
|
-
export interface ToolbarRegionProps {
|
|
7
|
-
contributions: readonly
|
|
8
|
-
ctx:
|
|
6
|
+
export interface ToolbarRegionProps<TCtx = TrialChromeContext> {
|
|
7
|
+
contributions: readonly RegionContribution<NoInfer<TCtx>>[];
|
|
8
|
+
ctx: TCtx;
|
|
9
|
+
/** Which region's items to lay out; the lab's action bar is `header`. */
|
|
10
|
+
region?: string;
|
|
11
|
+
/** The bar's accessible name. */
|
|
12
|
+
label?: string;
|
|
9
13
|
}
|
|
10
14
|
|
|
11
|
-
interface Group {
|
|
15
|
+
interface Group<TCtx> {
|
|
12
16
|
key: string;
|
|
13
17
|
end: boolean;
|
|
14
|
-
entries:
|
|
18
|
+
entries: RegionContribution<TCtx>[];
|
|
15
19
|
}
|
|
16
20
|
|
|
17
21
|
/** Bucket by group, preserving first-appearance order. Ungrouped
|
|
18
22
|
* contributions each become their own bucket so they stay in place. */
|
|
19
|
-
function groupsOf(contributions: readonly
|
|
20
|
-
const groups: Group[] = [];
|
|
21
|
-
const byKey = new Map<string, Group
|
|
23
|
+
function groupsOf<TCtx>(contributions: readonly RegionContribution<TCtx>[]): Group<TCtx>[] {
|
|
24
|
+
const groups: Group<TCtx>[] = [];
|
|
25
|
+
const byKey = new Map<string, Group<TCtx>>();
|
|
22
26
|
for (const c of contributions) {
|
|
23
27
|
if (c.group == null) {
|
|
24
28
|
groups.push({ key: c.id, end: c.end ?? false, entries: [c] });
|
|
@@ -35,10 +39,15 @@ function groupsOf(contributions: readonly TrialContribution[]): Group[] {
|
|
|
35
39
|
return groups;
|
|
36
40
|
}
|
|
37
41
|
|
|
38
|
-
function renderEntry(
|
|
42
|
+
function renderEntry<TCtx>(
|
|
43
|
+
c: RegionContribution<TCtx>,
|
|
44
|
+
ctx: TCtx,
|
|
45
|
+
region: string,
|
|
46
|
+
): ReactNode {
|
|
39
47
|
if (c.render) return <span key={c.id}>{c.render(ctx)}</span>;
|
|
40
|
-
if (c.region !==
|
|
41
|
-
const { icon: Icon, label, shortcut, disabled, danger, showLabel, pressed, onActivate } =
|
|
48
|
+
if (c.region !== region || !c.item) return null;
|
|
49
|
+
const { icon: Icon, label, shortcut, disabled, danger, showLabel, pressed, onActivate } =
|
|
50
|
+
c.item as ToolbarItem<TCtx>;
|
|
42
51
|
return (
|
|
43
52
|
<Toolbar.Button
|
|
44
53
|
key={c.id}
|
|
@@ -56,15 +65,20 @@ function renderEntry(c: TrialContribution, ctx: TrialChromeContext): ReactNode {
|
|
|
56
65
|
);
|
|
57
66
|
}
|
|
58
67
|
|
|
59
|
-
/** Lays a
|
|
60
|
-
export function ToolbarRegion
|
|
68
|
+
/** Lays a bar's contributions out, grouped by their `group`. */
|
|
69
|
+
export function ToolbarRegion<TCtx = TrialChromeContext>({
|
|
70
|
+
contributions,
|
|
71
|
+
ctx,
|
|
72
|
+
region = 'toolbar',
|
|
73
|
+
label = 'Trial actions',
|
|
74
|
+
}: ToolbarRegionProps<TCtx>) {
|
|
61
75
|
if (contributions.length === 0) return null;
|
|
62
76
|
const groups = groupsOf(contributions);
|
|
63
77
|
return (
|
|
64
|
-
<Toolbar aria-label=
|
|
78
|
+
<Toolbar aria-label={label}>
|
|
65
79
|
{groups.map((g) => (
|
|
66
80
|
<Toolbar.Group key={g.key} end={g.end} aria-label={g.key}>
|
|
67
|
-
{g.entries.map((c) => renderEntry(c, ctx))}
|
|
81
|
+
{g.entries.map((c) => renderEntry(c, ctx, region))}
|
|
68
82
|
</Toolbar.Group>
|
|
69
83
|
))}
|
|
70
84
|
</Toolbar>
|
package/src/chrome/types.ts
CHANGED
|
@@ -4,14 +4,15 @@ import type { ConfigField } from '../controls/types';
|
|
|
4
4
|
import type { SavedSnapshot } from '../state/types';
|
|
5
5
|
|
|
6
6
|
/** A named position in a trial's chrome. Content is not a region — that is
|
|
7
|
-
* the instrument. */
|
|
7
|
+
* the instrument. The lab's own boxes are named in `LabRegion`. */
|
|
8
8
|
export type TrialRegion = 'titlebar' | 'toolbar' | 'palette' | 'sidebar' | 'viewport' | 'status';
|
|
9
9
|
|
|
10
10
|
/** An icon component taking a pixel size, as `@weasel-js/ui` glyphs do. */
|
|
11
11
|
export type IconComponent = ComponentType<{ size?: number }>;
|
|
12
12
|
|
|
13
|
-
/** A button in
|
|
14
|
-
|
|
13
|
+
/** A button in a toolbar. `TCtx` is the chrome context its `onActivate` is
|
|
14
|
+
* handed — a trial's, or the lab's for a lab-level bar. */
|
|
15
|
+
export interface ToolbarItem<TCtx = TrialChromeContext> {
|
|
15
16
|
icon: IconComponent;
|
|
16
17
|
label: string;
|
|
17
18
|
/** Shown in the tooltip. Not bound here — the trial owns its keymap. */
|
|
@@ -24,10 +25,10 @@ export interface ToolbarItem {
|
|
|
24
25
|
pressed?: boolean;
|
|
25
26
|
/** Render the label beside the glyph rather than only in the tooltip. */
|
|
26
27
|
showLabel?: boolean;
|
|
27
|
-
/** Handed the
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
onActivate: (ctx:
|
|
28
|
+
/** Handed the chrome context it was declared against, so a contribution can
|
|
29
|
+
* reach `ctx.saveSnapshot()` and the rest without the `render` escape. A
|
|
30
|
+
* zero-argument handler stays valid. */
|
|
31
|
+
onActivate: (ctx: TCtx) => void;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
/** A selectable tool in the palette region. */
|
|
@@ -52,12 +53,12 @@ export interface SidebarSection {
|
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
/** A control acting on the view of the trial, not on the trial. */
|
|
55
|
-
export interface ViewportControl {
|
|
56
|
+
export interface ViewportControl<TCtx = TrialChromeContext> {
|
|
56
57
|
icon: IconComponent;
|
|
57
58
|
label: string;
|
|
58
59
|
disabled?: boolean;
|
|
59
60
|
/** Handed the trial's chrome context, as `ToolbarItem.onActivate` is. */
|
|
60
|
-
onActivate: (ctx:
|
|
61
|
+
onActivate: (ctx: TCtx) => void;
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
/** A readout in the status bar. */
|
|
@@ -68,8 +69,8 @@ export interface StatusReadout {
|
|
|
68
69
|
title?: string;
|
|
69
70
|
}
|
|
70
71
|
|
|
71
|
-
/** What every contribution shares. */
|
|
72
|
-
interface ContributionBase {
|
|
72
|
+
/** What every contribution shares, whichever chrome it is declared against. */
|
|
73
|
+
export interface ContributionBase {
|
|
73
74
|
id: string;
|
|
74
75
|
/** Groups sort by first appearance; items sort within a group by
|
|
75
76
|
* declaration order. Contributions with no group sort after grouped ones. */
|
|
@@ -96,18 +97,49 @@ export type TrialContribution =
|
|
|
96
97
|
render: (ctx: TrialChromeContext) => ReactNode;
|
|
97
98
|
});
|
|
98
99
|
|
|
100
|
+
/**
|
|
101
|
+
* A tool slot a region can reflect and write. Both chrome contexts carry one:
|
|
102
|
+
* a trial's resolves to the lab's when its instrument declares no tools.
|
|
103
|
+
*/
|
|
104
|
+
export interface ToolSlotContext {
|
|
105
|
+
activeToolId: string | null;
|
|
106
|
+
setActiveTool: (id: string) => void;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** What a sidebar region reflects and writes: which sections are folded, and —
|
|
110
|
+
* where the chrome can tear a section out — where it goes. A trial supplies
|
|
111
|
+
* all four; a lab supplies the fold state only. */
|
|
112
|
+
export interface SidebarSlotContext {
|
|
113
|
+
collapsedSections: Readonly<Record<string, boolean>>;
|
|
114
|
+
setSectionCollapsed: (key: string, collapsed: boolean) => void;
|
|
115
|
+
undockedPanels?: readonly string[];
|
|
116
|
+
undockPanel?: (sectionId: string, as?: 'tile' | 'floating') => void;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* A contribution as a region renderer sees it. A renderer checks the region
|
|
121
|
+
* name against its own and narrows `item` itself, which is what lets one
|
|
122
|
+
* renderer serve both chromes — the trial's and the lab's — without knowing
|
|
123
|
+
* which context it was handed.
|
|
124
|
+
*/
|
|
125
|
+
export interface RegionContribution<TCtx> extends ContributionBase {
|
|
126
|
+
region: string;
|
|
127
|
+
item?: unknown;
|
|
128
|
+
render?: (ctx: TCtx) => ReactNode;
|
|
129
|
+
}
|
|
130
|
+
|
|
99
131
|
/**
|
|
100
132
|
* Everything a contribution can read about the trial it is being rendered
|
|
101
133
|
* into. Replaces the three separate slot contexts, which each carried a
|
|
102
134
|
* hand-picked subset.
|
|
103
135
|
*/
|
|
104
|
-
export interface TrialChromeContext {
|
|
136
|
+
export interface TrialChromeContext extends ToolSlotContext, SidebarSlotContext {
|
|
105
137
|
trialId: string;
|
|
106
138
|
instrumentName: string;
|
|
107
|
-
/** What the title bar reads, which is the instrument's
|
|
108
|
-
* calls `setTitle`. */
|
|
139
|
+
/** What the title bar reads, which is the instrument's title (or, lacking
|
|
140
|
+
* one, its name) until something calls `setTitle`. */
|
|
109
141
|
title: string;
|
|
110
|
-
/** Retitle this trial; `null` restores the instrument
|
|
142
|
+
/** Retitle this trial; `null` restores the instrument's title. Persisted with
|
|
111
143
|
* the trial, so a title survives a reload. */
|
|
112
144
|
setTitle: (title: string | null) => void;
|
|
113
145
|
isLastTrial: boolean;
|
|
@@ -157,9 +189,4 @@ export interface TrialChromeContext {
|
|
|
157
189
|
clone: () => void;
|
|
158
190
|
reset: () => void;
|
|
159
191
|
close: () => void;
|
|
160
|
-
|
|
161
|
-
/** Resolved active tool: the trial's slot, or the lab's when the trial has
|
|
162
|
-
* none. Null when neither holds one. */
|
|
163
|
-
activeToolId: string | null;
|
|
164
|
-
setActiveTool: (id: string) => void;
|
|
165
192
|
}
|
|
@@ -35,6 +35,10 @@ describe('builder', () => {
|
|
|
35
35
|
expect(n.annotations.suffix).toBe('px');
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
+
it('annotates a number with a display format', () => {
|
|
39
|
+
expect(f.number(0).format('compact').annotations.format).toBe('compact');
|
|
40
|
+
});
|
|
41
|
+
|
|
38
42
|
it('keeps a custom node kind across a chain', () => {
|
|
39
43
|
const n = f.custom('vector2', { x: 0 }).label('Offset');
|
|
40
44
|
expect(n.kind).toBe('vector2');
|
package/src/config/builder.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PrefLeaf } from '@weasel-js/ui';
|
|
1
|
+
import type { PrefLeaf, PrefNumberFormat } from '@weasel-js/ui';
|
|
2
2
|
import type {
|
|
3
3
|
Annotations,
|
|
4
4
|
BranchAnnotations,
|
|
@@ -21,7 +21,7 @@ export function isConfigBranch(entry: ConfigEntry): entry is ConfigBranch {
|
|
|
21
21
|
|
|
22
22
|
/** Shared chaining surface. Every method clones, so a node can be reused as a
|
|
23
23
|
* base for several leaves without one bleeding into the next. */
|
|
24
|
-
abstract class BaseNode<T> implements ConfigNode<T> {
|
|
24
|
+
export abstract class BaseNode<T> implements ConfigNode<T> {
|
|
25
25
|
abstract readonly kind: string | null;
|
|
26
26
|
|
|
27
27
|
// `default` is a reserved word in a parameter position, so the field is
|
|
@@ -89,7 +89,7 @@ abstract class BaseNode<T> implements ConfigNode<T> {
|
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
class NumberNode extends BaseNode<number> {
|
|
92
|
+
export class NumberNode extends BaseNode<number> {
|
|
93
93
|
readonly kind = 'number';
|
|
94
94
|
|
|
95
95
|
/** Bound the value. A number with both bounds renders as a slider. */
|
|
@@ -107,6 +107,12 @@ class NumberNode extends BaseNode<number> {
|
|
|
107
107
|
return this.ann({ suffix });
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
/** Show the value in a named format — `'compact'` reads `2.0M`. Presentation
|
|
111
|
+
* only: the stored value stays a plain number, and a typed `2.5m` reads back. */
|
|
112
|
+
format(format: PrefNumberFormat): this {
|
|
113
|
+
return this.ann({ format });
|
|
114
|
+
}
|
|
115
|
+
|
|
110
116
|
/** Force a slider even without both bounds. */
|
|
111
117
|
slider(): this {
|
|
112
118
|
return this.ann({ control: 'slider' });
|
|
@@ -118,7 +124,7 @@ class NumberNode extends BaseNode<number> {
|
|
|
118
124
|
}
|
|
119
125
|
}
|
|
120
126
|
|
|
121
|
-
class BooleanNode extends BaseNode<boolean> {
|
|
127
|
+
export class BooleanNode extends BaseNode<boolean> {
|
|
122
128
|
readonly kind = 'boolean';
|
|
123
129
|
|
|
124
130
|
/** Ask for a switch. `ControlPanel` still draws a checkbox; weasel-ui's
|
|
@@ -128,7 +134,7 @@ class BooleanNode extends BaseNode<boolean> {
|
|
|
128
134
|
}
|
|
129
135
|
}
|
|
130
136
|
|
|
131
|
-
class StringNode extends BaseNode<string> {
|
|
137
|
+
export class StringNode extends BaseNode<string> {
|
|
132
138
|
readonly kind = 'string';
|
|
133
139
|
|
|
134
140
|
placeholder(placeholder: string): this {
|
|
@@ -145,11 +151,11 @@ class StringNode extends BaseNode<string> {
|
|
|
145
151
|
}
|
|
146
152
|
}
|
|
147
153
|
|
|
148
|
-
class ColorNode extends BaseNode<string> {
|
|
154
|
+
export class ColorNode extends BaseNode<string> {
|
|
149
155
|
readonly kind = 'color';
|
|
150
156
|
}
|
|
151
157
|
|
|
152
|
-
class EnumNode<T extends string> extends BaseNode<T> {
|
|
158
|
+
export class EnumNode<T extends string> extends BaseNode<T> {
|
|
153
159
|
readonly kind = 'enum';
|
|
154
160
|
|
|
155
161
|
/** Render as a segmented control rather than a select. */
|
|
@@ -165,12 +171,12 @@ class EnumNode<T extends string> extends BaseNode<T> {
|
|
|
165
171
|
}
|
|
166
172
|
|
|
167
173
|
/** A leaf whose kind the rule chain decides. */
|
|
168
|
-
class ValueNode<T> extends BaseNode<T> {
|
|
174
|
+
export class ValueNode<T> extends BaseNode<T> {
|
|
169
175
|
readonly kind = null;
|
|
170
176
|
}
|
|
171
177
|
|
|
172
178
|
/** A leaf of a kind labkit does not ship a row for. */
|
|
173
|
-
class CustomNode<T> extends BaseNode<T> {
|
|
179
|
+
export class CustomNode<T> extends BaseNode<T> {
|
|
174
180
|
constructor(
|
|
175
181
|
readonly kind: string,
|
|
176
182
|
defaultValue: T,
|
|
@@ -190,7 +196,7 @@ class CustomNode<T> extends BaseNode<T> {
|
|
|
190
196
|
* one is read and written at a dotted path. Distinct from `.section()`, which
|
|
191
197
|
* puts a heading over sibling leaves and leaves their paths alone.
|
|
192
198
|
*/
|
|
193
|
-
class GroupNode<S extends ConfigShape> implements ConfigBranch<S> {
|
|
199
|
+
export class GroupNode<S extends ConfigShape> implements ConfigBranch<S> {
|
|
194
200
|
constructor(
|
|
195
201
|
readonly children: S,
|
|
196
202
|
readonly annotations: Readonly<BranchAnnotations> = {},
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { f } from './builder';
|
|
2
|
+
|
|
3
|
+
export const flat = f.schema({ n: f.number(1).range(0, 4), s: f.string('x').label('S') });
|
|
4
|
+
|
|
5
|
+
export const grouped = f.schema({
|
|
6
|
+
g: f.group({ on: f.boolean(true).toggle(), e: f.enum('a', ['a', 'b']).radio() }).label('G'),
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export const node = f.number(1).slider();
|
|
10
|
+
|
|
11
|
+
export const group = f.group({ c: f.color('#fff'), v: f.value(1), k: f.custom('k', 'x') });
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import ts from 'typescript';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
const root = resolve(__dirname, '../../../..');
|
|
6
|
+
const fixture = resolve(__dirname, 'declarationEmit.fixture.ts');
|
|
7
|
+
|
|
8
|
+
/** Emits declarations for one file under the root tsconfig and returns every diagnostic, formatted. */
|
|
9
|
+
function declarationDiagnostics(file: string): string[] {
|
|
10
|
+
const parsed = ts.getParsedCommandLineOfConfigFile(
|
|
11
|
+
resolve(root, 'tsconfig.json'),
|
|
12
|
+
{},
|
|
13
|
+
{
|
|
14
|
+
...ts.sys,
|
|
15
|
+
onUnRecoverableConfigFileDiagnostic: (d) => {
|
|
16
|
+
throw new Error(ts.flattenDiagnosticMessageText(d.messageText, '\n'));
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
);
|
|
20
|
+
if (!parsed) throw new Error('could not read the root tsconfig');
|
|
21
|
+
const options: ts.CompilerOptions = {
|
|
22
|
+
...parsed.options,
|
|
23
|
+
noEmit: false,
|
|
24
|
+
declaration: true,
|
|
25
|
+
emitDeclarationOnly: true,
|
|
26
|
+
declarationMap: false,
|
|
27
|
+
sourceMap: false,
|
|
28
|
+
incremental: false,
|
|
29
|
+
tsBuildInfoFile: undefined,
|
|
30
|
+
types: [],
|
|
31
|
+
};
|
|
32
|
+
const program = ts.createProgram({ rootNames: [file], options });
|
|
33
|
+
const source = program.getSourceFile(file);
|
|
34
|
+
if (!source) throw new Error(`${file} is not in the program`);
|
|
35
|
+
const emitted = program.emit(source, () => {}, undefined, true);
|
|
36
|
+
const diagnostics = [
|
|
37
|
+
...program.getSyntacticDiagnostics(source),
|
|
38
|
+
...program.getSemanticDiagnostics(source),
|
|
39
|
+
...emitted.diagnostics,
|
|
40
|
+
];
|
|
41
|
+
return diagnostics.map((d) => {
|
|
42
|
+
const text = ts.flattenDiagnosticMessageText(d.messageText, '\n');
|
|
43
|
+
return d.file ? `${d.file.fileName}: TS${d.code} ${text}` : `TS${d.code} ${text}`;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe('config schema declarations', () => {
|
|
48
|
+
it('emit for exported schemas, groups and nodes', () => {
|
|
49
|
+
expect(declarationDiagnostics(fixture)).toEqual([]);
|
|
50
|
+
}, 60_000);
|
|
51
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { dirname, resolve } from 'node:path';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
const IMPORT =
|
|
6
|
+
/^\s*(?:import|export)\s+(?!type\b)[^'"]*from\s+['"]([^'"]+)['"]|^\s*import\s+['"]([^'"]+)['"]|import\(\s*['"]([^'"]+)['"]\s*\)/gm;
|
|
7
|
+
|
|
8
|
+
const isFile = (path: string) => statSync(path, { throwIfNoEntry: false })?.isFile() ?? false;
|
|
9
|
+
|
|
10
|
+
function walk(file: string, seen = new Set<string>()): Set<string> {
|
|
11
|
+
if (seen.has(file)) return seen;
|
|
12
|
+
seen.add(file);
|
|
13
|
+
const source = readFileSync(file, 'utf8');
|
|
14
|
+
for (const match of source.matchAll(IMPORT)) {
|
|
15
|
+
const spec = match[1] ?? match[2] ?? match[3];
|
|
16
|
+
if (!spec?.startsWith('.')) continue;
|
|
17
|
+
const base = resolve(dirname(file), spec.replace(/\.js$/, ''));
|
|
18
|
+
const candidates = [
|
|
19
|
+
resolve(dirname(file), spec),
|
|
20
|
+
`${base}.ts`,
|
|
21
|
+
`${base}.tsx`,
|
|
22
|
+
resolve(base, 'index.ts'),
|
|
23
|
+
resolve(base, 'index.tsx'),
|
|
24
|
+
];
|
|
25
|
+
const hit = candidates.find(isFile);
|
|
26
|
+
// An import the walk cannot follow would let a stylesheet behind it pass unseen.
|
|
27
|
+
if (!hit) throw new Error(`${file}: cannot resolve ${spec}`);
|
|
28
|
+
walk(hit, seen);
|
|
29
|
+
}
|
|
30
|
+
return seen;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('@weasel-js/labkit/config', () => {
|
|
34
|
+
it('reaches no stylesheet', () => {
|
|
35
|
+
const files = [...walk(resolve(__dirname, 'index.ts'))];
|
|
36
|
+
expect(files.filter((f) => /\.(less|css)$/.test(f))).toEqual([]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('is published as an entry', async () => {
|
|
40
|
+
const pkg = JSON.parse(readFileSync(resolve(__dirname, '../../package.json'), 'utf8'));
|
|
41
|
+
expect(pkg.exports['./config']).toEqual({
|
|
42
|
+
types: './dist/config/index.d.ts',
|
|
43
|
+
import: './dist/config/index.js',
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
});
|
package/src/config/index.ts
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export {
|
|
2
|
+
BaseNode,
|
|
3
|
+
BooleanNode,
|
|
4
|
+
ColorNode,
|
|
5
|
+
CustomNode,
|
|
6
|
+
EnumNode,
|
|
7
|
+
f,
|
|
8
|
+
GroupNode,
|
|
9
|
+
isConfigBranch,
|
|
10
|
+
NumberNode,
|
|
11
|
+
StringNode,
|
|
12
|
+
ValueNode,
|
|
13
|
+
} from './builder';
|
|
2
14
|
export { fromConfigFields } from './fromConfigField';
|
|
3
15
|
export {
|
|
4
16
|
fillConfigDefaults,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { f, GroupNode, NumberNode } from './index';
|
|
3
|
+
|
|
4
|
+
describe('config node classes', () => {
|
|
5
|
+
it('are exported as values', () => {
|
|
6
|
+
expect(typeof NumberNode).toBe('function');
|
|
7
|
+
expect(typeof GroupNode).toBe('function');
|
|
8
|
+
expect(f.number(1)).toBeInstanceOf(NumberNode);
|
|
9
|
+
expect(f.group({ n: f.number(1) })).toBeInstanceOf(GroupNode);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('can be extended', () => {
|
|
13
|
+
class Mine extends NumberNode {}
|
|
14
|
+
const node = new Mine(2);
|
|
15
|
+
expect(node).toBeInstanceOf(NumberNode);
|
|
16
|
+
expect(node.default).toBe(2);
|
|
17
|
+
});
|
|
18
|
+
});
|
package/src/config/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PrefGroup, PrefLeaf, PrefRenderer } from '@weasel-js/ui';
|
|
1
|
+
import type { PrefGroup, PrefLeaf, PrefNumberFormat, PrefRenderer } from '@weasel-js/ui';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Renders the control cell for one config leaf. Identical to weasel-ui's
|
|
@@ -25,6 +25,8 @@ export interface Annotations {
|
|
|
25
25
|
max?: number;
|
|
26
26
|
step?: number;
|
|
27
27
|
suffix?: string;
|
|
28
|
+
/** How a number's value is shown. `compact` abbreviates from a thousand up. */
|
|
29
|
+
format?: PrefNumberFormat;
|
|
28
30
|
control?: string;
|
|
29
31
|
options?: readonly ConfigOption[];
|
|
30
32
|
placeholder?: string;
|
|
@@ -444,6 +444,22 @@ describe('<ControlPanel> suffix', () => {
|
|
|
444
444
|
});
|
|
445
445
|
});
|
|
446
446
|
|
|
447
|
+
describe('<ControlPanel> format', () => {
|
|
448
|
+
it('shows a slider readout in the format the leaf declares', () => {
|
|
449
|
+
render(
|
|
450
|
+
<ControlPanel
|
|
451
|
+
schema={resolveConfigSchema(
|
|
452
|
+
f.schema({ glyphs: f.number(0).range(0, 2_000_000).format('compact') }),
|
|
453
|
+
[],
|
|
454
|
+
)}
|
|
455
|
+
config={{ glyphs: 2_000_000 }}
|
|
456
|
+
setConfig={vi.fn()}
|
|
457
|
+
/>,
|
|
458
|
+
);
|
|
459
|
+
expect(screen.getByDisplayValue('2.0M')).toBeInTheDocument();
|
|
460
|
+
});
|
|
461
|
+
});
|
|
462
|
+
|
|
447
463
|
describe('<ControlPanel> collapsible sections', () => {
|
|
448
464
|
const schema = resolveConfigSchema(
|
|
449
465
|
f.schema({ showGrid: f.boolean(true), seed: f.number(0).section('Advanced') }),
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
NumberRow,
|
|
7
7
|
type PrefGroup,
|
|
8
8
|
type PrefLeaf,
|
|
9
|
+
type PrefNumberFormat,
|
|
9
10
|
type PropertyAlign,
|
|
10
11
|
type PropertyDensity,
|
|
11
12
|
PropertyGroup,
|
|
@@ -26,12 +27,12 @@ import { isLeafVisible } from '../config/visible';
|
|
|
26
27
|
import type { ConfigField } from './types';
|
|
27
28
|
|
|
28
29
|
/** How a panel packs its rows into the two-column property grid.
|
|
29
|
-
* - `'auto'`: narrow controls (numbers, checkboxes, dropdowns,
|
|
30
|
+
* - `'auto'`: narrow controls (numbers, checkboxes, dropdowns, colors) sit
|
|
30
31
|
* two per row; the ones that need the width — text, sliders, segmented
|
|
31
32
|
* toggles — span it.
|
|
32
33
|
* - `'pairs'`: every row pairs. A control that needs the full width says so
|
|
33
34
|
* itself, with `<PropertyRow span>`.
|
|
34
|
-
* - `'one-up'`: one control per row,
|
|
35
|
+
* - `'one-up'`: one control per row, colors excepted.
|
|
35
36
|
*/
|
|
36
37
|
export type ControlPack = 'auto' | 'pairs' | 'one-up';
|
|
37
38
|
|
|
@@ -247,6 +248,7 @@ function ControlRow<TC extends Record<string, unknown>>({
|
|
|
247
248
|
const max = extra<number>(leaf, 'max');
|
|
248
249
|
const step = extra<number>(leaf, 'step');
|
|
249
250
|
const suffix = extra<string>(leaf, 'suffix');
|
|
251
|
+
const notation = extra<PrefNumberFormat>(leaf, 'format');
|
|
250
252
|
if (extra<string>(leaf, 'control') === 'slider' && min !== undefined && max !== undefined) {
|
|
251
253
|
return (
|
|
252
254
|
<SliderRow
|
|
@@ -255,6 +257,7 @@ function ControlRow<TC extends Record<string, unknown>>({
|
|
|
255
257
|
min={min}
|
|
256
258
|
max={max}
|
|
257
259
|
step={step}
|
|
260
|
+
notation={notation}
|
|
258
261
|
unit={suffix}
|
|
259
262
|
onChange={write}
|
|
260
263
|
layout={layout}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// fake-indexeddb's `./auto` export declares no `types` condition, so under
|
|
2
|
+
// `moduleResolution: Bundler` tsc finds no declaration for it — the ambient one
|
|
3
|
+
// the package ships at its root is never part of the compilation. The subpath
|
|
4
|
+
// is side-effect-only, so a bare declaration is the whole of it.
|
|
5
|
+
declare module 'fake-indexeddb/auto';
|
package/src/index.test.ts
CHANGED
|
@@ -51,6 +51,13 @@ describe('public export surface', () => {
|
|
|
51
51
|
});
|
|
52
52
|
});
|
|
53
53
|
|
|
54
|
+
describe('ui re-exports', () => {
|
|
55
|
+
it('carries ToggleBar and Button from the package root', () => {
|
|
56
|
+
expect(labkit).toHaveProperty('ToggleBar');
|
|
57
|
+
expect(labkit).toHaveProperty('Button');
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
54
61
|
describe('surface, job and orbit entry points', () => {
|
|
55
62
|
it('are reachable from the package root', async () => {
|
|
56
63
|
const kit = await import('./index');
|