@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,4 +1,4 @@
|
|
|
1
|
-
import { act, render } from '@testing-library/react';
|
|
1
|
+
import { act, render, renderHook } from '@testing-library/react';
|
|
2
2
|
import { useEffect } from 'react';
|
|
3
3
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
4
|
import type { SurfaceFrame, SurfaceHandle } from './useTiledSurface';
|
|
@@ -286,4 +286,234 @@ describe('useTiledSurface', () => {
|
|
|
286
286
|
flushFrames();
|
|
287
287
|
expect(paint).toHaveBeenCalledTimes(1);
|
|
288
288
|
});
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* The surface owner resizes the shared buffer from inside `onFrame`, which
|
|
292
|
+
* costs every tile its pixels. Both tests below are about the two ways that
|
|
293
|
+
* loss goes unrepaired.
|
|
294
|
+
*/
|
|
295
|
+
describe('when the buffer loses its contents', () => {
|
|
296
|
+
it('keeps an invalidation the owner makes from inside onFrame', () => {
|
|
297
|
+
let handle: SurfaceHandle | null = null;
|
|
298
|
+
let claimAll = false;
|
|
299
|
+
function Owner() {
|
|
300
|
+
const surface = useTiledSurface({
|
|
301
|
+
onFrame: () => {
|
|
302
|
+
if (!claimAll) return;
|
|
303
|
+
claimAll = false;
|
|
304
|
+
// What `Lab` does after sizing the canvas: the buffer is blank, so
|
|
305
|
+
// every tile owes a repaint, not only the one that moved.
|
|
306
|
+
surface.invalidateAll();
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
handle = surface;
|
|
310
|
+
return (
|
|
311
|
+
<div
|
|
312
|
+
ref={(el) => {
|
|
313
|
+
if (!el) return;
|
|
314
|
+
stubBox(el, 0, 0, 800, 600);
|
|
315
|
+
surface.containerRef(el);
|
|
316
|
+
}}
|
|
317
|
+
>
|
|
318
|
+
<div
|
|
319
|
+
ref={(el) => {
|
|
320
|
+
if (!el) return;
|
|
321
|
+
stubBox(el, 0, 0, 400, 600);
|
|
322
|
+
surface.registerTile('a', el);
|
|
323
|
+
}}
|
|
324
|
+
/>
|
|
325
|
+
<div
|
|
326
|
+
ref={(el) => {
|
|
327
|
+
if (!el) return;
|
|
328
|
+
stubBox(el, 400, 0, 400, 600);
|
|
329
|
+
surface.registerTile('b', el);
|
|
330
|
+
}}
|
|
331
|
+
/>
|
|
332
|
+
</div>
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
render(<Owner />);
|
|
336
|
+
flushFrames();
|
|
337
|
+
|
|
338
|
+
const paintA = vi.fn();
|
|
339
|
+
act(() => {
|
|
340
|
+
handle?.registerPainter('a', paintA);
|
|
341
|
+
handle?.registerPainter('b', vi.fn());
|
|
342
|
+
});
|
|
343
|
+
flushFrames();
|
|
344
|
+
paintA.mockClear();
|
|
345
|
+
|
|
346
|
+
// Only b is dirty; the owner resizes the buffer during the frame, which
|
|
347
|
+
// blanks a as well.
|
|
348
|
+
claimAll = true;
|
|
349
|
+
act(() => handle?.invalidate('b'));
|
|
350
|
+
flushFrames();
|
|
351
|
+
flushFrames();
|
|
352
|
+
|
|
353
|
+
expect(paintA).toHaveBeenCalled();
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it('tells the owner when the tile geometry changed, so it can clear', () => {
|
|
357
|
+
const frames: SurfaceFrame[] = [];
|
|
358
|
+
let handle: SurfaceHandle | null = null;
|
|
359
|
+
const { getByTestId } = render(
|
|
360
|
+
<Harness
|
|
361
|
+
frames={frames}
|
|
362
|
+
onHandle={(h) => {
|
|
363
|
+
handle = h;
|
|
364
|
+
}}
|
|
365
|
+
/>,
|
|
366
|
+
);
|
|
367
|
+
flushFrames();
|
|
368
|
+
frames.length = 0;
|
|
369
|
+
|
|
370
|
+
// A plain repaint leaves every tile where it was.
|
|
371
|
+
act(() => handle?.invalidate('a'));
|
|
372
|
+
flushFrames();
|
|
373
|
+
expect(frames[0]?.retiled).toBe(false);
|
|
374
|
+
frames.length = 0;
|
|
375
|
+
|
|
376
|
+
// A tile moving strands its old pixels in the gutter; nothing but the
|
|
377
|
+
// owner can clear those, and this is how it hears about it.
|
|
378
|
+
stubBox(getByTestId('b'), 300, 0, 400, 600);
|
|
379
|
+
act(() => handle?.invalidateRects());
|
|
380
|
+
flushFrames();
|
|
381
|
+
expect(frames[0]?.retiled).toBe(true);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it('runs every registered clear before any painter, and only on a re-tile', () => {
|
|
385
|
+
const frames: SurfaceFrame[] = [];
|
|
386
|
+
let handle: SurfaceHandle | null = null;
|
|
387
|
+
const { getByTestId } = render(
|
|
388
|
+
<Harness
|
|
389
|
+
frames={frames}
|
|
390
|
+
onHandle={(h) => {
|
|
391
|
+
handle = h;
|
|
392
|
+
}}
|
|
393
|
+
/>,
|
|
394
|
+
);
|
|
395
|
+
flushFrames();
|
|
396
|
+
|
|
397
|
+
const order: string[] = [];
|
|
398
|
+
const clearA = vi.fn(() => order.push('clearA'));
|
|
399
|
+
const clearB = vi.fn(() => order.push('clearB'));
|
|
400
|
+
act(() => {
|
|
401
|
+
handle?.registerClear('a', clearA);
|
|
402
|
+
handle?.registerClear('b', clearB);
|
|
403
|
+
handle?.registerPainter('a', () => order.push('paintA'));
|
|
404
|
+
handle?.registerPainter('b', () => order.push('paintB'));
|
|
405
|
+
});
|
|
406
|
+
flushFrames();
|
|
407
|
+
order.length = 0;
|
|
408
|
+
clearA.mockClear();
|
|
409
|
+
|
|
410
|
+
// A repaint of one tile leaves every other tile's pixels valid.
|
|
411
|
+
act(() => handle?.invalidate('a'));
|
|
412
|
+
flushFrames();
|
|
413
|
+
expect(clearA).not.toHaveBeenCalled();
|
|
414
|
+
expect(order).toEqual(['paintA']);
|
|
415
|
+
order.length = 0;
|
|
416
|
+
|
|
417
|
+
stubBox(getByTestId('b'), 300, 0, 400, 600);
|
|
418
|
+
act(() => handle?.invalidateRects());
|
|
419
|
+
flushFrames();
|
|
420
|
+
|
|
421
|
+
// Both tenants share the buffer, so both clears run — and a clear after
|
|
422
|
+
// a painter would erase the tile that just drew.
|
|
423
|
+
expect(order.slice(0, 2).sort()).toEqual(['clearA', 'clearB']);
|
|
424
|
+
expect(order.slice(2).sort()).toEqual(['paintA', 'paintB']);
|
|
425
|
+
expect(clearA).toHaveBeenCalledWith({ width: 800, height: 600 }, expect.any(Number));
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it('keeps measuring while a tile is still moving, and stops when it settles', () => {
|
|
429
|
+
const frames: SurfaceFrame[] = [];
|
|
430
|
+
let handle: SurfaceHandle | null = null;
|
|
431
|
+
const { getByTestId } = render(
|
|
432
|
+
<Harness
|
|
433
|
+
frames={frames}
|
|
434
|
+
onHandle={(h) => {
|
|
435
|
+
handle = h;
|
|
436
|
+
}}
|
|
437
|
+
/>,
|
|
438
|
+
);
|
|
439
|
+
flushFrames();
|
|
440
|
+
|
|
441
|
+
const seen: number[] = [];
|
|
442
|
+
act(() => {
|
|
443
|
+
handle?.registerPainter('b', (rect) => seen.push(rect.x));
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
// A panel whose size is already final but whose position is still
|
|
447
|
+
// animating: `ResizeObserver` has nothing left to report, so the only
|
|
448
|
+
// thing that can catch the last 60px is the surface measuring again.
|
|
449
|
+
const b = getByTestId('b');
|
|
450
|
+
const stops = [340, 370, 400, 400, 400];
|
|
451
|
+
let call = 0;
|
|
452
|
+
vi.spyOn(b, 'getBoundingClientRect').mockImplementation(() => {
|
|
453
|
+
const left = stops[Math.min(call++, stops.length - 1)] ?? 0;
|
|
454
|
+
return {
|
|
455
|
+
left,
|
|
456
|
+
top: 0,
|
|
457
|
+
width: 400,
|
|
458
|
+
height: 600,
|
|
459
|
+
right: left + 400,
|
|
460
|
+
bottom: 600,
|
|
461
|
+
x: left,
|
|
462
|
+
y: 0,
|
|
463
|
+
toJSON: () => ({}),
|
|
464
|
+
} as DOMRect;
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
seen.length = 0;
|
|
468
|
+
act(() => handle?.invalidateRects());
|
|
469
|
+
// One flush per animation frame; nothing else invalidates in between.
|
|
470
|
+
flushFrames();
|
|
471
|
+
flushFrames();
|
|
472
|
+
flushFrames();
|
|
473
|
+
flushFrames();
|
|
474
|
+
|
|
475
|
+
expect(seen.at(-1)).toBe(400);
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
it('stops calling a clear that unregisters', () => {
|
|
479
|
+
const frames: SurfaceFrame[] = [];
|
|
480
|
+
let handle: SurfaceHandle | null = null;
|
|
481
|
+
const { getByTestId } = render(
|
|
482
|
+
<Harness
|
|
483
|
+
frames={frames}
|
|
484
|
+
onHandle={(h) => {
|
|
485
|
+
handle = h;
|
|
486
|
+
}}
|
|
487
|
+
/>,
|
|
488
|
+
);
|
|
489
|
+
flushFrames();
|
|
490
|
+
|
|
491
|
+
const clear = vi.fn();
|
|
492
|
+
let off: (() => void) | undefined;
|
|
493
|
+
act(() => {
|
|
494
|
+
off = handle?.registerClear('a', clear);
|
|
495
|
+
});
|
|
496
|
+
stubBox(getByTestId('b'), 310, 0, 400, 600);
|
|
497
|
+
act(() => handle?.invalidateRects());
|
|
498
|
+
flushFrames();
|
|
499
|
+
expect(clear).toHaveBeenCalledTimes(1);
|
|
500
|
+
|
|
501
|
+
act(() => off?.());
|
|
502
|
+
stubBox(getByTestId('b'), 320, 0, 400, 600);
|
|
503
|
+
act(() => handle?.invalidateRects());
|
|
504
|
+
flushFrames();
|
|
505
|
+
expect(clear).toHaveBeenCalledTimes(1);
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
describe('useTiledSurface handle identity', () => {
|
|
511
|
+
it('returns the same handle across renders', () => {
|
|
512
|
+
// A new handle per render reaches useSurfaceTile through context as a new
|
|
513
|
+
// ref callback, which unregisters and re-registers every tile and retiles.
|
|
514
|
+
const { result, rerender } = renderHook(() => useTiledSurface({ onFrame: () => {} }));
|
|
515
|
+
const first = result.current;
|
|
516
|
+
rerender();
|
|
517
|
+
expect(result.current).toBe(first);
|
|
518
|
+
});
|
|
289
519
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useVisibleRaf } from '@weasel-js/core';
|
|
2
|
-
import { useCallback, useEffect, useRef } from 'react';
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
|
3
3
|
import { composeRects, rectsEqual } from './composeRects';
|
|
4
4
|
import type { Box, Rect } from './rect';
|
|
5
5
|
|
|
@@ -14,6 +14,11 @@ export interface SurfaceFrame {
|
|
|
14
14
|
rects: ReadonlyMap<string, Rect>;
|
|
15
15
|
dpr: number;
|
|
16
16
|
size: { width: number; height: number };
|
|
17
|
+
/** The tile geometry changed this frame — a tile moved, resized, appeared or
|
|
18
|
+
* went away, or the dpr did. Every tile is dirty on such a frame, but the
|
|
19
|
+
* pixels a tile vacated are in no tile's scissor now, so the owner clears
|
|
20
|
+
* the whole buffer. `<Lab>` does; a host owning its own surface must too. */
|
|
21
|
+
retiled: boolean;
|
|
17
22
|
}
|
|
18
23
|
|
|
19
24
|
/** The invalidators and the two ref callbacks that publish geometry. */
|
|
@@ -26,6 +31,12 @@ export interface SurfaceHandle {
|
|
|
26
31
|
* moved something a ResizeObserver cannot see. */
|
|
27
32
|
invalidateRects: () => void;
|
|
28
33
|
registerTile: (id: string, el: HTMLElement | null) => void;
|
|
34
|
+
/** Register a tenant's whole-buffer clear. Every one registered runs, before
|
|
35
|
+
* any painter, on a frame where the tile geometry changed — the pixels a
|
|
36
|
+
* tile vacated are in no scissor now, and only the context that drew them
|
|
37
|
+
* can erase them. Backend-agnostic by necessity: labkit owns the canvas,
|
|
38
|
+
* never the context. */
|
|
39
|
+
registerClear: (id: string, clear: SurfaceClear) => () => void;
|
|
29
40
|
/** Subscribe a tile to the frames it is dirty on. A tile paints on its own
|
|
30
41
|
* loop, so this is how the surface wakes one: a resize of the shared buffer
|
|
31
42
|
* clears every tile, not only the one that moved. */
|
|
@@ -40,6 +51,10 @@ export interface SurfaceHandle {
|
|
|
40
51
|
* frame that dirtied it. */
|
|
41
52
|
export type TilePainter = (rect: Rect, frame: SurfaceFrame) => void;
|
|
42
53
|
|
|
54
|
+
/** Erase the whole shared buffer. Device pixels, because that is what a GL
|
|
55
|
+
* viewport takes. */
|
|
56
|
+
export type SurfaceClear = (size: { width: number; height: number }, dpr: number) => void;
|
|
57
|
+
|
|
43
58
|
export interface UseTiledSurfaceOptions {
|
|
44
59
|
onFrame: (frame: SurfaceFrame) => void;
|
|
45
60
|
}
|
|
@@ -50,6 +65,7 @@ export function useTiledSurface({ onFrame }: UseTiledSurfaceOptions): SurfaceHan
|
|
|
50
65
|
const rects = useRef(new Map<string, Rect>());
|
|
51
66
|
const dirty = useRef(new Set<string>());
|
|
52
67
|
const painters = useRef(new Map<string, TilePainter>());
|
|
68
|
+
const clears = useRef(new Map<string, SurfaceClear>());
|
|
53
69
|
const needsMeasure = useRef(true);
|
|
54
70
|
const observer = useRef<ResizeObserver | null>(null);
|
|
55
71
|
const lastDpr = useRef(0);
|
|
@@ -80,13 +96,25 @@ export function useTiledSurface({ onFrame }: UseTiledSurfaceOptions): SurfaceHan
|
|
|
80
96
|
dirty.current.clear();
|
|
81
97
|
return;
|
|
82
98
|
}
|
|
99
|
+
let retiled = false;
|
|
83
100
|
if (needsMeasure.current) {
|
|
84
101
|
needsMeasure.current = false;
|
|
85
|
-
if (measure())
|
|
102
|
+
if (measure()) {
|
|
103
|
+
retiled = true;
|
|
104
|
+
for (const id of rects.current.keys()) dirty.current.add(id);
|
|
105
|
+
// Geometry that changed may still be moving: a panel whose size has
|
|
106
|
+
// settled while its position animates gives `ResizeObserver` nothing
|
|
107
|
+
// more to report, and the tile would hold the frame it was caught
|
|
108
|
+
// mid-flight at. Measure again next frame, and keep going until a
|
|
109
|
+
// measurement finds nothing moved.
|
|
110
|
+
needsMeasure.current = true;
|
|
111
|
+
schedule();
|
|
112
|
+
}
|
|
86
113
|
}
|
|
87
114
|
const dpr = globalThis.devicePixelRatio ?? 1;
|
|
88
115
|
if (dpr !== lastDpr.current) {
|
|
89
116
|
lastDpr.current = dpr;
|
|
117
|
+
retiled = true;
|
|
90
118
|
for (const id of rects.current.keys()) dirty.current.add(id);
|
|
91
119
|
}
|
|
92
120
|
if (dirty.current.size === 0) return;
|
|
@@ -96,14 +124,27 @@ export function useTiledSurface({ onFrame }: UseTiledSurfaceOptions): SurfaceHan
|
|
|
96
124
|
rects: new Map(rects.current),
|
|
97
125
|
dpr,
|
|
98
126
|
size: { width: box.width, height: box.height },
|
|
127
|
+
retiled,
|
|
99
128
|
};
|
|
129
|
+
// Cleared before the owner runs, not after the painters: sizing the
|
|
130
|
+
// buffer blanks every tile, so the owner answers with `invalidateAll`,
|
|
131
|
+
// and a set cleared at the end of the frame would swallow it.
|
|
132
|
+
dirty.current.clear();
|
|
100
133
|
onFrameRef.current(frame);
|
|
101
|
-
// After the owner
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
if (rect) painters.current.get(id)?.(rect, frame);
|
|
134
|
+
// After the owner sized the buffer, before anything paints into it.
|
|
135
|
+
if (retiled) {
|
|
136
|
+
for (const clear of clears.current.values()) clear(frame.size, dpr);
|
|
105
137
|
}
|
|
138
|
+
// After the owner, which is what resized the buffer this frame — and
|
|
139
|
+
// whatever it just dirtied paints now rather than a frame later.
|
|
140
|
+
const painting = new Set(frame.dirty);
|
|
141
|
+
for (const id of dirty.current) painting.add(id);
|
|
106
142
|
dirty.current.clear();
|
|
143
|
+
const paintFrame: SurfaceFrame = { ...frame, dirty: painting };
|
|
144
|
+
for (const id of painting) {
|
|
145
|
+
const rect = frame.rects.get(id);
|
|
146
|
+
if (rect) painters.current.get(id)?.(rect, paintFrame);
|
|
147
|
+
}
|
|
107
148
|
},
|
|
108
149
|
// The host attaches its container through `containerRef`, which may land
|
|
109
150
|
// well after this hook's first effect; the gate re-resolves it per request.
|
|
@@ -151,6 +192,13 @@ export function useTiledSurface({ onFrame }: UseTiledSurfaceOptions): SurfaceHan
|
|
|
151
192
|
[schedule],
|
|
152
193
|
);
|
|
153
194
|
|
|
195
|
+
const registerClear = useCallback((id: string, clear: SurfaceClear) => {
|
|
196
|
+
clears.current.set(id, clear);
|
|
197
|
+
return () => {
|
|
198
|
+
if (clears.current.get(id) === clear) clears.current.delete(id);
|
|
199
|
+
};
|
|
200
|
+
}, []);
|
|
201
|
+
|
|
154
202
|
const registerPainter = useCallback((id: string, paint: TilePainter) => {
|
|
155
203
|
painters.current.set(id, paint);
|
|
156
204
|
return () => {
|
|
@@ -187,13 +235,26 @@ export function useTiledSurface({ onFrame }: UseTiledSurfaceOptions): SurfaceHan
|
|
|
187
235
|
};
|
|
188
236
|
}, [schedule, frameLoop]);
|
|
189
237
|
|
|
190
|
-
return
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
238
|
+
return useMemo(
|
|
239
|
+
() => ({
|
|
240
|
+
invalidate,
|
|
241
|
+
invalidateAll,
|
|
242
|
+
invalidateRects,
|
|
243
|
+
registerTile,
|
|
244
|
+
registerClear,
|
|
245
|
+
registerPainter,
|
|
246
|
+
containerRef,
|
|
247
|
+
getContainer,
|
|
248
|
+
}),
|
|
249
|
+
[
|
|
250
|
+
invalidate,
|
|
251
|
+
invalidateAll,
|
|
252
|
+
invalidateRects,
|
|
253
|
+
registerTile,
|
|
254
|
+
registerClear,
|
|
255
|
+
registerPainter,
|
|
256
|
+
containerRef,
|
|
257
|
+
getContainer,
|
|
258
|
+
],
|
|
259
|
+
);
|
|
199
260
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from '@
|
|
1
|
+
import type { Meta, StoryObj } from '@weasel-js/forge';
|
|
2
2
|
import type { TokenName } from '@weasel-js/theme';
|
|
3
3
|
import { useTheme } from '@weasel-js/theme/react';
|
|
4
4
|
import {
|
|
@@ -18,7 +18,7 @@ const meta: Meta = {
|
|
|
18
18
|
description: {
|
|
19
19
|
component:
|
|
20
20
|
'The interstellar theme: deep-space cosmic gradient + starscape backdrop, glass surfaces, Oswald display type, purple accent. ' +
|
|
21
|
-
'
|
|
21
|
+
'A pins-only theme definition in `src/theme/interstellar.theme.json`, loaded with `defineTheme`; it extends the built-in weasel theme, overriding values rather than adding tokens.',
|
|
22
22
|
},
|
|
23
23
|
},
|
|
24
24
|
},
|
|
@@ -55,16 +55,16 @@ const ACCENT_TOKENS: Swatch[] = [
|
|
|
55
55
|
];
|
|
56
56
|
|
|
57
57
|
const PALETTE_TOKENS: Swatch[] = [
|
|
58
|
+
{ name: 'fuchsia', cssVar: '--wzl-swatch-fuchsia' },
|
|
58
59
|
{ name: 'green', cssVar: '--wzl-swatch-green' },
|
|
59
|
-
{ name: '
|
|
60
|
-
{ name: 'cyan', cssVar: '--wzl-swatch-cyan' },
|
|
61
|
-
{ name: 'gold', cssVar: '--wzl-swatch-gold' },
|
|
60
|
+
{ name: 'sky', cssVar: '--wzl-swatch-sky' },
|
|
62
61
|
{ name: 'amber', cssVar: '--wzl-swatch-amber' },
|
|
62
|
+
{ name: 'teal', cssVar: '--wzl-swatch-teal' },
|
|
63
|
+
{ name: 'red', cssVar: '--wzl-swatch-red' },
|
|
64
|
+
{ name: 'blue', cssVar: '--wzl-swatch-blue' },
|
|
65
|
+
{ name: 'citron', cssVar: '--wzl-swatch-citron' },
|
|
66
|
+
{ name: 'rose', cssVar: '--wzl-swatch-rose' },
|
|
63
67
|
{ name: 'violet', cssVar: '--wzl-swatch-violet' },
|
|
64
|
-
{ name: 'mint', cssVar: '--wzl-swatch-mint' },
|
|
65
|
-
{ name: 'sky', cssVar: '--wzl-swatch-sky' },
|
|
66
|
-
{ name: 'orange', cssVar: '--wzl-swatch-orange' },
|
|
67
|
-
{ name: 'magenta', cssVar: '--wzl-swatch-magenta' },
|
|
68
68
|
];
|
|
69
69
|
|
|
70
70
|
function SwatchGrid({ tokens, title }: { tokens: readonly Swatch[]; title: string }) {
|
|
@@ -1,11 +1,20 @@
|
|
|
1
|
-
import { resolveTheme } from '@weasel-js/theme';
|
|
1
|
+
import { resolveTheme, THEME_SOURCES, type ThemeDefinition } from '@weasel-js/theme';
|
|
2
|
+
import { derive } from '@weasel-js/theme/engine';
|
|
2
3
|
import { describe, expect, it } from 'vitest';
|
|
3
4
|
import { interstellarTheme } from './interstellar';
|
|
5
|
+
import definition from './interstellar.theme.json' with { type: 'json' };
|
|
4
6
|
|
|
5
7
|
describe('interstellarTheme', () => {
|
|
8
|
+
it('derives as a definition extending weasel with no issues', () => {
|
|
9
|
+
const lookup = (name: string) => (name === 'weasel' ? THEME_SOURCES.weasel : undefined);
|
|
10
|
+
for (const mode of ['dark', 'light']) {
|
|
11
|
+
expect(derive(definition as ThemeDefinition, { mode }, lookup).issues, mode).toEqual([]);
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
|
|
6
15
|
it('extends weasel and resolves both modes', () => {
|
|
7
|
-
const dark = resolveTheme(interstellarTheme, 'dark');
|
|
8
|
-
const light = resolveTheme(interstellarTheme, 'light');
|
|
16
|
+
const dark = resolveTheme(interstellarTheme, { mode: 'dark' });
|
|
17
|
+
const light = resolveTheme(interstellarTheme, { mode: 'light' });
|
|
9
18
|
|
|
10
19
|
// Values carried over verbatim from the retired Less.
|
|
11
20
|
expect(dark['--wzl-surface']).toBe('#0a0a14');
|
|
@@ -15,7 +24,7 @@ describe('interstellarTheme', () => {
|
|
|
15
24
|
|
|
16
25
|
// Inherited from weasel — interstellar overrides values, not the token set.
|
|
17
26
|
expect(dark['--wzl-space-md']).toBe('12px');
|
|
18
|
-
expect(dark['--wzl-swatch-
|
|
27
|
+
expect(dark['--wzl-swatch-teal']).toBe('#3ee1cb');
|
|
19
28
|
|
|
20
29
|
// Deliberate divergences from the base.
|
|
21
30
|
expect(dark['--wzl-radius-md']).toBe('6px');
|
|
@@ -24,7 +33,9 @@ describe('interstellarTheme', () => {
|
|
|
24
33
|
});
|
|
25
34
|
|
|
26
35
|
it('carries the cosmic backdrop in dark and drops it in light', () => {
|
|
27
|
-
expect(resolveTheme(interstellarTheme, 'dark')['--wzl-backdrop']).toContain(
|
|
28
|
-
|
|
36
|
+
expect(resolveTheme(interstellarTheme, { mode: 'dark' })['--wzl-backdrop']).toContain(
|
|
37
|
+
'radial-gradient',
|
|
38
|
+
);
|
|
39
|
+
expect(resolveTheme(interstellarTheme, { mode: 'light' })['--wzl-backdrop']).toBe('none');
|
|
29
40
|
});
|
|
30
41
|
});
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "interstellar",
|
|
3
|
+
"extends": "weasel",
|
|
4
|
+
"pins": {
|
|
5
|
+
"radius-md": {
|
|
6
|
+
"value": "6px",
|
|
7
|
+
"type": "dimension"
|
|
8
|
+
},
|
|
9
|
+
"glass-blur": {
|
|
10
|
+
"value": "12px",
|
|
11
|
+
"type": "dimension"
|
|
12
|
+
},
|
|
13
|
+
"font-weight-light": {
|
|
14
|
+
"value": 300,
|
|
15
|
+
"type": "fontWeight"
|
|
16
|
+
},
|
|
17
|
+
"font-weight-medium": {
|
|
18
|
+
"value": 500,
|
|
19
|
+
"type": "fontWeight"
|
|
20
|
+
},
|
|
21
|
+
"font-weight-bold": {
|
|
22
|
+
"value": 700,
|
|
23
|
+
"type": "fontWeight"
|
|
24
|
+
},
|
|
25
|
+
"surface": {
|
|
26
|
+
"by": "mode",
|
|
27
|
+
"dark": {
|
|
28
|
+
"value": "#0a0a14",
|
|
29
|
+
"type": "color"
|
|
30
|
+
},
|
|
31
|
+
"light": {
|
|
32
|
+
"value": "#fafaf7",
|
|
33
|
+
"type": "color"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"surface-raised": {
|
|
37
|
+
"by": "mode",
|
|
38
|
+
"dark": {
|
|
39
|
+
"value": "rgba(20, 20, 36, 0.55)",
|
|
40
|
+
"type": "color"
|
|
41
|
+
},
|
|
42
|
+
"light": {
|
|
43
|
+
"value": "#ffffff",
|
|
44
|
+
"type": "color"
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"surface-sunken": {
|
|
48
|
+
"by": "mode",
|
|
49
|
+
"dark": {
|
|
50
|
+
"value": "#02020a",
|
|
51
|
+
"type": "color"
|
|
52
|
+
},
|
|
53
|
+
"light": {
|
|
54
|
+
"value": "#f0ebe2",
|
|
55
|
+
"type": "color"
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"border": {
|
|
59
|
+
"by": "mode",
|
|
60
|
+
"dark": {
|
|
61
|
+
"value": "rgba(255, 255, 255, 0.14)",
|
|
62
|
+
"type": "color"
|
|
63
|
+
},
|
|
64
|
+
"light": {
|
|
65
|
+
"value": "#c9bba5",
|
|
66
|
+
"type": "color"
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"line-subtle": {
|
|
70
|
+
"by": "mode",
|
|
71
|
+
"dark": {
|
|
72
|
+
"value": "rgba(255, 255, 255, 0.08)",
|
|
73
|
+
"type": "color"
|
|
74
|
+
},
|
|
75
|
+
"light": {
|
|
76
|
+
"value": "rgba(0, 0, 0, 0.08)",
|
|
77
|
+
"type": "color"
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
"fg": {
|
|
81
|
+
"by": "mode",
|
|
82
|
+
"dark": {
|
|
83
|
+
"value": "#e8e8f0",
|
|
84
|
+
"type": "color"
|
|
85
|
+
},
|
|
86
|
+
"light": {
|
|
87
|
+
"value": "#2a2018",
|
|
88
|
+
"type": "color"
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
"fg-muted": {
|
|
92
|
+
"by": "mode",
|
|
93
|
+
"dark": {
|
|
94
|
+
"value": "#a8a8c0",
|
|
95
|
+
"type": "color"
|
|
96
|
+
},
|
|
97
|
+
"light": {
|
|
98
|
+
"value": "#6b5c4a",
|
|
99
|
+
"type": "color"
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
"fg-subtle": {
|
|
103
|
+
"by": "mode",
|
|
104
|
+
"dark": {
|
|
105
|
+
"value": "#5a5a78",
|
|
106
|
+
"type": "color"
|
|
107
|
+
},
|
|
108
|
+
"light": {
|
|
109
|
+
"value": "#a89b85",
|
|
110
|
+
"type": "color"
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
"accent": {
|
|
114
|
+
"by": "mode",
|
|
115
|
+
"dark": {
|
|
116
|
+
"value": "#b08adb",
|
|
117
|
+
"type": "color"
|
|
118
|
+
},
|
|
119
|
+
"light": {
|
|
120
|
+
"value": "#a86f3c",
|
|
121
|
+
"type": "color"
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
"accent-hover": {
|
|
125
|
+
"by": "mode",
|
|
126
|
+
"dark": {
|
|
127
|
+
"value": "#c6a3ee",
|
|
128
|
+
"type": "color"
|
|
129
|
+
},
|
|
130
|
+
"light": {
|
|
131
|
+
"value": "#c2854f",
|
|
132
|
+
"type": "color"
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
"focus-ring": {
|
|
136
|
+
"by": "mode",
|
|
137
|
+
"dark": {
|
|
138
|
+
"value": "rgba(176, 138, 219, 0.45)",
|
|
139
|
+
"type": "color"
|
|
140
|
+
},
|
|
141
|
+
"light": {
|
|
142
|
+
"value": "rgba(168, 111, 60, 0.35)",
|
|
143
|
+
"type": "color"
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
"shadow": {
|
|
147
|
+
"by": "mode",
|
|
148
|
+
"dark": {
|
|
149
|
+
"value": "rgba(0, 0, 0, 0.7)",
|
|
150
|
+
"type": "color"
|
|
151
|
+
},
|
|
152
|
+
"light": {
|
|
153
|
+
"value": "rgba(80, 60, 40, 0.18)",
|
|
154
|
+
"type": "color"
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
"border-strong": {
|
|
158
|
+
"by": "mode",
|
|
159
|
+
"dark": {
|
|
160
|
+
"value": "rgba(255, 255, 255, 0.36)",
|
|
161
|
+
"type": "color"
|
|
162
|
+
},
|
|
163
|
+
"light": {
|
|
164
|
+
"value": "#9e8b71",
|
|
165
|
+
"type": "color"
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
"backdrop": {
|
|
169
|
+
"by": "mode",
|
|
170
|
+
"dark": {
|
|
171
|
+
"value": "radial-gradient(ellipse 60% 80% at 18% 30%, rgba(150, 80, 200, 0.22), transparent 70%), radial-gradient(ellipse 55% 90% at 78% 70%, rgba(220, 100, 140, 0.18), transparent 65%), radial-gradient(ellipse 50% 70% at 50% 50%, rgba(60, 110, 200, 0.18), transparent 75%), radial-gradient(ellipse at center, #0a0a18 0%, #02020a 100%)",
|
|
172
|
+
"type": "gradient"
|
|
173
|
+
},
|
|
174
|
+
"light": {
|
|
175
|
+
"value": "none",
|
|
176
|
+
"type": "gradient"
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import { defineTheme, type Theme, type ThemeInput, weaselTheme } from '@weasel-js/theme';
|
|
2
|
+
import definition from './interstellar.theme.json' with { type: 'json' };
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* labkit's theme: a cosmic dark and a warm parchment light, extending the
|
|
6
|
-
* built-in weasel theme.
|
|
7
|
-
*
|
|
6
|
+
* built-in weasel theme. A pins-only theme definition: nothing in it is
|
|
7
|
+
* derived, so it loads without the engine.
|
|
8
8
|
*/
|
|
9
|
-
export const interstellarTheme: Theme =
|
|
9
|
+
export const interstellarTheme: Theme = defineTheme({
|
|
10
|
+
...definition,
|
|
11
|
+
extends: weaselTheme,
|
|
12
|
+
} as ThemeInput);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { InstrumentList } from '../instrument/types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The tool the lab's slot holds, once its default is applied: a lab with any
|
|
5
|
+
* annotating instrument starts in `pointer`, so a first click reaches the
|
|
6
|
+
* instrument rather than the marks over it.
|
|
7
|
+
*/
|
|
8
|
+
export function resolveLabTool(slot: string | null, instruments: InstrumentList): string | null {
|
|
9
|
+
if (slot !== null) return slot;
|
|
10
|
+
return instruments.some((i) => i.annotations != null) ? 'pointer' : null;
|
|
11
|
+
}
|