@weasel-js/labkit 1.4.4 → 1.5.1
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 +54 -6
- package/dist/_dts/{CanvasStackContext-kTEZvEgE.d.ts → CanvasStackContext-CJNMS6mz.d.ts} +1 -1
- package/dist/_dts/{PrefsForm.d-DDHFANkc.d.ts → PrefsForm.d-DPy9I8It.d.ts} +4 -0
- package/dist/_dts/Select.d-Gwr8fLpK.d.ts +453 -0
- package/dist/_dts/{frac-z7ker2Vx.d.ts → frac-B4okC2lX.d.ts} +291 -31
- package/dist/_dts/{index-Blv9uQzu.d.ts → index-BNdz-tNd.d.ts} +105 -17
- package/dist/_dts/index-CSJoj4LC.d.ts +248 -0
- package/dist/_dts/{types-B9_zrHmb.d.ts → types-DzoNY1pO.d.ts} +91 -4
- package/dist/_dts/usePanZoom-Pzf9f2jh.d.ts +160 -0
- package/dist/_dts/useTrialState-uLlmaccn.d.ts +212 -0
- package/dist/canvas/index.d.ts +18 -129
- package/dist/canvas/index.js +4 -2
- package/dist/chrome/index.d.ts +5 -93
- package/dist/chrome/index.js +10 -6
- package/dist/chunk-7PFH3CNM.js +150 -0
- package/dist/chunk-7PFH3CNM.js.map +1 -0
- package/dist/{chunk-I6JCVE24.js → chunk-CEIUBPPW.js} +143 -17
- package/dist/chunk-CEIUBPPW.js.map +1 -0
- package/dist/chunk-EH7UEQV7.js +590 -0
- package/dist/chunk-EH7UEQV7.js.map +1 -0
- package/dist/{chunk-MOM3GOVY.js → chunk-EHGVROVV.js} +125 -27
- package/dist/chunk-EHGVROVV.js.map +1 -0
- package/dist/{chunk-TJ7QY3OC.js → chunk-ERFMC6WU.js} +160 -78
- package/dist/chunk-ERFMC6WU.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-64ZCN3DA.js → chunk-IHUVQ3TX.js} +250 -160
- package/dist/chunk-IHUVQ3TX.js.map +1 -0
- package/dist/{chunk-FQMJUVHJ.js → chunk-IJJUK4FU.js} +53 -46
- package/dist/chunk-IJJUK4FU.js.map +1 -0
- package/dist/chunk-IXTQI7JX.js +737 -0
- package/dist/chunk-IXTQI7JX.js.map +1 -0
- package/dist/{chunk-BKFVHKJH.js → chunk-MDVTK5U3.js} +4 -4
- package/dist/{chunk-BKFVHKJH.js.map → chunk-MDVTK5U3.js.map} +1 -1
- package/dist/chunk-PHK4W562.js +72 -0
- package/dist/chunk-PHK4W562.js.map +1 -0
- package/dist/chunk-PJ7WEQBJ.js +18 -0
- package/dist/chunk-PJ7WEQBJ.js.map +1 -0
- package/dist/chunk-RWNDDKOH.js +682 -0
- package/dist/chunk-RWNDDKOH.js.map +1 -0
- package/dist/chunk-T3EC2DXD.js +12628 -0
- package/dist/chunk-T3EC2DXD.js.map +1 -0
- package/dist/{chunk-SMHP6XZ4.js → chunk-UN4U4P63.js} +10 -8
- package/dist/chunk-UN4U4P63.js.map +1 -0
- package/dist/chunk-WIG6XHJ7.js +35 -0
- package/dist/chunk-WIG6XHJ7.js.map +1 -0
- package/dist/chunk-Z76KEZAX.js +15 -0
- package/dist/chunk-Z76KEZAX.js.map +1 -0
- package/dist/chunk-ZDHY47EV.js +363 -0
- package/dist/chunk-ZDHY47EV.js.map +1 -0
- package/dist/config/index.d.ts +276 -0
- package/dist/config/index.js +8 -0
- package/dist/config/index.js.map +1 -0
- package/dist/controls/index.d.ts +3 -4
- package/dist/controls/index.js +4 -3
- package/dist/dragdrop/index.d.ts +3 -5
- package/dist/index.d.ts +266 -413
- package/dist/index.js +1775 -2251
- package/dist/index.js.map +1 -1
- package/dist/layers/index.d.ts +4 -6
- package/dist/layers/index.js +2 -2
- package/dist/loupe/index.d.ts +69 -20
- package/dist/loupe/index.js +2 -2
- package/dist/passthrough/weasel-ui.d.ts +175 -309
- package/dist/passthrough/weasel-ui.js +1 -1
- package/dist/primitives/index.d.ts +55 -6
- package/dist/primitives/index.js +6 -4
- package/dist/state/index.d.ts +13 -8
- package/dist/state/index.js +18 -13
- package/dist/state/index.js.map +1 -1
- package/dist/styles.css +223 -76
- 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 +3 -5
- package/package.json +15 -8
- package/src/annotations/AnnotationOverlay.tsx +56 -5
- package/src/annotations/Annotations.less +4 -0
- package/src/annotations/Annotations.overlay.test.tsx +105 -6
- package/src/annotations/drawOne.test.ts +1 -1
- package/src/annotations/drawOne.ts +2 -2
- 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 +5 -4
- package/src/annotations/svgNodes.test.ts +2 -2
- package/src/annotations/svgNodes.ts +1 -0
- package/src/annotations/toolMap.test.ts +9 -5
- package/src/annotations/toolMap.ts +6 -3
- package/src/annotations/types.ts +6 -3
- package/src/canvas/CameraWheelContext.ts +12 -0
- package/src/canvas/CanvasStack.stories.tsx +1 -1
- 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.test.ts +28 -0
- package/src/canvas/usePanZoom.ts +21 -9
- package/src/chrome/ChromeRegions.stories.tsx +1 -4
- package/src/chrome/LabChrome.tsx +130 -0
- package/src/chrome/builtins.test.ts +1 -0
- package/src/chrome/builtins.tsx +21 -31
- package/src/chrome/index.ts +16 -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 +16 -9
- package/src/chrome/regions/SidebarRegion.stories.tsx +44 -0
- package/src/chrome/regions/SidebarRegion.tsx +25 -13
- package/src/chrome/regions/StatusRegion.tsx +15 -8
- package/src/chrome/regions/ToolbarRegion.tsx +34 -16
- package/src/chrome/types.ts +50 -20
- 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 +42 -0
- package/src/config/builder.ts +68 -17
- 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 +16 -1
- package/src/config/nodeClasses.test.ts +18 -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 +34 -2
- package/src/config/useResolvedConfig.ts +26 -0
- package/src/controls/AutoControls.stories.tsx +88 -0
- package/src/controls/ControlPanel.stories.tsx +42 -1
- package/src/controls/ControlPanel.test.tsx +530 -4
- package/src/controls/ControlPanel.tsx +562 -82
- package/src/fake-indexeddb-auto.d.ts +5 -0
- package/src/index.test.ts +7 -0
- package/src/index.ts +66 -5
- package/src/instrument/serializers.test.ts +22 -0
- package/src/instrument/serializers.ts +40 -1
- package/src/instrument/types.ts +25 -1
- package/src/lab/Lab.chrome.test.tsx +16 -0
- package/src/lab/Lab.less +4 -13
- package/src/lab/Lab.persist.test.tsx +147 -0
- package/src/lab/Lab.stories.tsx +1 -3
- package/src/lab/Lab.surface.test.tsx +71 -3
- package/src/lab/Lab.test.tsx +168 -16
- package/src/lab/Lab.tsx +434 -129
- package/src/lab/LabContext.ts +8 -0
- package/src/lab/LabFit.stories.less +18 -0
- package/src/lab/LabFit.stories.tsx +219 -0
- package/src/lab/LabFullChrome.stories.tsx +1 -4
- package/src/lab/LabHeader.test.tsx +40 -9
- package/src/lab/LabHeader.tsx +7 -14
- package/src/lab/LabPalette.tsx +13 -26
- package/src/lab/LabRoot.test.tsx +56 -0
- package/src/lab/LabRoot.tsx +47 -0
- package/src/lab/LabShell.less +73 -15
- package/src/lab/LabShell.stories.tsx +50 -1
- package/src/lab/LabShell.tsx +18 -27
- package/src/lab/LabSwitcher.less +18 -11
- 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 -0
- package/src/lab/fitCheck.test.ts +57 -0
- package/src/lab/fitCheck.ts +125 -0
- package/src/lab/index.ts +2 -0
- package/src/layers/LayerList.stories.tsx +1 -1
- package/src/loupe/AGENTS.md +17 -5
- package/src/loupe/Loupe.less +1 -1
- package/src/loupe/LoupeGestures.tsx +67 -0
- package/src/loupe/TrialLoupe.tsx +43 -28
- package/src/loupe/canvasLens.ts +1 -1
- package/src/loupe/index.ts +4 -0
- package/src/loupe/loupeActions.ts +98 -0
- package/src/loupe/types.ts +1 -1
- package/src/loupe/useLoupe.test.tsx +60 -3
- package/src/loupe/useLoupe.ts +29 -49
- package/src/passthrough/weasel-ui.ts +9 -0
- package/src/primitives/FloatingPanel.stories.tsx +14 -2
- package/src/primitives/FloatingPanel.test.tsx +47 -16
- package/src/primitives/FloatingPanel.tsx +11 -25
- package/src/primitives/FpsMeter.stories.tsx +1 -1
- package/src/primitives/JobProgress.stories.tsx +1 -1
- 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.less +14 -0
- package/src/primitives/Split.test.tsx +133 -0
- package/src/primitives/Split.tsx +175 -0
- package/src/primitives/StatusBar.stories.tsx +1 -1
- package/src/primitives/Toolbar.stories.tsx +1 -1
- package/src/primitives/ZoomControl.tsx +5 -2
- package/src/primitives/index.ts +2 -0
- 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 +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 +1 -1
- 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 +169 -635
- package/src/state/store.ts +146 -163
- package/src/state/toolSlot.test.ts +1 -1
- package/src/state/types.ts +65 -17
- 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 +87 -3
- package/src/state/useTrialState.ts +9 -1
- 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 +1 -0
- package/src/surface/useSurfaceTile.ts +6 -5
- package/src/surface/useTiledSurface.test.tsx +231 -1
- package/src/surface/useTiledSurface.ts +76 -15
- package/src/theme/Interstellar.stories.tsx +9 -9
- package/src/theme/interstellar.test.ts +17 -6
- package/src/theme/interstellar.theme.json +180 -0
- package/src/theme/interstellar.ts +8 -5
- package/src/tools/labTool.ts +11 -0
- package/src/trial/Trial.annotations.persist.test.tsx +100 -58
- package/src/trial/Trial.annotations.test.tsx +59 -7
- package/src/trial/Trial.canvas.test.tsx +32 -0
- package/src/trial/Trial.config.test.tsx +75 -2
- package/src/trial/Trial.job.test.tsx +3 -3
- package/src/trial/Trial.less +5 -26
- package/src/trial/Trial.stories.tsx +5 -3
- package/src/trial/Trial.test.tsx +31 -4
- package/src/trial/Trial.trialId.test.tsx +23 -1
- package/src/trial/Trial.tsx +139 -52
- package/src/trial/TrialBody.tsx +17 -141
- package/src/trial/TrialChrome.tsx +24 -7
- package/src/trial/trialOps.test.ts +101 -1
- package/src/trial/trialOps.ts +67 -7
- 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-2P6PP5N4.js +0 -553
- package/dist/chunk-2P6PP5N4.js.map +0 -1
- package/dist/chunk-64ZCN3DA.js.map +0 -1
- package/dist/chunk-CTRKTLYZ.js.map +0 -1
- package/dist/chunk-E44SU6XS.js +0 -50
- package/dist/chunk-E44SU6XS.js.map +0 -1
- package/dist/chunk-FQMJUVHJ.js.map +0 -1
- package/dist/chunk-I6JCVE24.js.map +0 -1
- package/dist/chunk-MOM3GOVY.js.map +0 -1
- package/dist/chunk-SMHP6XZ4.js.map +0 -1
- package/dist/chunk-TJ7QY3OC.js.map +0 -1
- package/dist/chunk-U3IYHIAE.js +0 -343
- package/dist/chunk-U3IYHIAE.js.map +0 -1
- package/dist/chunk-W3ECWC2K.js +0 -7335
- package/dist/chunk-W3ECWC2K.js.map +0 -1
- package/src/theme/interstellar.tokens.json +0 -61
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { fireEvent, render, screen } from '@testing-library/react';
|
|
2
2
|
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
-
import type {
|
|
3
|
+
import type { RegionContribution, ToolSlotContext } from '../types';
|
|
4
4
|
import { PaletteRegion } from './PaletteRegion';
|
|
5
5
|
|
|
6
6
|
const Glyph = () => <svg />;
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
// The region reads a tool slot and nothing else, so the fixture is a real
|
|
9
|
+
// `ToolSlotContext` rather than a faked trial.
|
|
10
|
+
function ctxWith(activeToolId: string | null, setActiveTool = vi.fn()): ToolSlotContext {
|
|
11
|
+
return { activeToolId, setActiveTool };
|
|
10
12
|
}
|
|
11
13
|
|
|
12
|
-
function tool(id: string):
|
|
14
|
+
function tool(id: string): RegionContribution<ToolSlotContext> {
|
|
13
15
|
return { id, region: 'palette', item: { icon: Glyph, label: id } };
|
|
14
16
|
}
|
|
15
17
|
|
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
import { ToolButton, ToolGroup } from '../../passthrough/weasel-ui';
|
|
2
2
|
import { useRovingTabIndex } from '../../primitives/useRovingTabIndex';
|
|
3
|
-
import type {
|
|
3
|
+
import type { RegionContribution, ToolItem, ToolSlotContext, TrialChromeContext } from '../types';
|
|
4
4
|
|
|
5
5
|
/** Props for `<PaletteRegion>`. */
|
|
6
|
-
export interface PaletteRegionProps {
|
|
7
|
-
contributions: readonly
|
|
8
|
-
ctx:
|
|
6
|
+
export interface PaletteRegionProps<TCtx extends ToolSlotContext = TrialChromeContext> {
|
|
7
|
+
contributions: readonly RegionContribution<NoInfer<TCtx>>[];
|
|
8
|
+
ctx: TCtx;
|
|
9
|
+
/** Which region's items to lay out. The lab's rail names the same box
|
|
10
|
+
* `palette` too, so this only moves for a chrome that does not. */
|
|
11
|
+
region?: string;
|
|
9
12
|
}
|
|
10
13
|
|
|
11
|
-
/** A
|
|
12
|
-
* slot; this region only reflects it. */
|
|
13
|
-
export function PaletteRegion
|
|
14
|
+
/** A tool strip: a trial's, or the lab's. Selection lives in whichever tool
|
|
15
|
+
* slot the context carries; this region only reflects it. */
|
|
16
|
+
export function PaletteRegion<TCtx extends ToolSlotContext = TrialChromeContext>({
|
|
17
|
+
contributions,
|
|
18
|
+
ctx,
|
|
19
|
+
region = 'palette',
|
|
20
|
+
}: PaletteRegionProps<TCtx>) {
|
|
14
21
|
const { ref, onKeyDown } = useRovingTabIndex<HTMLDivElement>('vertical');
|
|
15
22
|
if (contributions.length === 0) return null;
|
|
16
23
|
return (
|
|
@@ -25,8 +32,8 @@ export function PaletteRegion({ contributions, ctx }: PaletteRegionProps) {
|
|
|
25
32
|
<ToolGroup orientation="vertical">
|
|
26
33
|
{contributions.map((c) => {
|
|
27
34
|
if (c.render) return <span key={c.id}>{c.render(ctx)}</span>;
|
|
28
|
-
if (c.region !==
|
|
29
|
-
const { icon: Icon, label, shortcut, disabled } = c.item;
|
|
35
|
+
if (c.region !== region || !c.item) return null;
|
|
36
|
+
const { icon: Icon, label, shortcut, disabled } = c.item as ToolItem;
|
|
30
37
|
return (
|
|
31
38
|
<ToolButton
|
|
32
39
|
key={c.id}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@weasel-js/forge';
|
|
2
|
+
import { expect } from '@weasel-js/forge/play';
|
|
3
|
+
import { LabRoot } from '../../lab/LabRoot';
|
|
4
|
+
import type { SidebarSlotContext } from '../types';
|
|
5
|
+
import { SidebarRegion } from './SidebarRegion';
|
|
6
|
+
|
|
7
|
+
const meta: Meta<typeof SidebarRegion> = {
|
|
8
|
+
title: 'labkit/Chrome/SidebarRegion',
|
|
9
|
+
component: SidebarRegion,
|
|
10
|
+
parameters: { layout: 'fullscreen' },
|
|
11
|
+
};
|
|
12
|
+
export default meta;
|
|
13
|
+
|
|
14
|
+
type Story = StoryObj<typeof SidebarRegion>;
|
|
15
|
+
|
|
16
|
+
const ctx: SidebarSlotContext = { collapsedSections: {}, setSectionCollapsed: () => {} };
|
|
17
|
+
|
|
18
|
+
// jsdom has no layout, so this runs in the browser story projects.
|
|
19
|
+
export const LastSectionFillsItsPane: Story = {
|
|
20
|
+
render: () => (
|
|
21
|
+
<LabRoot>
|
|
22
|
+
<div className="lk-lab__aside" data-testid="pane" style={{ height: 400 }}>
|
|
23
|
+
<SidebarRegion
|
|
24
|
+
ctx={ctx}
|
|
25
|
+
contributions={[
|
|
26
|
+
{ id: 'first', region: 'sidebar', item: { title: 'First', body: <p>one</p> } },
|
|
27
|
+
{ id: 'last', region: 'sidebar', item: { title: 'Last', body: <p>two</p> } },
|
|
28
|
+
]}
|
|
29
|
+
/>
|
|
30
|
+
</div>
|
|
31
|
+
</LabRoot>
|
|
32
|
+
),
|
|
33
|
+
play: async ({ canvasElement }) => {
|
|
34
|
+
const pane = canvasElement.querySelector('[data-testid="pane"]') as HTMLElement;
|
|
35
|
+
const sections = [...pane.querySelectorAll<HTMLElement>('.lk-sidebar-section')];
|
|
36
|
+
const first = sections[0] as HTMLElement;
|
|
37
|
+
const last = sections[sections.length - 1] as HTMLElement;
|
|
38
|
+
await expect(Math.round(last.getBoundingClientRect().bottom)).toBe(
|
|
39
|
+
Math.round(pane.getBoundingClientRect().bottom),
|
|
40
|
+
);
|
|
41
|
+
await expect(getComputedStyle(last).borderBottomWidth).toBe('0px');
|
|
42
|
+
await expect(getComputedStyle(first).borderBottomWidth).not.toBe('0px');
|
|
43
|
+
},
|
|
44
|
+
};
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
RegionContribution,
|
|
4
|
+
SidebarSection,
|
|
5
|
+
SidebarSlotContext,
|
|
6
|
+
TrialChromeContext,
|
|
7
|
+
} from '../types';
|
|
3
8
|
|
|
4
9
|
/** Props for `<SidebarRegion>`. */
|
|
5
|
-
export interface SidebarRegionProps {
|
|
6
|
-
contributions: readonly
|
|
7
|
-
ctx:
|
|
10
|
+
export interface SidebarRegionProps<TCtx extends SidebarSlotContext = TrialChromeContext> {
|
|
11
|
+
contributions: readonly RegionContribution<NoInfer<TCtx>>[];
|
|
12
|
+
ctx: TCtx;
|
|
13
|
+
/** Which region's items to lay out. */
|
|
14
|
+
region?: string;
|
|
8
15
|
}
|
|
9
16
|
|
|
10
17
|
function Section({
|
|
@@ -48,17 +55,22 @@ function Section({
|
|
|
48
55
|
);
|
|
49
56
|
}
|
|
50
57
|
|
|
51
|
-
/** Lays
|
|
52
|
-
* A section the trial has torn out is not rendered
|
|
53
|
-
* body into the workspace instead.
|
|
54
|
-
|
|
58
|
+
/** Lays `sidebar` contributions out as titled, collapsible sections — a
|
|
59
|
+
* trial's, or the lab's. A section the trial has torn out is not rendered
|
|
60
|
+
* here — `Trial` portals its body into the workspace instead. A context with
|
|
61
|
+
* no `undockPanel` offers no tear-out. */
|
|
62
|
+
export function SidebarRegion<TCtx extends SidebarSlotContext = TrialChromeContext>({
|
|
63
|
+
contributions,
|
|
64
|
+
ctx,
|
|
65
|
+
region = 'sidebar',
|
|
66
|
+
}: SidebarRegionProps<TCtx>) {
|
|
55
67
|
if (contributions.length === 0) return null;
|
|
56
68
|
return (
|
|
57
69
|
<>
|
|
58
70
|
{contributions.map((c) => {
|
|
59
71
|
if (c.render) return <div key={c.id}>{c.render(ctx)}</div>;
|
|
60
|
-
if (c.region !==
|
|
61
|
-
if (ctx.undockedPanels
|
|
72
|
+
if (c.region !== region || !c.item) return null;
|
|
73
|
+
if (ctx.undockedPanels?.includes(c.id)) return null;
|
|
62
74
|
const item = c.item as SidebarSection;
|
|
63
75
|
return (
|
|
64
76
|
<Section
|
|
@@ -67,9 +79,9 @@ export function SidebarRegion({ contributions, ctx }: SidebarRegionProps) {
|
|
|
67
79
|
collapsed={ctx.collapsedSections[c.id] ?? item.defaultCollapsed ?? false}
|
|
68
80
|
onCollapsedChange={(next) => ctx.setSectionCollapsed(c.id, next)}
|
|
69
81
|
onUndock={
|
|
70
|
-
item.undockable
|
|
71
|
-
?
|
|
72
|
-
:
|
|
82
|
+
ctx.undockPanel && item.undockable !== false
|
|
83
|
+
? () => ctx.undockPanel?.(c.id, item.undockAs ?? 'tile')
|
|
84
|
+
: undefined
|
|
73
85
|
}
|
|
74
86
|
>
|
|
75
87
|
{item.body}
|
|
@@ -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,19 @@ function groupsOf(contributions: readonly TrialContribution[]): Group[] {
|
|
|
35
39
|
return groups;
|
|
36
40
|
}
|
|
37
41
|
|
|
38
|
-
function renderEntry(c:
|
|
42
|
+
function renderEntry<TCtx>(c: RegionContribution<TCtx>, ctx: TCtx, region: string): ReactNode {
|
|
39
43
|
if (c.render) return <span key={c.id}>{c.render(ctx)}</span>;
|
|
40
|
-
if (c.region !==
|
|
41
|
-
const {
|
|
44
|
+
if (c.region !== region || !c.item) return null;
|
|
45
|
+
const {
|
|
46
|
+
icon: Icon,
|
|
47
|
+
label,
|
|
48
|
+
shortcut,
|
|
49
|
+
disabled,
|
|
50
|
+
danger,
|
|
51
|
+
showLabel,
|
|
52
|
+
pressed,
|
|
53
|
+
onActivate,
|
|
54
|
+
} = c.item as ToolbarItem<TCtx>;
|
|
42
55
|
return (
|
|
43
56
|
<Toolbar.Button
|
|
44
57
|
key={c.id}
|
|
@@ -56,15 +69,20 @@ function renderEntry(c: TrialContribution, ctx: TrialChromeContext): ReactNode {
|
|
|
56
69
|
);
|
|
57
70
|
}
|
|
58
71
|
|
|
59
|
-
/** Lays a
|
|
60
|
-
export function ToolbarRegion
|
|
72
|
+
/** Lays a bar's contributions out, grouped by their `group`. */
|
|
73
|
+
export function ToolbarRegion<TCtx = TrialChromeContext>({
|
|
74
|
+
contributions,
|
|
75
|
+
ctx,
|
|
76
|
+
region = 'toolbar',
|
|
77
|
+
label = 'Trial actions',
|
|
78
|
+
}: ToolbarRegionProps<TCtx>) {
|
|
61
79
|
if (contributions.length === 0) return null;
|
|
62
80
|
const groups = groupsOf(contributions);
|
|
63
81
|
return (
|
|
64
|
-
<Toolbar aria-label=
|
|
82
|
+
<Toolbar aria-label={label}>
|
|
65
83
|
{groups.map((g) => (
|
|
66
84
|
<Toolbar.Group key={g.key} end={g.end} aria-label={g.key}>
|
|
67
|
-
{g.entries.map((c) => renderEntry(c, ctx))}
|
|
85
|
+
{g.entries.map((c) => renderEntry(c, ctx, region))}
|
|
68
86
|
</Toolbar.Group>
|
|
69
87
|
))}
|
|
70
88
|
</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;
|
|
@@ -133,6 +165,9 @@ export interface TrialChromeContext {
|
|
|
133
165
|
* `config`, since a builder schema has no `ConfigField[]` form. */
|
|
134
166
|
configFields: ConfigField[];
|
|
135
167
|
config: unknown;
|
|
168
|
+
/** The trial's unpinned paths. `config` stays raw, so a control drawn from
|
|
169
|
+
* it sits at the value un-pinning writes back. */
|
|
170
|
+
auto: ReadonlySet<string>;
|
|
136
171
|
setConfig: (key: string, value: unknown) => void;
|
|
137
172
|
|
|
138
173
|
/** Which of this trial's collapsible sections are folded. A sidebar
|
|
@@ -157,9 +192,4 @@ export interface TrialChromeContext {
|
|
|
157
192
|
clone: () => void;
|
|
158
193
|
reset: () => void;
|
|
159
194
|
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
195
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { auto, isAuto } from './auto';
|
|
3
|
+
|
|
4
|
+
describe('auto', () => {
|
|
5
|
+
it('is a symbol distinct from every ordinary config value', () => {
|
|
6
|
+
expect(typeof auto).toBe('symbol');
|
|
7
|
+
expect(isAuto(auto)).toBe(true);
|
|
8
|
+
for (const v of [undefined, null, 0, '', 'auto', false, Symbol('auto'), {}]) {
|
|
9
|
+
expect(isAuto(v)).toBe(false);
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Written at a config path to mean "do not pin this — let the instrument
|
|
3
|
+
* decide". Accepted by `setConfig`, by a trial's `configSeed`, and by
|
|
4
|
+
* `.initial()` in a schema. It is normalized into the trial's set of unpinned
|
|
5
|
+
* paths on the way into the store and is never itself stored, so a serialized
|
|
6
|
+
* trial contains no sentinel and the last pinned value survives.
|
|
7
|
+
*/
|
|
8
|
+
export const auto: unique symbol = Symbol('weasel.auto');
|
|
9
|
+
|
|
10
|
+
/** The type of the `auto` sentinel, for widening a value parameter. */
|
|
11
|
+
export type Auto = typeof auto;
|
|
12
|
+
|
|
13
|
+
export function isAuto(value: unknown): value is Auto {
|
|
14
|
+
return value === auto;
|
|
15
|
+
}
|
|
@@ -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
|
+
});
|