@weasel-js/labkit 1.6.1 → 1.7.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 +3 -3
- package/dist/_dts/{CanvasStackContext-Wo6OwIVM.d.ts → CanvasStackContext-Cjy6I-Iv.d.ts} +4 -1
- package/dist/_dts/{Stage-DWBBi8IU.d.ts → Stage-D6t7o0pF.d.ts} +59 -18
- package/dist/_dts/{index-CKyX0ME_.d.ts → index-BTmaRy6a.d.ts} +10 -6
- package/dist/_dts/{types-B7yBTTF9.d.ts → index-DCMA4Fy1.d.ts} +59 -113
- package/dist/_dts/{types-DTuViBvc.d.ts → types-BUBSIZuV.d.ts} +18 -8
- package/dist/_dts/{useTrialState-CMaV6UD4.d.ts → useTrialState-Bh8bzFgE.d.ts} +1 -1
- package/dist/canvas/index.d.ts +8 -6
- package/dist/canvas/index.js +6 -6
- package/dist/chrome/index.d.ts +6 -4
- package/dist/chrome/index.js +11 -10
- package/dist/chunk-2ES4W47N.js +8 -0
- package/dist/chunk-2ES4W47N.js.map +1 -0
- package/dist/{chunk-WPLAANH4.js → chunk-2N7UBSS2.js} +12 -12
- package/dist/chunk-2N7UBSS2.js.map +1 -0
- package/dist/chunk-4KSC7NXL.js +15491 -0
- package/dist/chunk-4KSC7NXL.js.map +1 -0
- package/dist/{chunk-SLAQPUI4.js → chunk-CEGFXTBD.js} +10 -63
- package/dist/chunk-CEGFXTBD.js.map +1 -0
- package/dist/chunk-CZZPXUOU.js +3 -0
- package/dist/{chunk-SBXNQW4G.js.map → chunk-CZZPXUOU.js.map} +1 -1
- package/dist/{chunk-QGMOA2YE.js → chunk-E23Q6A3Y.js} +296 -104
- package/dist/chunk-E23Q6A3Y.js.map +1 -0
- package/dist/{chunk-GHGO52SO.js → chunk-E6NARGU7.js} +5 -5
- package/dist/{chunk-GHGO52SO.js.map → chunk-E6NARGU7.js.map} +1 -1
- package/dist/{chunk-IJJUK4FU.js → chunk-EESJUSYR.js} +5 -6
- package/dist/chunk-EESJUSYR.js.map +1 -0
- package/dist/{chunk-HXXTULDN.js → chunk-EFXRM6XQ.js} +8 -11
- package/dist/chunk-EFXRM6XQ.js.map +1 -0
- package/dist/chunk-F4ACIK7Z.js +210 -0
- package/dist/chunk-F4ACIK7Z.js.map +1 -0
- package/dist/{chunk-6FLXL3I7.js → chunk-FB4MIZOO.js} +77 -45
- package/dist/chunk-FB4MIZOO.js.map +1 -0
- package/dist/{chunk-AWQG7FCC.js → chunk-FG3EXZDV.js} +3 -3
- package/dist/{chunk-AWQG7FCC.js.map → chunk-FG3EXZDV.js.map} +1 -1
- package/dist/{chunk-4TT3PNPR.js → chunk-GAHELLJT.js} +95 -95
- package/dist/chunk-GAHELLJT.js.map +1 -0
- package/dist/{chunk-HKV7LRHY.js → chunk-IBVRMHW5.js} +5 -5
- package/dist/{chunk-HKV7LRHY.js.map → chunk-IBVRMHW5.js.map} +1 -1
- package/dist/{chunk-ASOK76ZH.js → chunk-JFOJESJF.js} +72 -29
- package/dist/chunk-JFOJESJF.js.map +1 -0
- package/dist/{chunk-RWNDDKOH.js → chunk-PMLZP7RT.js} +18 -15
- package/dist/chunk-PMLZP7RT.js.map +1 -0
- package/dist/{chunk-TJ77A4A3.js → chunk-READTDQX.js} +5 -9
- package/dist/chunk-READTDQX.js.map +1 -0
- package/dist/{chunk-JVBYV6TL.js → chunk-ROGJWDQA.js} +3 -3
- package/dist/{chunk-JVBYV6TL.js.map → chunk-ROGJWDQA.js.map} +1 -1
- package/dist/{chunk-CGHF6RZV.js → chunk-T6DLGEJB.js} +18 -18
- package/dist/chunk-T6DLGEJB.js.map +1 -0
- package/dist/{chunk-24W5VEJW.js → chunk-ULYYNWGP.js} +2 -2
- package/dist/{chunk-24W5VEJW.js.map → chunk-ULYYNWGP.js.map} +1 -1
- package/dist/{chunk-DWMSB4CA.js → chunk-VYVGVBGY.js} +95 -32
- package/dist/chunk-VYVGVBGY.js.map +1 -0
- package/dist/{chunk-IRTGUQ5O.js → chunk-WLMDTI2Q.js} +16 -6
- package/dist/chunk-WLMDTI2Q.js.map +1 -0
- package/dist/{chunk-NEXR37YJ.js → chunk-XE5HJOPL.js} +6 -8
- package/dist/chunk-XE5HJOPL.js.map +1 -0
- package/dist/chunk-XVUKDSYV.js +38 -0
- package/dist/chunk-XVUKDSYV.js.map +1 -0
- package/dist/{chunk-O6FFFOHS.js → chunk-YCFKW5RX.js} +16 -8
- package/dist/chunk-YCFKW5RX.js.map +1 -0
- package/dist/{chunk-CQLQPQ4P.js → chunk-ZQIHJXP4.js} +2 -2
- package/dist/chunk-ZQIHJXP4.js.map +1 -0
- package/dist/config/index.d.ts +28 -13
- package/dist/config/index.js +4 -4
- package/dist/controls/index.d.ts +4 -7
- package/dist/controls/index.js +6 -5
- package/dist/dragdrop/index.d.ts +23 -3
- package/dist/dragdrop/index.js +4 -3
- package/dist/fonts/oswald-tabular-digits.woff2 +0 -0
- package/dist/index.d.ts +72 -487
- package/dist/index.js +603 -441
- package/dist/index.js.map +1 -1
- package/dist/job/index.d.ts +8 -0
- package/dist/job/index.js +1 -1
- package/dist/layers/index.d.ts +6 -5
- package/dist/layers/index.js +3 -2
- package/dist/loupe/index.d.ts +100 -30
- package/dist/loupe/index.js +585 -6
- package/dist/loupe/index.js.map +1 -1
- package/dist/overview/index.d.ts +4 -2
- package/dist/overview/index.js +20 -18
- package/dist/overview/index.js.map +1 -1
- package/dist/passthrough/weasel-canvas.d.ts +2 -0
- package/dist/passthrough/weasel-ui.d.ts +2 -2701
- package/dist/passthrough/weasel-ui.js +4 -3
- package/dist/primitives/index.d.ts +37 -2
- package/dist/primitives/index.js +9 -8
- package/dist/state/index.d.ts +7 -5
- package/dist/state/index.js +6 -6
- package/dist/styles.css +150 -126
- package/dist/surface/index.d.ts +10 -2
- package/dist/surface/index.js +1 -1
- package/dist/undo/index.d.ts +5 -3
- package/package.json +14 -13
- package/src/annotations/AnnotationOverlay.abandon.test.tsx +143 -0
- package/src/annotations/AnnotationOverlay.tsx +52 -33
- package/src/annotations/Annotations.less +0 -10
- package/src/annotations/Annotations.overlay.test.tsx +31 -0
- package/src/annotations/MarkList.tsx +4 -8
- package/src/annotations/capture.browser.test.ts +41 -0
- package/src/annotations/capture.ts +14 -7
- package/src/annotations/drawOne.ts +2 -2
- package/src/annotations/frac.test.ts +44 -1
- package/src/annotations/frac.ts +46 -18
- package/src/annotations/index.ts +1 -0
- package/src/annotations/paint.test.ts +76 -46
- package/src/annotations/paint.ts +65 -43
- package/src/annotations/store.test.ts +212 -2
- package/src/annotations/store.ts +130 -27
- package/src/annotations/svgNodes.test.ts +28 -19
- package/src/annotations/svgNodes.ts +6 -6
- package/src/annotations/types.ts +29 -10
- package/src/canvas/AGENTS.md +1 -0
- package/src/canvas/CameraInput.test.tsx +30 -0
- package/src/canvas/CameraInput.tsx +60 -48
- package/src/canvas/CanvasStack.tsx +11 -6
- package/src/canvas/CanvasStackContext.ts +4 -1
- package/src/canvas/LinkedCursor.less +1 -1
- package/src/canvas/Stage.test.tsx +33 -4
- package/src/canvas/Stage.tsx +9 -4
- package/src/canvas/cameraRegistry.test.ts +55 -0
- package/src/canvas/cameraRegistry.ts +87 -0
- package/src/canvas/cameraZoom.test.ts +46 -0
- package/src/canvas/cameraZoom.ts +24 -0
- package/src/canvas/index.ts +14 -2
- package/src/canvas/sharedScope.test.tsx +94 -0
- package/src/canvas/worldSpec.ts +3 -1
- package/src/chrome/ChromeRegions.stories.tsx +1 -0
- package/src/chrome/builtins.test.ts +7 -6
- package/src/chrome/builtins.tsx +5 -1
- package/src/chrome/index.ts +1 -1
- package/src/chrome/merge.test.ts +5 -0
- package/src/chrome/merge.ts +4 -2
- package/src/chrome/regions/PaletteRegion.less +0 -7
- package/src/chrome/regions/PaletteRegion.test.tsx +2 -3
- package/src/chrome/regions/PaletteRegion.tsx +3 -4
- package/src/chrome/regions/SidebarRegion.less +12 -2
- package/src/chrome/regions/ViewportRegion.browser.test.less +6 -0
- package/src/chrome/regions/ViewportRegion.browser.test.tsx +38 -0
- package/src/chrome/regions/ViewportRegion.less +6 -0
- package/src/chrome/regions/ViewportRegion.tsx +10 -4
- package/src/chrome/types.ts +4 -2
- package/src/config/auto.ts +1 -0
- package/src/config/builder.test.ts +2 -1
- package/src/config/builder.ts +18 -5
- package/src/config/fromConfigField.test.ts +14 -0
- package/src/config/fromConfigField.ts +18 -4
- package/src/config/path.ts +2 -5
- package/src/config/resolve.ts +4 -0
- package/src/config/types.ts +14 -9
- package/src/controls/ControlPanel.less +1 -1
- package/src/controls/ControlPanel.stories.tsx +3 -2
- package/src/controls/ControlPanel.test.tsx +98 -11
- package/src/controls/ControlPanel.tsx +61 -2
- package/src/dragdrop/DragDropRuntime.tsx +22 -13
- package/src/dragdrop/DragGhost.tsx +3 -0
- package/src/dragdrop/Palette.tsx +2 -0
- package/src/index.test.ts +5 -0
- package/src/index.ts +16 -2
- package/src/instrument/types.ts +0 -6
- package/src/job/useJob.ts +14 -9
- package/src/lab/Lab.tsx +88 -73
- package/src/lab/{LabFit.stories.less → LabFit.browser.test.less} +1 -1
- package/src/lab/{LabFit.stories.tsx → LabFit.browser.test.tsx} +41 -63
- package/src/lab/LabFullChrome.stories.tsx +1 -0
- package/src/lab/LabHeader.test.tsx +24 -14
- package/src/lab/LabHeader.tsx +11 -12
- package/src/lab/LabShell.less +9 -17
- package/src/lab/LabSwitcher.browser.test.tsx +27 -0
- package/src/lab/LabSwitcher.less +8 -6
- package/src/lab/LabZoom.test.tsx +223 -0
- package/src/lab/LabZoom.tsx +170 -0
- package/src/lab/Workspace.tsx +48 -52
- package/src/lab/index.ts +1 -0
- package/src/loupe/AGENTS.md +28 -15
- package/src/loupe/CanvasLoupe.tsx +2 -3
- package/src/loupe/DomLoupe.tsx +1 -7
- package/src/loupe/Loupe.less +16 -0
- package/src/loupe/LoupeGestures.tsx +1 -1
- package/src/loupe/TrialLoupe.tsx +120 -57
- package/src/loupe/index.ts +1 -6
- package/src/loupe/types.test.ts +3 -3
- package/src/loupe/types.ts +24 -27
- package/src/loupe/useLoupe.test.tsx +29 -4
- package/src/loupe/useLoupe.ts +20 -17
- package/src/overview/OverviewMarks.tsx +12 -4
- package/src/overview/TrialOverview.tsx +10 -8
- package/src/passthrough/weasel-ui.test.ts +2 -0
- package/src/passthrough/weasel-ui.ts +17 -5
- package/src/primitives/FloatingPanel.test.tsx +11 -0
- package/src/primitives/FloatingPanel.tsx +8 -7
- package/src/primitives/JobProgress.less +2 -1
- package/src/primitives/Legend.stories.tsx +1 -0
- package/src/primitives/Readout.browser.test.tsx +137 -0
- package/src/primitives/Readout.less +12 -0
- package/src/primitives/Readout.stories.tsx +58 -0
- package/src/primitives/Readout.test.tsx +93 -0
- package/src/primitives/Readout.tsx +56 -0
- package/src/primitives/Split.tsx +69 -41
- package/src/primitives/Toolbar.tsx +3 -3
- package/src/primitives/ZoomControl.less +1 -2
- package/src/primitives/index.ts +2 -0
- package/src/specimen/Specimen.stories.tsx +1 -0
- package/src/specimen/Specimen.tsx +2 -3
- package/src/state/Persistence.browser.test.tsx +47 -0
- package/src/state/store.test.ts +5 -1
- package/src/state/undock.ts +3 -0
- package/src/state/useOpenOnce.ts +20 -16
- package/src/state/usePersistedState.test.tsx +1 -1
- package/src/state/view.ts +1 -0
- package/src/styles.less +1 -0
- package/src/surface/index.ts +1 -1
- package/src/surface/useTiledSurface.test.tsx +14 -0
- package/src/surface/useTiledSurface.ts +11 -3
- package/src/theme/Interstellar.stories.tsx +3 -1
- package/src/theme/base.less +21 -47
- package/src/theme/interstellar.test.ts +1 -1
- package/src/theme/interstellar.theme.json +10 -3
- package/src/trial/Trial.annotations.test.tsx +73 -0
- package/src/trial/Trial.less +0 -29
- package/src/trial/Trial.loupe.test.tsx +28 -11
- package/src/trial/Trial.tsx +85 -67
- package/src/trial/TrialChrome.tsx +27 -6
- package/src/trial/index.ts +2 -0
- package/src/trial/loupeSwitch.ts +15 -0
- package/dist/_dts/LayerList.d-BoELAMsM.d.ts +0 -83
- package/dist/_dts/PrefsRow.d-DFpPIeIC.d.ts +0 -39
- package/dist/_dts/PropertyField.d-D0ndfIIt.d.ts +0 -404
- package/dist/_dts/Select.d-Cr8-zoup.d.ts +0 -569
- package/dist/chunk-4TT3PNPR.js.map +0 -1
- package/dist/chunk-6FLXL3I7.js.map +0 -1
- package/dist/chunk-ASOK76ZH.js.map +0 -1
- package/dist/chunk-CGHF6RZV.js.map +0 -1
- package/dist/chunk-CQLQPQ4P.js.map +0 -1
- package/dist/chunk-DWMSB4CA.js.map +0 -1
- package/dist/chunk-HXXTULDN.js.map +0 -1
- package/dist/chunk-IJJUK4FU.js.map +0 -1
- package/dist/chunk-IRTGUQ5O.js.map +0 -1
- package/dist/chunk-KHK5FGER.js +0 -145
- package/dist/chunk-KHK5FGER.js.map +0 -1
- package/dist/chunk-NEXR37YJ.js.map +0 -1
- package/dist/chunk-O6FFFOHS.js.map +0 -1
- package/dist/chunk-QGMOA2YE.js.map +0 -1
- package/dist/chunk-RWNDDKOH.js.map +0 -1
- package/dist/chunk-S4NCI32V.js +0 -549
- package/dist/chunk-S4NCI32V.js.map +0 -1
- package/dist/chunk-SBXNQW4G.js +0 -3
- package/dist/chunk-SLAQPUI4.js.map +0 -1
- package/dist/chunk-TJ77A4A3.js.map +0 -1
- package/dist/chunk-TYN6MLXX.js +0 -13864
- package/dist/chunk-TYN6MLXX.js.map +0 -1
- package/dist/chunk-WPLAANH4.js.map +0 -1
- package/src/primitives/useRovingTabIndex.test.ts +0 -48
- package/src/primitives/useRovingTabIndex.ts +0 -88
- package/src/state/Persistence.stories.tsx +0 -75
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import '@weasel-js/theme/tokens.css';
|
|
2
|
+
import '@weasel-js/theme/fonts.css';
|
|
3
|
+
import '../styles.less';
|
|
4
|
+
import { cleanup, render } from '@testing-library/react';
|
|
5
|
+
import { afterEach, expect, test } from 'vitest';
|
|
6
|
+
import { Readout, type ReadoutRow } from './Readout';
|
|
7
|
+
|
|
8
|
+
function must<T>(el: T | null): T {
|
|
9
|
+
if (el == null) throw new Error('expected an element');
|
|
10
|
+
return el;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Right alignment, the figure column's minimum width and a height that holds
|
|
14
|
+
// while values come and go are all layout, which only a real browser has.
|
|
15
|
+
|
|
16
|
+
afterEach(cleanup);
|
|
17
|
+
|
|
18
|
+
const FIGURE_SPACE = ' ';
|
|
19
|
+
|
|
20
|
+
const rows = (withValues: boolean): ReadoutRow[] => [
|
|
21
|
+
{
|
|
22
|
+
label: 'Lock margin',
|
|
23
|
+
value: withValues ? <span data-v>locked +47.9°</span> : undefined,
|
|
24
|
+
status: 'success',
|
|
25
|
+
},
|
|
26
|
+
{ label: 'Score', value: withValues ? <span data-v>{`${FIGURE_SPACE}0.145`}</span> : undefined },
|
|
27
|
+
{ label: 'Error', value: withValues ? <span data-v>12.345</span> : undefined, status: 'danger' },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const box = (el: Element) => el.getBoundingClientRect();
|
|
31
|
+
|
|
32
|
+
test('values sit right-aligned at the edge of the list', () => {
|
|
33
|
+
const { container } = render(
|
|
34
|
+
<div style={{ width: 320 }}>
|
|
35
|
+
<Readout rows={rows(true)} />
|
|
36
|
+
</div>,
|
|
37
|
+
);
|
|
38
|
+
const dl = must(container.querySelector('dl'));
|
|
39
|
+
const values = [...container.querySelectorAll('[data-v]')];
|
|
40
|
+
expect(values).toHaveLength(3);
|
|
41
|
+
for (const v of values) expect(Math.abs(box(v).right - box(dl).right)).toBeLessThan(0.5);
|
|
42
|
+
expect(box(values[0]).width).not.toBeCloseTo(box(values[2]).width, 0);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('figures are equal width, so padding lines a column up on the decimal point', async () => {
|
|
46
|
+
// 1 and 8 differ in width wherever digits are proportional, as they are in
|
|
47
|
+
// the theme's display face, which has no tabular figures to switch on.
|
|
48
|
+
const { container } = render(
|
|
49
|
+
<div style={{ width: 320, fontFamily: 'var(--wzl-font-display)' }}>
|
|
50
|
+
<Readout
|
|
51
|
+
rows={[
|
|
52
|
+
{ label: 'a', value: <span data-v>{`${FIGURE_SPACE}1.111`}</span> },
|
|
53
|
+
{ label: 'b', value: <span data-v>88.888</span> },
|
|
54
|
+
]}
|
|
55
|
+
/>
|
|
56
|
+
</div>,
|
|
57
|
+
);
|
|
58
|
+
await document.fonts.ready;
|
|
59
|
+
const [narrow, wide] = [...container.querySelectorAll('[data-v]')];
|
|
60
|
+
expect(Math.abs(box(narrow).width - box(wide).width)).toBeLessThan(0.5);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('the value column holds a minimum width in a shrink-wrapped readout', () => {
|
|
64
|
+
const { container } = render(
|
|
65
|
+
<div style={{ width: 'max-content' }}>
|
|
66
|
+
<Readout rows={[{ label: 'n', value: <span data-v>1</span> }]} />
|
|
67
|
+
</div>,
|
|
68
|
+
);
|
|
69
|
+
const digit = box(must(container.querySelector('[data-v]'))).width;
|
|
70
|
+
const dd = box(must(container.querySelector('dd'))).width;
|
|
71
|
+
expect(dd).toBeGreaterThanOrEqual(8 * digit - 0.5);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('rows keep their place and the list its height while values are absent', () => {
|
|
75
|
+
const { container, rerender } = render(
|
|
76
|
+
<div style={{ width: 320 }}>
|
|
77
|
+
<Readout rows={rows(true)} />
|
|
78
|
+
</div>,
|
|
79
|
+
);
|
|
80
|
+
const dl = () => must(container.querySelector('dl'));
|
|
81
|
+
const rowHeights = () => [...dl().children].map((r) => box(r).height);
|
|
82
|
+
const withValues = { list: box(dl()).height, rows: rowHeights() };
|
|
83
|
+
rerender(
|
|
84
|
+
<div style={{ width: 320 }}>
|
|
85
|
+
<Readout rows={rows(false)} />
|
|
86
|
+
</div>,
|
|
87
|
+
);
|
|
88
|
+
expect(dl().querySelectorAll('[data-empty]')).toHaveLength(3);
|
|
89
|
+
expect(box(dl()).height).toBeCloseTo(withValues.list, 1);
|
|
90
|
+
expect(rowHeights()).toEqual(withValues.rows);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('a status dot is drawn before its value, on the same line', () => {
|
|
94
|
+
const { container } = render(
|
|
95
|
+
<div style={{ width: 320 }}>
|
|
96
|
+
<Readout rows={rows(true)} />
|
|
97
|
+
</div>,
|
|
98
|
+
);
|
|
99
|
+
const dd = must(container.querySelector('dd'));
|
|
100
|
+
const dot = box(must(dd.querySelector('[aria-hidden]')));
|
|
101
|
+
const value = box(must(dd.querySelector('[data-v]')));
|
|
102
|
+
expect(dot.width).toBeGreaterThan(0);
|
|
103
|
+
expect(dot.right).toBeLessThanOrEqual(value.left);
|
|
104
|
+
expect(dot.top).toBeGreaterThanOrEqual(value.top - 1);
|
|
105
|
+
expect(dot.bottom).toBeLessThanOrEqual(value.bottom + 1);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('a text row wraps inside a figures readout instead of overflowing it', () => {
|
|
109
|
+
const { container } = render(
|
|
110
|
+
<div style={{ width: 300, fontFamily: 'var(--wzl-font-display)' }}>
|
|
111
|
+
<Readout
|
|
112
|
+
rows={[
|
|
113
|
+
{ label: 'Score', value: <span data-v>0.145</span> },
|
|
114
|
+
{
|
|
115
|
+
label: 'Sides',
|
|
116
|
+
value: (
|
|
117
|
+
<span data-t>
|
|
118
|
+
1 flat, 2 tab, 3 blank, 4 blank, 5 flat, 6 tab, 7 blank, 8 blank, 9 flat
|
|
119
|
+
</span>
|
|
120
|
+
),
|
|
121
|
+
values: 'text',
|
|
122
|
+
},
|
|
123
|
+
]}
|
|
124
|
+
/>
|
|
125
|
+
</div>,
|
|
126
|
+
);
|
|
127
|
+
const dl = must(container.querySelector('dl'));
|
|
128
|
+
expect(dl.scrollWidth).toBeLessThanOrEqual(dl.clientWidth);
|
|
129
|
+
const text = must(container.querySelector('[data-t]'));
|
|
130
|
+
expect(box(text).right).toBeLessThanOrEqual(box(dl).right + 0.5);
|
|
131
|
+
expect(box(text).height).toBeGreaterThan(
|
|
132
|
+
box(must(container.querySelector('[data-v]'))).height * 1.5,
|
|
133
|
+
);
|
|
134
|
+
expect(
|
|
135
|
+
Math.abs(box(must(container.querySelector('[data-v]'))).right - box(dl).right),
|
|
136
|
+
).toBeLessThan(0.5);
|
|
137
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@weasel-js/forge';
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { Readout } from './Readout';
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof Readout> = {
|
|
6
|
+
title: 'labkit/Primitives/Readout',
|
|
7
|
+
component: Readout,
|
|
8
|
+
};
|
|
9
|
+
export default meta;
|
|
10
|
+
|
|
11
|
+
type Story = StoryObj<typeof Readout>;
|
|
12
|
+
|
|
13
|
+
const FIGURE_SPACE = ' ';
|
|
14
|
+
|
|
15
|
+
export const Measurements: Story = {
|
|
16
|
+
tags: ['gallery'],
|
|
17
|
+
args: {
|
|
18
|
+
rows: [
|
|
19
|
+
{ label: 'Lock margin', value: 'locked +47.9°', status: 'success' },
|
|
20
|
+
{ label: 'Score', value: `${FIGURE_SPACE}0.145` },
|
|
21
|
+
{ label: 'Residual', value: `${FIGURE_SPACE}3.020` },
|
|
22
|
+
{ label: 'Overshoot', value: 'over 12.345', status: 'danger' },
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const WithTitleAndNote: Story = {
|
|
28
|
+
args: {
|
|
29
|
+
title: 'Fit',
|
|
30
|
+
rows: [
|
|
31
|
+
{ label: 'Score', value: '0.145' },
|
|
32
|
+
{ label: 'Iterations', value: `${FIGURE_SPACE}${FIGURE_SPACE}42` },
|
|
33
|
+
],
|
|
34
|
+
children: 'Scores under 0.2 converge within one pass.',
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** Values vanish while the cursor is off the picture; the rows hold their
|
|
39
|
+
* place and the readout its height. */
|
|
40
|
+
export const ValuesComeAndGo: Story = {
|
|
41
|
+
render: () => {
|
|
42
|
+
const [on, setOn] = useState(true);
|
|
43
|
+
return (
|
|
44
|
+
<div>
|
|
45
|
+
<button type="button" onClick={() => setOn((v) => !v)}>
|
|
46
|
+
{on ? 'Hide values' : 'Show values'}
|
|
47
|
+
</button>
|
|
48
|
+
<Readout
|
|
49
|
+
rows={[
|
|
50
|
+
{ label: 'x', value: on ? `${FIGURE_SPACE}12.50` : undefined },
|
|
51
|
+
{ label: 'y', value: on ? '-3.75' : undefined },
|
|
52
|
+
{ label: 'Hit', value: on ? 'inside' : undefined, status: on ? 'success' : undefined },
|
|
53
|
+
]}
|
|
54
|
+
/>
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
},
|
|
58
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import * as labkit from '../index';
|
|
4
|
+
import { Readout } from './Readout';
|
|
5
|
+
|
|
6
|
+
function must<T>(el: T | null): T {
|
|
7
|
+
if (el == null) throw new Error('expected an element');
|
|
8
|
+
return el;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe('Readout', () => {
|
|
12
|
+
it('renders its rows as one <dl>, in order, each a label and a value', () => {
|
|
13
|
+
const { container } = render(
|
|
14
|
+
<Readout
|
|
15
|
+
rows={[
|
|
16
|
+
{ label: 'Lock margin', value: '+47.9°' },
|
|
17
|
+
{ label: 'Score', value: '0.145' },
|
|
18
|
+
]}
|
|
19
|
+
/>,
|
|
20
|
+
);
|
|
21
|
+
const dl = must(container.querySelector('dl'));
|
|
22
|
+
expect([...dl.querySelectorAll('dt')].map((d) => d.textContent)).toEqual([
|
|
23
|
+
'Lock margin',
|
|
24
|
+
'Score',
|
|
25
|
+
]);
|
|
26
|
+
expect([...dl.querySelectorAll('dd')].map((d) => d.textContent)).toEqual(['+47.9°', '0.145']);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('sets its values as figures', () => {
|
|
30
|
+
const { container } = render(<Readout rows={[{ label: 'a', value: '1' }]} />);
|
|
31
|
+
expect(must(container.querySelector('dl')).getAttribute('data-values')).toBe('figures');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('keeps a row whose value is absent, showing a dash', () => {
|
|
35
|
+
const { container } = render(
|
|
36
|
+
<Readout
|
|
37
|
+
rows={[
|
|
38
|
+
{ label: 'Lock margin', value: undefined },
|
|
39
|
+
{ label: 'Score', value: '0.145' },
|
|
40
|
+
]}
|
|
41
|
+
/>,
|
|
42
|
+
);
|
|
43
|
+
expect(container.querySelectorAll('dl > div')).toHaveLength(2);
|
|
44
|
+
expect(must(container.querySelector('dd')).textContent).toBe('–');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('marks a row with its status', () => {
|
|
48
|
+
const { container } = render(
|
|
49
|
+
<Readout rows={[{ label: 'Lock', value: 'locked', status: 'success' }]} />,
|
|
50
|
+
);
|
|
51
|
+
expect(must(container.querySelector('dl > div')).getAttribute('data-status')).toBe('success');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('puts its children below the rows', () => {
|
|
55
|
+
const { container } = render(
|
|
56
|
+
<Readout rows={[{ label: 'a', value: '1' }]}>
|
|
57
|
+
<p>note</p>
|
|
58
|
+
</Readout>,
|
|
59
|
+
);
|
|
60
|
+
const root = must(container.firstElementChild);
|
|
61
|
+
expect(root.className).toContain('lk-readout');
|
|
62
|
+
const note = screen.getByText('note');
|
|
63
|
+
expect(
|
|
64
|
+
must(root.querySelector('dl')).compareDocumentPosition(note) &
|
|
65
|
+
Node.DOCUMENT_POSITION_FOLLOWING,
|
|
66
|
+
).toBeTruthy();
|
|
67
|
+
expect(note.closest('dl')).toBeNull();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('draws no footer without children', () => {
|
|
71
|
+
const { container } = render(<Readout rows={[{ label: 'a', value: '1' }]} />);
|
|
72
|
+
expect(container.querySelector('.lk-readout__footer')).toBeNull();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('names the list by its title', () => {
|
|
76
|
+
render(<Readout title="Measure" rows={[{ label: 'a', value: '1' }]} />);
|
|
77
|
+
const heading = screen.getByRole('heading', { name: 'Measure' });
|
|
78
|
+
expect(
|
|
79
|
+
must(must(heading.parentElement).querySelector('dl')).getAttribute('aria-labelledby'),
|
|
80
|
+
).toBe(heading.id);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('merges a consumer className on its root', () => {
|
|
84
|
+
const { container } = render(<Readout className="is-mine" rows={[]} />);
|
|
85
|
+
expect(must(container.firstElementChild).className).toMatch(/\blk-readout\b.*\bis-mine\b/);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('is exported from the labkit barrel, with the list it is built on', () => {
|
|
89
|
+
expect(labkit.Readout).toBe(Readout);
|
|
90
|
+
expect(typeof labkit.DetailList).toBe('function');
|
|
91
|
+
expect(typeof labkit.DetailRow).toBe('function');
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { DetailList, type DetailListValues, DetailRow, type DetailRowStatus } from '@weasel-js/ui';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
/** One measurement in a {@link Readout}. */
|
|
5
|
+
export interface ReadoutRow {
|
|
6
|
+
/** React key. Defaults to `label` when that is a string, else the row's index. */
|
|
7
|
+
key?: string;
|
|
8
|
+
label: ReactNode;
|
|
9
|
+
/** `null` or `undefined` keeps the row in place showing an en dash, so the
|
|
10
|
+
* readout holds its height while a measurement comes and goes. */
|
|
11
|
+
value?: ReactNode;
|
|
12
|
+
/** A dot in the status color before the value. Color is never the only
|
|
13
|
+
* signal: the value has to say the same thing in words. */
|
|
14
|
+
status?: DetailRowStatus;
|
|
15
|
+
/** `text` sets this row's value as wrapping prose instead of a figure. */
|
|
16
|
+
values?: DetailListValues;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Props for {@link Readout}. */
|
|
20
|
+
export interface ReadoutProps {
|
|
21
|
+
rows: readonly ReadoutRow[];
|
|
22
|
+
/** Heading above the rows, which also names the list. */
|
|
23
|
+
title?: ReactNode;
|
|
24
|
+
/** Drawn below the rows — a small table, a note. */
|
|
25
|
+
children?: ReactNode;
|
|
26
|
+
className?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Values a lab only reads — measurements shown beside its picture, such as
|
|
31
|
+
* "Lock margin +47.9°". A {@link DetailList} set as figures: labels take the
|
|
32
|
+
* params label recipe and `--wzl-params-label-width` rail, so a readout lines
|
|
33
|
+
* up with a `ControlPanel` beside it, and values sit right-aligned in the mono
|
|
34
|
+
* face with spaces kept, so figure-space padding aligns a column on its
|
|
35
|
+
* decimal point. Rows compose by hand as `DetailRow`s inside a `DetailList`
|
|
36
|
+
* with `values="figures"`.
|
|
37
|
+
*/
|
|
38
|
+
export function Readout({ rows, title, children, className }: ReadoutProps) {
|
|
39
|
+
return (
|
|
40
|
+
<div className={className ? `lk-readout ${className}` : 'lk-readout'}>
|
|
41
|
+
<DetailList title={title} values="figures">
|
|
42
|
+
{rows.map((row, i) => (
|
|
43
|
+
<DetailRow
|
|
44
|
+
key={row.key ?? (typeof row.label === 'string' ? row.label : i)}
|
|
45
|
+
label={row.label}
|
|
46
|
+
status={row.status}
|
|
47
|
+
values={row.values}
|
|
48
|
+
>
|
|
49
|
+
{row.value}
|
|
50
|
+
</DetailRow>
|
|
51
|
+
))}
|
|
52
|
+
</DetailList>
|
|
53
|
+
{children != null && <div className="lk-readout__footer">{children}</div>}
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
}
|
package/src/primitives/Split.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useLatest } from '@weasel-js/core';
|
|
1
2
|
import { type ReactNode, useEffect, useMemo, useRef } from 'react';
|
|
2
3
|
import { asNodeId, createNode, Store, stripStrategy } from 'windease';
|
|
3
4
|
import { type ChromeMap, Container, Provider, StrategyRegistryProvider } from 'windease/react';
|
|
@@ -42,6 +43,65 @@ export interface SplitProps {
|
|
|
42
43
|
viewport?: { w: number; h: number };
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
interface SplitInit {
|
|
47
|
+
side: 'start' | 'end';
|
|
48
|
+
label: string;
|
|
49
|
+
sidebarWidth: number;
|
|
50
|
+
minWidth: number;
|
|
51
|
+
maxWidth: number;
|
|
52
|
+
contentMinWidth: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** A strip holding the sidebar and the content pane, sized as the first render asks. */
|
|
56
|
+
function createSplitStore({
|
|
57
|
+
side,
|
|
58
|
+
label,
|
|
59
|
+
sidebarWidth,
|
|
60
|
+
minWidth,
|
|
61
|
+
maxWidth,
|
|
62
|
+
contentMinWidth,
|
|
63
|
+
}: SplitInit): Store {
|
|
64
|
+
const store = new Store();
|
|
65
|
+
store.registerNode(
|
|
66
|
+
createNode({
|
|
67
|
+
kind: 'zone',
|
|
68
|
+
id: ZONE_ID,
|
|
69
|
+
// A strip's seam resizes the pane before it, which on the end side is the
|
|
70
|
+
// content; 'neighbor' has the seam size the sidebar after it too.
|
|
71
|
+
container: {
|
|
72
|
+
strategyId: 'strip',
|
|
73
|
+
config: {
|
|
74
|
+
axis: 'x',
|
|
75
|
+
resizable: true,
|
|
76
|
+
...(side === 'end' ? { resizeMode: 'neighbor' } : {}),
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
}),
|
|
80
|
+
);
|
|
81
|
+
const sidebarNode = createNode({
|
|
82
|
+
kind: 'sidebar',
|
|
83
|
+
id: SIDEBAR_ID,
|
|
84
|
+
parentId: ZONE_ID,
|
|
85
|
+
meta: { title: label },
|
|
86
|
+
placement: { size: { w: sidebarWidth } },
|
|
87
|
+
hints: { minSize: { w: minWidth, h: 0 }, maxSize: { w: maxWidth, h: 0 } },
|
|
88
|
+
});
|
|
89
|
+
const contentNode = createNode({
|
|
90
|
+
kind: 'content',
|
|
91
|
+
id: CONTENT_ID,
|
|
92
|
+
parentId: ZONE_ID,
|
|
93
|
+
meta: { title: 'Content' },
|
|
94
|
+
hints: { minSize: { w: contentMinWidth, h: 0 } },
|
|
95
|
+
});
|
|
96
|
+
// A strip lays its children out in registration order.
|
|
97
|
+
for (const node of side === 'end' ? [contentNode, sidebarNode] : [sidebarNode, contentNode]) {
|
|
98
|
+
store.registerNode(node);
|
|
99
|
+
}
|
|
100
|
+
store.showNode(SIDEBAR_ID);
|
|
101
|
+
store.showNode(CONTENT_ID);
|
|
102
|
+
return store;
|
|
103
|
+
}
|
|
104
|
+
|
|
45
105
|
/**
|
|
46
106
|
* A sidebar and a content pane as a two-pane strip with a draggable seam.
|
|
47
107
|
*
|
|
@@ -70,45 +130,14 @@ export function Split({
|
|
|
70
130
|
// stretches its panes to the container's height and never reads `h`.
|
|
71
131
|
const storeRef = useRef<Store | null>(null);
|
|
72
132
|
if (storeRef.current === null) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
container: {
|
|
81
|
-
strategyId: 'strip',
|
|
82
|
-
config: {
|
|
83
|
-
axis: 'x',
|
|
84
|
-
resizable: true,
|
|
85
|
-
...(side === 'end' ? { resizeMode: 'neighbor' } : {}),
|
|
86
|
-
},
|
|
87
|
-
},
|
|
88
|
-
}),
|
|
89
|
-
);
|
|
90
|
-
const sidebarNode = createNode({
|
|
91
|
-
kind: 'sidebar',
|
|
92
|
-
id: SIDEBAR_ID,
|
|
93
|
-
parentId: ZONE_ID,
|
|
94
|
-
meta: { title: label },
|
|
95
|
-
placement: { size: { w: width ?? defaultWidth } },
|
|
96
|
-
hints: { minSize: { w: minWidth, h: 0 }, maxSize: { w: maxWidth, h: 0 } },
|
|
97
|
-
});
|
|
98
|
-
const contentNode = createNode({
|
|
99
|
-
kind: 'content',
|
|
100
|
-
id: CONTENT_ID,
|
|
101
|
-
parentId: ZONE_ID,
|
|
102
|
-
meta: { title: 'Content' },
|
|
103
|
-
hints: { minSize: { w: contentMinWidth, h: 0 } },
|
|
133
|
+
storeRef.current = createSplitStore({
|
|
134
|
+
side,
|
|
135
|
+
label,
|
|
136
|
+
sidebarWidth: width ?? defaultWidth,
|
|
137
|
+
minWidth,
|
|
138
|
+
maxWidth,
|
|
139
|
+
contentMinWidth,
|
|
104
140
|
});
|
|
105
|
-
// A strip lays its children out in registration order.
|
|
106
|
-
for (const node of side === 'end' ? [contentNode, sidebarNode] : [sidebarNode, contentNode]) {
|
|
107
|
-
store.registerNode(node);
|
|
108
|
-
}
|
|
109
|
-
store.showNode(SIDEBAR_ID);
|
|
110
|
-
store.showNode(CONTENT_ID);
|
|
111
|
-
storeRef.current = store;
|
|
112
141
|
}
|
|
113
142
|
const store = storeRef.current;
|
|
114
143
|
|
|
@@ -134,8 +163,7 @@ export function Split({
|
|
|
134
163
|
store.patchPlacement(SIDEBAR_ID, { size: { w: width } });
|
|
135
164
|
}, [store, width]);
|
|
136
165
|
|
|
137
|
-
const onWidthChangeRef =
|
|
138
|
-
onWidthChangeRef.current = onWidthChange;
|
|
166
|
+
const onWidthChangeRef = useLatest(onWidthChange);
|
|
139
167
|
useEffect(
|
|
140
168
|
() =>
|
|
141
169
|
store.events.on('node.placementChanged', () => {
|
|
@@ -148,7 +176,7 @@ export function Split({
|
|
|
148
176
|
widthRef.current = next;
|
|
149
177
|
onWidthChangeRef.current?.(next);
|
|
150
178
|
}),
|
|
151
|
-
[store],
|
|
179
|
+
[onWidthChangeRef, store],
|
|
152
180
|
);
|
|
153
181
|
|
|
154
182
|
const chrome = useMemo<ChromeMap>(
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { MouseEventHandler, ReactNode } from 'react';
|
|
2
|
-
import { useRovingTabIndex } from '
|
|
2
|
+
import { useRovingTabIndex } from '../passthrough/weasel-ui';
|
|
3
3
|
|
|
4
4
|
/** Props for `<Toolbar>`. */
|
|
5
5
|
export interface ToolbarProps {
|
|
@@ -12,10 +12,10 @@ export interface ToolbarProps {
|
|
|
12
12
|
/** A horizontal bar of controls. Fill it with `<Toolbar.Title>`,
|
|
13
13
|
* `<Toolbar.Group>`, `<Toolbar.Button>` and `<Toolbar.Spacer>`. */
|
|
14
14
|
export function Toolbar({ children, 'aria-label': ariaLabel }: ToolbarProps) {
|
|
15
|
-
const {
|
|
15
|
+
const { rootRef, onKeyDown } = useRovingTabIndex<HTMLDivElement>({ orientation: 'horizontal' });
|
|
16
16
|
return (
|
|
17
17
|
<div
|
|
18
|
-
ref={
|
|
18
|
+
ref={rootRef}
|
|
19
19
|
className="lk-toolbar"
|
|
20
20
|
role="toolbar"
|
|
21
21
|
aria-label={ariaLabel}
|
|
@@ -13,8 +13,7 @@
|
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
//
|
|
17
|
-
// See the `.lk-root` prefix convention in theme/base.less.
|
|
16
|
+
// Under `.lk-root` to outrank theme/base.less's element defaults; see its prefix convention.
|
|
18
17
|
.lk-root {
|
|
19
18
|
.lk-zoom {
|
|
20
19
|
// Narrow enough to sit in a trial toolbar, wide enough for the octave
|
package/src/primitives/index.ts
CHANGED
|
@@ -5,6 +5,8 @@ export type { JobProgressProps } from './JobProgress';
|
|
|
5
5
|
export { JobProgress } from './JobProgress';
|
|
6
6
|
export type { LegendEntry, LegendMark, LegendProps } from './Legend';
|
|
7
7
|
export { Legend } from './Legend';
|
|
8
|
+
export type { ReadoutProps, ReadoutRow } from './Readout';
|
|
9
|
+
export { Readout } from './Readout';
|
|
8
10
|
export type { ScaleIndicatorProps } from './ScaleIndicator';
|
|
9
11
|
export { ScaleIndicator } from './ScaleIndicator';
|
|
10
12
|
export type { SplitProps } from './Split';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { FillStyle,
|
|
1
|
+
import type { FillStyle, Track } from '@weasel-js/core';
|
|
2
2
|
import {
|
|
3
3
|
Badge,
|
|
4
4
|
type Band,
|
|
@@ -321,7 +321,7 @@ function FieldsAndPickers() {
|
|
|
321
321
|
const [rate, setRate] = useState(1);
|
|
322
322
|
const [width, setWidth] = useState(6);
|
|
323
323
|
const [color, setColor] = useState('#b08adbff');
|
|
324
|
-
const [gradient, setGradient] = useState<
|
|
324
|
+
const [gradient, setGradient] = useState<FillStyle>({
|
|
325
325
|
fill: 'linear-gradient',
|
|
326
326
|
from: { x: 0.1, y: 0.1 },
|
|
327
327
|
to: { x: 0.9, y: 0.9 },
|
|
@@ -666,7 +666,6 @@ function ToolChrome() {
|
|
|
666
666
|
label={id}
|
|
667
667
|
shortcut={id.slice(0, 1).toUpperCase()}
|
|
668
668
|
active={tool === id}
|
|
669
|
-
tabbable={tool === id}
|
|
670
669
|
onClick={() => setTool(id)}
|
|
671
670
|
/>
|
|
672
671
|
))}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
2
|
+
import { afterEach, expect, test } from 'vitest';
|
|
3
|
+
import { createIndexedDbAdapter } from './adapters';
|
|
4
|
+
import { Persistence } from './Persistence';
|
|
5
|
+
import { usePersistedState } from './usePersistedState';
|
|
6
|
+
|
|
7
|
+
// jsdom only has fake-indexeddb, an imitation; this goes through the browser's own.
|
|
8
|
+
|
|
9
|
+
afterEach(cleanup);
|
|
10
|
+
|
|
11
|
+
function Tab() {
|
|
12
|
+
const [tab, setTab] = usePersistedState('tab', 'shape');
|
|
13
|
+
return (
|
|
14
|
+
<p>
|
|
15
|
+
<output aria-label="tab">{tab}</output>
|
|
16
|
+
<button type="button" onClick={() => setTab('color')}>
|
|
17
|
+
choose color
|
|
18
|
+
</button>
|
|
19
|
+
</p>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const mount = (storage: ReturnType<typeof createIndexedDbAdapter>) =>
|
|
24
|
+
render(
|
|
25
|
+
<Persistence storageKey="test" storage={storage} fallback={<p>loading</p>}>
|
|
26
|
+
<Tab />
|
|
27
|
+
</Persistence>,
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
test('a value survives a remount through real IndexedDB', async () => {
|
|
31
|
+
const database = `labkit-browser-test-${Date.now()}`;
|
|
32
|
+
|
|
33
|
+
const first = mount(createIndexedDbAdapter({ database }));
|
|
34
|
+
await waitFor(() => expect(screen.getByLabelText('tab').textContent).toBe('shape'));
|
|
35
|
+
fireEvent.click(screen.getByText('choose color'));
|
|
36
|
+
await waitFor(() => expect(screen.getByLabelText('tab').textContent).toBe('color'));
|
|
37
|
+
|
|
38
|
+
// Unmounting closes the provider, whose deferred close sends the queued write.
|
|
39
|
+
first.unmount();
|
|
40
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
41
|
+
|
|
42
|
+
// A fresh adapter, so nothing it reads can come from the first one's memory.
|
|
43
|
+
mount(createIndexedDbAdapter({ database }));
|
|
44
|
+
await waitFor(() => expect(screen.getByLabelText('tab').textContent).toBe('color'), {
|
|
45
|
+
timeout: 3000,
|
|
46
|
+
});
|
|
47
|
+
});
|
package/src/state/store.test.ts
CHANGED
|
@@ -420,7 +420,11 @@ describe('setInstruments', () => {
|
|
|
420
420
|
store.getState().addTrial(mappedTrial);
|
|
421
421
|
store.getState().setInstruments([{ ...small, name: 'Mapped' }]);
|
|
422
422
|
store.getState().saveSnapshot('m', 'one');
|
|
423
|
-
|
|
423
|
+
// By tag, not `toEqual(new Set(…))`: under jsdom structuredClone hands back Node's Set.
|
|
424
|
+
const seen = (store.getState().savedSnapshots[0]?.state as { seen: Set<string> } | undefined)
|
|
425
|
+
?.seen;
|
|
426
|
+
expect(Object.prototype.toString.call(seen)).toBe('[object Set]');
|
|
427
|
+
expect([...seen!]).toEqual(['a']);
|
|
424
428
|
});
|
|
425
429
|
|
|
426
430
|
it('changes neither hooks nor state when a new default config throws', () => {
|
package/src/state/undock.ts
CHANGED
|
@@ -8,10 +8,13 @@ export interface UndockedPanel {
|
|
|
8
8
|
/** Undocked panels, keyed by {@link panelKey}. */
|
|
9
9
|
export type UndockedPanels = Record<string, UndockedPanel>;
|
|
10
10
|
|
|
11
|
+
/** The `UndockedPanels` key for one trial's section. */
|
|
11
12
|
export function panelKey(trialId: string, sectionId: string): string {
|
|
12
13
|
return `${trialId}::${sectionId}`;
|
|
13
14
|
}
|
|
14
15
|
|
|
16
|
+
/** `panels` with the section marked undocked `as` a tile or a floating panel,
|
|
17
|
+
* replacing any earlier entry for it. Returns a new record. */
|
|
15
18
|
export function undockPanel(
|
|
16
19
|
panels: UndockedPanels,
|
|
17
20
|
trialId: string,
|