@weasel-js/labkit 1.5.1 → 1.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_dts/{CanvasStackContext-CJNMS6mz.d.ts → CanvasStackContext-CMMo180l.d.ts} +1 -1
- package/dist/_dts/{Select.d-Gwr8fLpK.d.ts → Select.d-ZTiwzYyT.d.ts} +27 -1
- package/dist/_dts/{frac-B4okC2lX.d.ts → frac-B4hFJMiN.d.ts} +16 -6
- package/dist/_dts/{index-BNdz-tNd.d.ts → index-8LiKe3A-.d.ts} +30 -43
- package/dist/_dts/{index-CSJoj4LC.d.ts → index-DlKolEIZ.d.ts} +5 -4
- package/dist/_dts/{types-DzoNY1pO.d.ts → types-Db-3NNk1.d.ts} +4 -4
- package/dist/_dts/{usePanZoom-Pzf9f2jh.d.ts → usePanZoom-SSTZe410.d.ts} +4 -47
- package/dist/_dts/{useTrialState-uLlmaccn.d.ts → useTrialState-pnAE-UBD.d.ts} +1 -1
- package/dist/canvas/index.d.ts +4 -4
- package/dist/canvas/index.js +1 -1
- package/dist/chrome/index.d.ts +3 -3
- package/dist/chrome/index.js +6 -6
- package/dist/{chunk-PJ7WEQBJ.js → chunk-6BUQ77ZV.js} +4 -4
- package/dist/{chunk-PJ7WEQBJ.js.map → chunk-6BUQ77ZV.js.map} +1 -1
- package/dist/{chunk-UN4U4P63.js → chunk-753Y6HE6.js} +9 -9
- package/dist/chunk-753Y6HE6.js.map +1 -0
- package/dist/{chunk-ZDHY47EV.js → chunk-A2ACAGW6.js} +6 -7
- package/dist/chunk-A2ACAGW6.js.map +1 -0
- package/dist/{chunk-IHUVQ3TX.js → chunk-CKUKF72R.js} +10 -10
- package/dist/chunk-CKUKF72R.js.map +1 -0
- package/dist/{chunk-MDVTK5U3.js → chunk-D35PFTS7.js} +3 -3
- package/dist/{chunk-MDVTK5U3.js.map → chunk-D35PFTS7.js.map} +1 -1
- package/dist/{chunk-PHK4W562.js → chunk-EMXAIRUJ.js} +3 -3
- package/dist/{chunk-PHK4W562.js.map → chunk-EMXAIRUJ.js.map} +1 -1
- package/dist/{chunk-EH7UEQV7.js → chunk-FKLT3ITC.js} +69 -69
- package/dist/chunk-FKLT3ITC.js.map +1 -0
- package/dist/{chunk-EHGVROVV.js → chunk-KJCUERQF.js} +3 -3
- package/dist/{chunk-EHGVROVV.js.map → chunk-KJCUERQF.js.map} +1 -1
- package/dist/chunk-LASCBWW7.js +13456 -0
- package/dist/chunk-LASCBWW7.js.map +1 -0
- package/dist/{chunk-IXTQI7JX.js → chunk-LRI3MBGR.js} +4 -4
- package/dist/{chunk-IXTQI7JX.js.map → chunk-LRI3MBGR.js.map} +1 -1
- package/dist/{chunk-7PFH3CNM.js → chunk-REOBGYMC.js} +4 -4
- package/dist/{chunk-7PFH3CNM.js.map → chunk-REOBGYMC.js.map} +1 -1
- package/dist/{chunk-CEIUBPPW.js → chunk-TT5C5S6R.js} +3 -86
- package/dist/chunk-TT5C5S6R.js.map +1 -0
- package/dist/config/index.d.ts +7 -8
- package/dist/config/index.js +5 -5
- package/dist/controls/index.d.ts +2 -2
- package/dist/controls/index.js +4 -4
- package/dist/dragdrop/index.d.ts +2 -2
- package/dist/index.d.ts +72 -28
- package/dist/index.js +142 -123
- package/dist/index.js.map +1 -1
- package/dist/layers/index.d.ts +3 -3
- package/dist/layers/index.js +2 -2
- package/dist/loupe/index.d.ts +4 -4
- package/dist/passthrough/weasel-ui.d.ts +33 -30
- package/dist/passthrough/weasel-ui.js +1 -1
- package/dist/primitives/index.js +3 -3
- package/dist/state/index.d.ts +4 -4
- package/dist/state/index.js +5 -5
- package/dist/styles.css +114 -18
- package/dist/ui/layers/index.js +1 -1
- package/dist/undo/index.d.ts +2 -2
- package/package.json +9 -9
- package/src/canvas/index.ts +0 -9
- package/src/chrome/labTypes.ts +1 -1
- package/src/chrome/regions/PaletteRegion.test.tsx +18 -0
- package/src/chrome/regions/PaletteRegion.tsx +5 -4
- package/src/chrome/regions/TitleBarRegion.tsx +1 -1
- package/src/chrome/types.ts +8 -2
- package/src/config/builder.ts +3 -4
- package/src/config/types.ts +4 -4
- package/src/controls/AutoControls.stories.tsx +6 -6
- package/src/controls/ControlPanel.less +23 -0
- package/src/controls/ControlPanel.stories.tsx +41 -0
- package/src/controls/ControlPanel.test.tsx +53 -110
- package/src/controls/ControlPanel.tsx +61 -77
- package/src/index.test.ts +0 -2
- package/src/index.ts +4 -10
- package/src/lab/Lab.tsx +21 -5
- package/src/layers/LayerList.less +1 -1
- package/src/layers/LayerList.stories.tsx +1 -1
- package/src/primitives/FpsMeter.less +6 -1
- package/src/primitives/FpsMeter.test.tsx +12 -4
- package/src/primitives/FpsMeter.tsx +6 -1
- package/src/primitives/JobProgress.stories.tsx +1 -1
- package/src/primitives/StatusBar.less +6 -0
- package/src/state/types.ts +6 -1
- package/src/state/view.test.ts +4 -4
- package/src/trial/Trial.canvas.test.tsx +8 -4
- package/src/trial/Trial.config.test.tsx +1 -1
- package/dist/chunk-CEIUBPPW.js.map +0 -1
- package/dist/chunk-EH7UEQV7.js.map +0 -1
- package/dist/chunk-IHUVQ3TX.js.map +0 -1
- package/dist/chunk-T3EC2DXD.js +0 -12628
- package/dist/chunk-T3EC2DXD.js.map +0 -1
- package/dist/chunk-UN4U4P63.js.map +0 -1
- package/dist/chunk-ZDHY47EV.js.map +0 -1
- package/src/canvas/useOrbit.test.ts +0 -167
- package/src/canvas/useOrbit.ts +0 -156
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
Switch,
|
|
24
24
|
SwitchRow,
|
|
25
25
|
TextRow,
|
|
26
|
+
ToggleBar,
|
|
26
27
|
ToggleRow,
|
|
27
28
|
} from '@weasel-js/ui';
|
|
28
29
|
import { Fragment, type ReactNode, useEffect, useMemo, useRef, useState } from 'react';
|
|
@@ -106,11 +107,11 @@ export interface ControlPanelProps<TC extends Record<string, unknown>> {
|
|
|
106
107
|
/** A fold moved. The key is a section's label — prefixed by its group's
|
|
107
108
|
* dotted path when the section sits inside one — or a group's own path. */
|
|
108
109
|
onCollapse?: (key: string, collapsed: boolean) => void;
|
|
109
|
-
/** Dotted paths currently unpinned. A row in this set
|
|
110
|
-
*
|
|
111
|
-
* `setConfig`. Omitted, the panel keeps the set itself: the
|
|
112
|
-
* and the sentinel never reaches `setConfig`, which would otherwise
|
|
113
|
-
* as the row's value. */
|
|
110
|
+
/** Dotted paths currently unpinned. A row in this set hides its control and
|
|
111
|
+
* reads out `auto`, and clicking its label writes the sentinel back through
|
|
112
|
+
* `setConfig`. Omitted, the panel keeps the set itself: the labels still
|
|
113
|
+
* toggle and the sentinel never reaches `setConfig`, which would otherwise
|
|
114
|
+
* store it as the row's value. */
|
|
114
115
|
auto?: ReadonlySet<string>;
|
|
115
116
|
/** Draw leaves marked `hidden`. */
|
|
116
117
|
showHidden?: boolean;
|
|
@@ -170,28 +171,6 @@ export function ControlPanel<TC extends Record<string, unknown>>({
|
|
|
170
171
|
[resolved, config, auto],
|
|
171
172
|
);
|
|
172
173
|
|
|
173
|
-
const listRef = useRef<HTMLDivElement>(null);
|
|
174
|
-
const toggles = useRef(new Map<string, () => void>());
|
|
175
|
-
// Capture phase, because `PropertyRow` is a <label>: a bubbled handler runs
|
|
176
|
-
// after the browser has already begun a range drag or a native control's
|
|
177
|
-
// activation, so the shift-click moves the very value it was meant to unpin.
|
|
178
|
-
useEffect(() => {
|
|
179
|
-
const host = listRef.current;
|
|
180
|
-
if (!host) return;
|
|
181
|
-
const onDown = (e: PointerEvent) => {
|
|
182
|
-
if (!e.shiftKey) return;
|
|
183
|
-
const row = (e.target as HTMLElement | null)?.closest('[data-auto-path]');
|
|
184
|
-
const path = row?.getAttribute('data-auto-path');
|
|
185
|
-
const toggle = path ? toggles.current.get(path) : undefined;
|
|
186
|
-
if (!toggle) return;
|
|
187
|
-
e.preventDefault();
|
|
188
|
-
e.stopPropagation();
|
|
189
|
-
toggle();
|
|
190
|
-
};
|
|
191
|
-
host.addEventListener('pointerdown', onDown, true);
|
|
192
|
-
return () => host.removeEventListener('pointerdown', onDown, true);
|
|
193
|
-
}, []);
|
|
194
|
-
|
|
195
174
|
const gridPack = gridOf(pack);
|
|
196
175
|
// A section that declares how it opens is foldable whether or not the lab
|
|
197
176
|
// asked for folds — there is nothing else for the declaration to mean.
|
|
@@ -230,7 +209,6 @@ export function ControlPanel<TC extends Record<string, unknown>>({
|
|
|
230
209
|
layout={rows.layout}
|
|
231
210
|
auto={auto}
|
|
232
211
|
setRowAuto={setRowAuto}
|
|
233
|
-
toggles={toggles.current}
|
|
234
212
|
/>
|
|
235
213
|
);
|
|
236
214
|
}
|
|
@@ -238,7 +216,16 @@ export function ControlPanel<TC extends Record<string, unknown>>({
|
|
|
238
216
|
// empty `PrefGroup.name` — so it contributes its rows and no chrome.
|
|
239
217
|
if (found.name === '') return <Fragment key={path}>{body(found, path, rows)}</Fragment>;
|
|
240
218
|
return (
|
|
241
|
-
<PropertyGroup
|
|
219
|
+
<PropertyGroup
|
|
220
|
+
key={path}
|
|
221
|
+
title={found.name}
|
|
222
|
+
description={found.description}
|
|
223
|
+
// A nested group is a block of rows, not a control: half of a
|
|
224
|
+
// two-column grid leaves it laying its own rows out inside a cell,
|
|
225
|
+
// which overlaps them.
|
|
226
|
+
span
|
|
227
|
+
{...fold(path, undefined, rows.grid)}
|
|
228
|
+
>
|
|
242
229
|
{body(found, path, rows)}
|
|
243
230
|
</PropertyGroup>
|
|
244
231
|
);
|
|
@@ -289,6 +276,9 @@ export function ControlPanel<TC extends Record<string, unknown>>({
|
|
|
289
276
|
shown={shown}
|
|
290
277
|
setConfig={setConfig}
|
|
291
278
|
layout={rows.layout}
|
|
279
|
+
// A segmented cell carries every choice at once, so a pair holding
|
|
280
|
+
// one needs the whole row the way a segmented row of its own does.
|
|
281
|
+
span={cells.some(({ leaf }) => extra<string>(leaf, 'control') === 'radio')}
|
|
292
282
|
/>,
|
|
293
283
|
);
|
|
294
284
|
i = j;
|
|
@@ -324,16 +314,14 @@ export function ControlPanel<TC extends Record<string, unknown>>({
|
|
|
324
314
|
};
|
|
325
315
|
|
|
326
316
|
return (
|
|
327
|
-
<
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
</PropertyList>
|
|
336
|
-
</div>
|
|
317
|
+
<PropertyList
|
|
318
|
+
pack={gridPack}
|
|
319
|
+
density={density}
|
|
320
|
+
align={align}
|
|
321
|
+
className={className ? `lk-control-panel ${className}` : 'lk-control-panel'}
|
|
322
|
+
>
|
|
323
|
+
{body(resolved.group, '', { pack, layout, grid: gridPack })}
|
|
324
|
+
</PropertyList>
|
|
337
325
|
);
|
|
338
326
|
}
|
|
339
327
|
|
|
@@ -357,7 +345,6 @@ interface ControlRowProps<TC extends Record<string, unknown>> {
|
|
|
357
345
|
layout?: PropertyRowLayout;
|
|
358
346
|
auto?: ReadonlySet<string>;
|
|
359
347
|
setRowAuto: (path: string, next: boolean, value: unknown) => void;
|
|
360
|
-
toggles: Map<string, () => void>;
|
|
361
348
|
}
|
|
362
349
|
|
|
363
350
|
/** Whether this leaf draws as a slider, mirroring the condition the `number`
|
|
@@ -390,7 +377,6 @@ function ControlRow<TC extends Record<string, unknown>>({
|
|
|
390
377
|
layout,
|
|
391
378
|
auto,
|
|
392
379
|
setRowAuto,
|
|
393
|
-
toggles,
|
|
394
380
|
}: ControlRowProps<TC>) {
|
|
395
381
|
const write = (value: unknown): void => setConfig(path, value);
|
|
396
382
|
const fallback = extra<unknown>(leaf, 'default');
|
|
@@ -408,30 +394,9 @@ function ControlRow<TC extends Record<string, unknown>>({
|
|
|
408
394
|
const value = resolvedValue ?? pinned;
|
|
409
395
|
const setAuto = (next: boolean): void => setRowAuto(path, next, value);
|
|
410
396
|
const onAutoChange = canAuto ? setAuto : undefined;
|
|
411
|
-
//
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
const readsItsOwnValue = !isSliderLeaf(leaf);
|
|
415
|
-
// A readout says what the control would say, so it reads in the display
|
|
416
|
-
// unit: a radian in a row that edits degrees is a different number.
|
|
417
|
-
const readoutValue =
|
|
418
|
-
leaf.kind === 'number' && typeof resolvedValue === 'number'
|
|
419
|
-
? numberField(leaf, resolvedValue).shown
|
|
420
|
-
: resolvedValue;
|
|
421
|
-
const autoReadout = !isAutoRow
|
|
422
|
-
? undefined
|
|
423
|
-
: resolvedValue === undefined || readsItsOwnValue
|
|
424
|
-
? 'auto'
|
|
425
|
-
: `auto · ${String(readoutValue)}`;
|
|
426
|
-
const autoProps = { auto: isAutoRow, onAutoChange, 'data-auto-path': canAuto ? path : undefined };
|
|
427
|
-
|
|
428
|
-
useEffect(() => {
|
|
429
|
-
if (!canAuto) return;
|
|
430
|
-
toggles.set(path, () => setAuto(!isAutoRow));
|
|
431
|
-
return () => {
|
|
432
|
-
toggles.delete(path);
|
|
433
|
-
};
|
|
434
|
-
});
|
|
397
|
+
// An auto row hides its control, so the word is all the row has to say.
|
|
398
|
+
const autoReadout = isAutoRow ? 'auto' : undefined;
|
|
399
|
+
const autoProps = { auto: isAutoRow, onAutoChange };
|
|
435
400
|
|
|
436
401
|
// Most specific wins, and within a tier the lab's entry beats the
|
|
437
402
|
// instrument's: controls[path] -> node .render -> controls[kind] -> built-in.
|
|
@@ -611,7 +576,6 @@ function DebouncedTextRow({
|
|
|
611
576
|
readout,
|
|
612
577
|
auto,
|
|
613
578
|
onAutoChange,
|
|
614
|
-
'data-auto-path': autoPath,
|
|
615
579
|
}: {
|
|
616
580
|
leaf: PrefLeaf;
|
|
617
581
|
label: string;
|
|
@@ -623,7 +587,6 @@ function DebouncedTextRow({
|
|
|
623
587
|
readout?: ReactNode;
|
|
624
588
|
auto?: boolean;
|
|
625
589
|
onAutoChange?: (next: boolean) => void;
|
|
626
|
-
'data-auto-path'?: string;
|
|
627
590
|
}) {
|
|
628
591
|
const text = useDebouncedText(
|
|
629
592
|
value,
|
|
@@ -640,7 +603,6 @@ function DebouncedTextRow({
|
|
|
640
603
|
description={description}
|
|
641
604
|
auto={auto}
|
|
642
605
|
onAutoChange={onAutoChange}
|
|
643
|
-
data-auto-path={autoPath}
|
|
644
606
|
value={text.local}
|
|
645
607
|
placeholder={extra<string>(leaf, 'placeholder')}
|
|
646
608
|
maxLength={extra<number>(leaf, 'maxLength')}
|
|
@@ -735,8 +697,8 @@ interface PairCellSpec {
|
|
|
735
697
|
*
|
|
736
698
|
* The cells are bare controls: a row holds one label column, so each leaf's
|
|
737
699
|
* own name stays with its control as the accessible name and its description
|
|
738
|
-
* as a title. For the same reason the row
|
|
739
|
-
* path, and this row has several.
|
|
700
|
+
* as a title. For the same reason the row's label does not toggle auto — it
|
|
701
|
+
* names one path, and this row has several.
|
|
740
702
|
*/
|
|
741
703
|
function PairedRow<TC extends Record<string, unknown>>({
|
|
742
704
|
label,
|
|
@@ -744,22 +706,29 @@ function PairedRow<TC extends Record<string, unknown>>({
|
|
|
744
706
|
shown,
|
|
745
707
|
setConfig,
|
|
746
708
|
layout,
|
|
709
|
+
span,
|
|
747
710
|
}: {
|
|
748
711
|
label: string;
|
|
749
712
|
cells: readonly PairCellSpec[];
|
|
750
713
|
shown: TC;
|
|
751
714
|
setConfig: (path: string, value: unknown) => void;
|
|
752
715
|
layout?: PropertyRowLayout;
|
|
716
|
+
span?: boolean;
|
|
753
717
|
}) {
|
|
754
718
|
return (
|
|
755
|
-
<PropertyRow group label={label} layout={layout}>
|
|
719
|
+
<PropertyRow group span={span} label={label} layout={layout}>
|
|
756
720
|
{cells.map(({ path, leaf }) => (
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
721
|
+
// The row is named by the pair, so a cell that shares it says which
|
|
722
|
+
// knob it is — without a caption a paired switch reads only to a
|
|
723
|
+
// screen reader. A cell whose leaf is deliberately unnamed keeps none.
|
|
724
|
+
<span key={path} className="lk-pair-cell">
|
|
725
|
+
{leaf.name ? <span className="lk-pair-label">{leaf.name}</span> : null}
|
|
726
|
+
<PairCell
|
|
727
|
+
leaf={leaf}
|
|
728
|
+
value={valueAtPath(shown, path) ?? extra<unknown>(leaf, 'default')}
|
|
729
|
+
write={(value) => setConfig(path, value)}
|
|
730
|
+
/>
|
|
731
|
+
</span>
|
|
763
732
|
))}
|
|
764
733
|
</PropertyRow>
|
|
765
734
|
);
|
|
@@ -845,6 +814,21 @@ function PairCell({
|
|
|
845
814
|
}
|
|
846
815
|
case 'enum': {
|
|
847
816
|
const options = extra<readonly { value: string; label: string }[]>(leaf, 'options') ?? [];
|
|
817
|
+
// A leaf that asked to be segmented stays segmented when it shares a row:
|
|
818
|
+
// the choice is the control's shape, not a whole row's worth of chrome.
|
|
819
|
+
if (extra<string>(leaf, 'control') === 'radio') {
|
|
820
|
+
return (
|
|
821
|
+
<span title={title}>
|
|
822
|
+
<ToggleBar<string>
|
|
823
|
+
ariaLabel={name}
|
|
824
|
+
size="sm"
|
|
825
|
+
value={typeof value === 'string' ? value : null}
|
|
826
|
+
onChange={(next) => next !== null && write(next)}
|
|
827
|
+
items={options.map((o) => ({ value: o.value, label: o.label }))}
|
|
828
|
+
/>
|
|
829
|
+
</span>
|
|
830
|
+
);
|
|
831
|
+
}
|
|
848
832
|
// `Select` takes no `title`, so the cell's help hangs off a wrapper.
|
|
849
833
|
return (
|
|
850
834
|
<span title={title}>
|
package/src/index.test.ts
CHANGED
|
@@ -72,8 +72,6 @@ describe('surface, job and orbit entry points', () => {
|
|
|
72
72
|
expect(typeof kit.annotationsFromJSON).toBe('function');
|
|
73
73
|
expect(typeof kit.seenFrom).toBe('function');
|
|
74
74
|
expect(typeof kit.fracToWorld).toBe('function');
|
|
75
|
-
expect(typeof kit.useOrbit).toBe('function');
|
|
76
|
-
expect(typeof kit.orbitAfterDrag).toBe('function');
|
|
77
75
|
expect(typeof kit.usePanZoom).toBe('function');
|
|
78
76
|
expect(typeof kit.useJob).toBe('function');
|
|
79
77
|
expect(typeof kit.as2DView).toBe('function');
|
package/src/index.ts
CHANGED
|
@@ -43,6 +43,8 @@ export type {
|
|
|
43
43
|
TokenCategory,
|
|
44
44
|
TokenEntry,
|
|
45
45
|
TokenPanelProps,
|
|
46
|
+
TokenScale,
|
|
47
|
+
TokenScaleRule,
|
|
46
48
|
} from '@weasel-js/ui';
|
|
47
49
|
export {
|
|
48
50
|
Button,
|
|
@@ -67,6 +69,7 @@ export {
|
|
|
67
69
|
PropertyPanel,
|
|
68
70
|
PropertyRow,
|
|
69
71
|
PropertySpan,
|
|
72
|
+
refitScale,
|
|
70
73
|
ResetIcon,
|
|
71
74
|
Select,
|
|
72
75
|
SelectRow,
|
|
@@ -148,13 +151,9 @@ export type {
|
|
|
148
151
|
CanvasStackContextValue,
|
|
149
152
|
CanvasStackProps,
|
|
150
153
|
CanvasStackSurface,
|
|
151
|
-
OrbitHandlers,
|
|
152
|
-
OrbitView,
|
|
153
154
|
PanZoomHandlers,
|
|
154
155
|
StageProps,
|
|
155
|
-
UseOrbitOptions,
|
|
156
156
|
UsePanZoomOptions,
|
|
157
|
-
Vec3,
|
|
158
157
|
ViewportSize,
|
|
159
158
|
WorldFrame,
|
|
160
159
|
WorldSpec,
|
|
@@ -165,19 +164,13 @@ export {
|
|
|
165
164
|
CanvasStack,
|
|
166
165
|
CanvasStackContext,
|
|
167
166
|
centerOn,
|
|
168
|
-
clampPitch,
|
|
169
167
|
DEFAULT_FRAME,
|
|
170
168
|
fitStage,
|
|
171
|
-
orbitAfterDrag,
|
|
172
|
-
orbitAfterWheel,
|
|
173
|
-
PITCH_LIMIT,
|
|
174
169
|
resolveFrame,
|
|
175
170
|
Stage,
|
|
176
171
|
screenToWorld,
|
|
177
|
-
useOrbit,
|
|
178
172
|
usePanZoom,
|
|
179
173
|
worldToScreen,
|
|
180
|
-
wrapYaw,
|
|
181
174
|
zoomAt,
|
|
182
175
|
} from './canvas';
|
|
183
176
|
export * from './chrome';
|
|
@@ -298,6 +291,7 @@ export { Persistence, type PersistenceProps } from './state/Persistence';
|
|
|
298
291
|
export type { RecordCache, RecordChange } from './state/records';
|
|
299
292
|
export type {
|
|
300
293
|
CreateLabStoreOptions,
|
|
294
|
+
LabDensity,
|
|
301
295
|
LabDocument,
|
|
302
296
|
LabMode,
|
|
303
297
|
LabStoreState,
|
package/src/lab/Lab.tsx
CHANGED
|
@@ -30,7 +30,7 @@ import { type OpenedLabStore, openLabStore } from '../state/openLabStore';
|
|
|
30
30
|
import { PersistenceContext } from '../state/Persistence';
|
|
31
31
|
import { createRecordCache } from '../state/records';
|
|
32
32
|
import { createLabStore, type LabStore } from '../state/store';
|
|
33
|
-
import type { LabMode, StorageAdapter, TrialRecord } from '../state/types';
|
|
33
|
+
import type { LabDensity, LabMode, StorageAdapter, TrialRecord } from '../state/types';
|
|
34
34
|
import { useOpenOnce, useWarnIgnoredChange } from '../state/useOpenOnce';
|
|
35
35
|
import { usePersistedState } from '../state/usePersistedState';
|
|
36
36
|
import { SurfaceCanvasContext, SurfaceContext } from '../surface/SurfaceContext';
|
|
@@ -64,6 +64,10 @@ interface LabBaseProps {
|
|
|
64
64
|
instruments: InstrumentList;
|
|
65
65
|
defaultInstrument: string;
|
|
66
66
|
mode?: LabMode;
|
|
67
|
+
/** How much room the lab's chrome takes. Default `'comfortable'`; a lab whose
|
|
68
|
+
* window is the whole app, rather than a panel beside one, reads better at
|
|
69
|
+
* `'roomy'`. The trials' own contents are unaffected. */
|
|
70
|
+
density?: LabDensity;
|
|
67
71
|
/** Rendered while a stored lab loads. Default: the lab's empty shell. */
|
|
68
72
|
fallback?: ReactNode;
|
|
69
73
|
/**
|
|
@@ -175,12 +179,17 @@ async function openStoredLab(
|
|
|
175
179
|
function LabFallback({
|
|
176
180
|
title,
|
|
177
181
|
mode,
|
|
182
|
+
density,
|
|
178
183
|
pages,
|
|
179
184
|
path,
|
|
180
|
-
}: Pick<LabBaseProps, 'title' | 'mode' | 'pages' | 'path'>) {
|
|
185
|
+
}: Pick<LabBaseProps, 'title' | 'mode' | 'density' | 'pages' | 'path'>) {
|
|
181
186
|
const resolvedMode = useResolvedMode(mode ?? 'auto');
|
|
182
187
|
return (
|
|
183
|
-
<ThemeProvider
|
|
188
|
+
<ThemeProvider
|
|
189
|
+
theme={interstellarTheme}
|
|
190
|
+
selection={{ mode: resolvedMode, density: density ?? 'comfortable' }}
|
|
191
|
+
className="lk-lab"
|
|
192
|
+
>
|
|
184
193
|
<LabShell
|
|
185
194
|
title={title ?? 'Labkit'}
|
|
186
195
|
mode={mode}
|
|
@@ -289,7 +298,13 @@ export function Lab(props: LabProps) {
|
|
|
289
298
|
if (!opened) {
|
|
290
299
|
if (props.fallback !== undefined) return props.fallback;
|
|
291
300
|
return (
|
|
292
|
-
<LabFallback
|
|
301
|
+
<LabFallback
|
|
302
|
+
title={props.title}
|
|
303
|
+
mode={props.mode}
|
|
304
|
+
density={props.density}
|
|
305
|
+
pages={props.pages}
|
|
306
|
+
path={props.path}
|
|
307
|
+
/>
|
|
293
308
|
);
|
|
294
309
|
}
|
|
295
310
|
return <LabRuntime {...props} opened={opened} />;
|
|
@@ -298,6 +313,7 @@ export function Lab(props: LabProps) {
|
|
|
298
313
|
function LabRuntime({
|
|
299
314
|
instruments,
|
|
300
315
|
mode,
|
|
316
|
+
density,
|
|
301
317
|
nebula,
|
|
302
318
|
title,
|
|
303
319
|
pages,
|
|
@@ -575,7 +591,7 @@ function LabRuntime({
|
|
|
575
591
|
<LabContext.Provider value={contextValue}>
|
|
576
592
|
<ThemeProvider
|
|
577
593
|
theme={interstellarTheme}
|
|
578
|
-
selection={{ mode: resolvedMode }}
|
|
594
|
+
selection={{ mode: resolvedMode, density: density ?? 'comfortable' }}
|
|
579
595
|
className="lk-lab"
|
|
580
596
|
style={backdropStyle}
|
|
581
597
|
>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
.lk-fps-meter {
|
|
2
2
|
display: inline-flex;
|
|
3
3
|
align-items: center;
|
|
4
|
+
white-space: nowrap;
|
|
4
5
|
gap: var(--wzl-space-xs);
|
|
5
6
|
font-family: var(--wzl-font-mono);
|
|
6
7
|
font-size: var(--wzl-font-size-sm);
|
|
@@ -9,6 +10,10 @@
|
|
|
9
10
|
|
|
10
11
|
.lk-fps-meter-value {
|
|
11
12
|
color: var(--wzl-fg);
|
|
12
|
-
|
|
13
|
+
/* Sized for the widest rate it can hold rather than the one it holds now:
|
|
14
|
+
a 240Hz display reads three digits, and a readout that grows into them
|
|
15
|
+
pushes whatever shares its row. */
|
|
16
|
+
min-width: 3ch;
|
|
17
|
+
font-variant-numeric: tabular-nums;
|
|
13
18
|
text-align: right;
|
|
14
19
|
}
|
|
@@ -9,9 +9,17 @@ describe('FpsMeter', () => {
|
|
|
9
9
|
|
|
10
10
|
test('renders an FPS label', () => {
|
|
11
11
|
render(<FpsMeter />);
|
|
12
|
-
expect(screen.getByText(
|
|
12
|
+
expect(screen.getByText('FPS')).toBeInTheDocument();
|
|
13
13
|
});
|
|
14
14
|
|
|
15
|
+
/* The label and the rate are separate elements so the rate can hold a fixed
|
|
16
|
+
width; a text query spanning both would go back to matching one box. */
|
|
17
|
+
const rate = (): HTMLElement => {
|
|
18
|
+
const el = document.querySelector('.lk-fps-meter-value');
|
|
19
|
+
if (!el) throw new Error('no rate readout');
|
|
20
|
+
return el as HTMLElement;
|
|
21
|
+
};
|
|
22
|
+
|
|
15
23
|
test('uses lk-fps-meter class', () => {
|
|
16
24
|
const { container } = render(<FpsMeter />);
|
|
17
25
|
expect((container.firstChild as HTMLElement).className).toBe('lk-fps-meter');
|
|
@@ -31,7 +39,7 @@ describe('FpsMeter', () => {
|
|
|
31
39
|
raf?.(16.67);
|
|
32
40
|
raf?.(33.34);
|
|
33
41
|
});
|
|
34
|
-
expect(
|
|
42
|
+
expect(rate().textContent).toMatch(/^\d+$/);
|
|
35
43
|
|
|
36
44
|
vi.unstubAllGlobals();
|
|
37
45
|
});
|
|
@@ -59,7 +67,7 @@ describe('FpsMeter', () => {
|
|
|
59
67
|
run(16.67);
|
|
60
68
|
run(33.34);
|
|
61
69
|
});
|
|
62
|
-
const before =
|
|
70
|
+
const before = rate().textContent;
|
|
63
71
|
|
|
64
72
|
// An hour hidden. The frame that lands on resume is not a 60-minute frame,
|
|
65
73
|
// and must not enter the rolling average as one.
|
|
@@ -74,7 +82,7 @@ describe('FpsMeter', () => {
|
|
|
74
82
|
run(3_600_016.67);
|
|
75
83
|
});
|
|
76
84
|
|
|
77
|
-
expect(
|
|
85
|
+
expect(rate().textContent).toBe(before);
|
|
78
86
|
|
|
79
87
|
setHidden(false);
|
|
80
88
|
vi.unstubAllGlobals();
|
|
@@ -42,7 +42,12 @@ export function FpsMeter() {
|
|
|
42
42
|
|
|
43
43
|
return (
|
|
44
44
|
<div className="lk-fps-meter" ref={rootRef}>
|
|
45
|
-
<span className="lk-fps-meter-
|
|
45
|
+
<span className="lk-fps-meter-label">FPS</span>
|
|
46
|
+
{/* The number is its own cell because it is the only part that changes:
|
|
47
|
+
in one box with the label, every digit it gains or loses resizes the
|
|
48
|
+
readout, and the status bar it sits in wraps to a second line and
|
|
49
|
+
back — which in a small trial resizes the window. */}
|
|
50
|
+
<span className="lk-fps-meter-value">{fps}</span>
|
|
46
51
|
</div>
|
|
47
52
|
);
|
|
48
53
|
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
.lk-status-bar {
|
|
2
2
|
display: flex;
|
|
3
3
|
align-items: center;
|
|
4
|
+
/* One line, always. A bar that grows a second line takes it from the canvas
|
|
5
|
+
above it, and a readout that changes width would do that on and off as it
|
|
6
|
+
ticks. Too narrow for its contents clips them instead. */
|
|
7
|
+
overflow: hidden;
|
|
4
8
|
gap: var(--wzl-space-md);
|
|
5
9
|
padding: var(--wzl-space-xs) var(--wzl-space-sm);
|
|
6
10
|
background: var(--wzl-surface-raised);
|
|
@@ -13,6 +17,8 @@
|
|
|
13
17
|
.lk-status-bar-section {
|
|
14
18
|
display: inline-flex;
|
|
15
19
|
align-items: center;
|
|
20
|
+
white-space: nowrap;
|
|
21
|
+
min-width: 0;
|
|
16
22
|
gap: var(--wzl-space-xs);
|
|
17
23
|
padding: 0 var(--wzl-space-xs);
|
|
18
24
|
border-right: 1px solid var(--wzl-line-subtle);
|
package/src/state/types.ts
CHANGED
|
@@ -80,6 +80,11 @@ export interface SavedSnapshot {
|
|
|
80
80
|
/** `auto` follows the OS; the other two are an explicit choice. */
|
|
81
81
|
export type LabMode = 'auto' | 'light' | 'dark';
|
|
82
82
|
|
|
83
|
+
/** How much room the lab's own chrome takes — the theme's density axis, named
|
|
84
|
+
* the way the theme names it. A lab's instruments are unaffected: this sizes
|
|
85
|
+
* the shell around them. */
|
|
86
|
+
export type LabDensity = 'compact' | 'comfortable' | 'roomy';
|
|
87
|
+
|
|
83
88
|
/** Everything a lab persists: its trials, its saved snapshots, and the
|
|
84
89
|
* chosen color mode. */
|
|
85
90
|
export interface LabStoreState {
|
|
@@ -126,7 +131,7 @@ export interface TrialStateHandle<TS, TC> {
|
|
|
126
131
|
setState: (next: TS | ((prev: TS) => TS)) => void;
|
|
127
132
|
config: TC;
|
|
128
133
|
/** The config as stored, before any auto path is resolved. What the control
|
|
129
|
-
* panel renders, so
|
|
134
|
+
* panel renders, so an auto row pins back to the value it was holding. */
|
|
130
135
|
raw: TC;
|
|
131
136
|
/** Dotted paths this trial has unpinned. */
|
|
132
137
|
auto: ReadonlySet<string>;
|
package/src/state/view.test.ts
CHANGED
|
@@ -5,13 +5,13 @@ import { openLabStore } from './openLabStore';
|
|
|
5
5
|
import { createLabStore } from './store';
|
|
6
6
|
import { as2DView, DEFAULT_VIEW, normalize2DView, withZoom } from './view';
|
|
7
7
|
|
|
8
|
-
interface
|
|
8
|
+
interface ArbitraryTrialView {
|
|
9
9
|
yaw: number;
|
|
10
10
|
pitch: number;
|
|
11
11
|
distance: number;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
const orbit:
|
|
14
|
+
const orbit: ArbitraryTrialView = { yaw: 1.1, pitch: 0.3, distance: 9 };
|
|
15
15
|
|
|
16
16
|
describe('as2DView', () => {
|
|
17
17
|
it('accepts the 2D shape', () => {
|
|
@@ -54,7 +54,7 @@ describe('a trial view labkit does not interpret', () => {
|
|
|
54
54
|
|
|
55
55
|
store.getState().updateTrialView('w1', orbit);
|
|
56
56
|
|
|
57
|
-
expect(store.getState().trials[0]?.view as
|
|
57
|
+
expect(store.getState().trials[0]?.view as ArbitraryTrialView).toEqual(orbit);
|
|
58
58
|
});
|
|
59
59
|
|
|
60
60
|
it('round-trips that view through persistence', async () => {
|
|
@@ -76,7 +76,7 @@ describe('a trial view labkit does not interpret', () => {
|
|
|
76
76
|
storageKey: 'view-b',
|
|
77
77
|
storage: createMemoryAdapter(backing),
|
|
78
78
|
});
|
|
79
|
-
expect(hydrated.store.getState().trials[0]?.view as
|
|
79
|
+
expect(hydrated.store.getState().trials[0]?.view as ArbitraryTrialView).toEqual(orbit);
|
|
80
80
|
});
|
|
81
81
|
});
|
|
82
82
|
|
|
@@ -93,16 +93,20 @@ describe('an auto config path', () => {
|
|
|
93
93
|
render: () => null,
|
|
94
94
|
canvas: { layers: [{ id: 'main', draw }] },
|
|
95
95
|
});
|
|
96
|
-
|
|
96
|
+
const { container } = render(
|
|
97
|
+
<Lab instruments={[instrument as Instrument]} defaultInstrument="Auto" />,
|
|
98
|
+
);
|
|
97
99
|
await new Promise((r) => requestAnimationFrame(() => r(null)));
|
|
98
100
|
|
|
99
101
|
expect(draw).toHaveBeenCalledWith(
|
|
100
102
|
expect.anything(),
|
|
101
103
|
expect.objectContaining({ config: { gap: 18 } }),
|
|
102
104
|
);
|
|
103
|
-
// The panel is handed the raw config, but
|
|
104
|
-
// resolver decided
|
|
105
|
-
|
|
105
|
+
// The panel is handed the raw config, but an auto row holds what the
|
|
106
|
+
// resolver decided, so pinning it keeps the number the instrument drew.
|
|
107
|
+
// The row hides its control while auto, which takes it out of the
|
|
108
|
+
// accessibility tree — so this reads the element rather than the role.
|
|
109
|
+
expect(container.querySelector('input[type=range]')).toHaveValue('18');
|
|
106
110
|
});
|
|
107
111
|
});
|
|
108
112
|
|
|
@@ -218,7 +218,7 @@ describe('a config written as auto', () => {
|
|
|
218
218
|
const { container } = render(<Lab instruments={[instrument]} defaultInstrument="Ghosted" />);
|
|
219
219
|
expect(screen.getByRole('button', { name: /Gap/ })).toHaveAttribute('aria-pressed', 'false');
|
|
220
220
|
expect(screen.getByRole('button', { name: /Width/ })).toHaveAttribute('aria-pressed', 'true');
|
|
221
|
-
// 432 / 24.
|
|
221
|
+
// 432 / 24. An auto row holds what the resolver decided rather than
|
|
222
222
|
// the pinned 12 underneath it, and un-pinning writes back what it drew.
|
|
223
223
|
expect((container.querySelector('input[type=range]') as HTMLInputElement).value).toBe('18');
|
|
224
224
|
});
|