@weasel-js/labkit 1.1.0 → 1.3.0-pre.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -0
- package/dist/_dts/{DrawCommand-BkZztJsW.d.ts → DrawCommand-DBB45NfN.d.ts} +244 -58
- package/dist/_dts/PrefsForm-BYa6cWnO.d.ts +201 -0
- package/dist/_dts/index-DgcNdEdh.d.ts +314 -0
- package/dist/_dts/types-1Sdxy_Pv.d.ts +90 -0
- package/dist/_dts/types-AHlQxBNN.d.ts +162 -0
- package/dist/_dts/types-DJ79Tg5J.d.ts +56 -0
- package/dist/_dts/{useTrialState-DYe2vUwN.d.ts → useTrialState-CsGMjhu9.d.ts} +6 -92
- package/dist/canvas/index.d.ts +8 -3
- package/dist/canvas/index.js +2 -1
- package/dist/chrome/index.d.ts +86 -0
- package/dist/chrome/index.js +8 -0
- package/dist/chrome/index.js.map +1 -0
- package/dist/{chunk-3TYUJR7Z.js → chunk-4TMMVIDM.js} +9 -3
- package/dist/chunk-4TMMVIDM.js.map +1 -0
- package/dist/chunk-HFDVGF4X.js +170 -0
- package/dist/chunk-HFDVGF4X.js.map +1 -0
- package/dist/chunk-LN6JDUGB.js +106 -0
- package/dist/chunk-LN6JDUGB.js.map +1 -0
- package/dist/{chunk-574LJAV4.js → chunk-NS54R52R.js} +31 -21
- package/dist/chunk-NS54R52R.js.map +1 -0
- package/dist/{chunk-NRKWVTVT.js → chunk-O2BULFHX.js} +5 -14
- package/dist/chunk-O2BULFHX.js.map +1 -0
- package/dist/chunk-O7NVSCLN.js +266 -0
- package/dist/chunk-O7NVSCLN.js.map +1 -0
- package/dist/chunk-PO6L3NSH.js +366 -0
- package/dist/chunk-PO6L3NSH.js.map +1 -0
- package/dist/chunk-PQJ5B2U2.js +27 -0
- package/dist/chunk-PQJ5B2U2.js.map +1 -0
- package/dist/chunk-RVNN4CHQ.js +212 -0
- package/dist/chunk-RVNN4CHQ.js.map +1 -0
- package/dist/chunk-TLGRYALP.js +6151 -0
- package/dist/chunk-TLGRYALP.js.map +1 -0
- package/dist/{chunk-SFL7NFKN.js → chunk-V7PRSIRQ.js} +15 -7
- package/dist/chunk-V7PRSIRQ.js.map +1 -0
- package/dist/{chunk-DJLDIRFN.js → chunk-Z6HJ5FEM.js} +9626 -6050
- package/dist/chunk-Z6HJ5FEM.js.map +1 -0
- package/dist/chunk-ZZAYVX4X.js +505 -0
- package/dist/chunk-ZZAYVX4X.js.map +1 -0
- package/dist/controls/index.d.ts +19 -5
- package/dist/controls/index.js +3 -1
- package/dist/dragdrop/index.d.ts +5 -2
- package/dist/index.d.ts +307 -107
- package/dist/index.js +797 -540
- package/dist/index.js.map +1 -1
- package/dist/job/index.d.ts +13 -0
- package/dist/job/index.js +3 -0
- package/dist/job/index.js.map +1 -0
- package/dist/layers/index.d.ts +6 -3
- package/dist/layers/index.js +2 -1
- package/dist/passthrough/weasel-canvas.d.ts +69 -13
- package/dist/passthrough/weasel-canvas.js +1 -1
- package/dist/passthrough/weasel-ui.d.ts +3804 -3661
- package/dist/passthrough/weasel-ui.js +2 -2
- package/dist/primitives/index.d.ts +102 -8
- package/dist/primitives/index.js +4 -1
- package/dist/state/index.d.ts +3 -2
- package/dist/state/index.js +2 -2
- package/dist/styles.css +543 -144
- package/dist/surface/index.d.ts +77 -0
- package/dist/surface/index.js +4 -0
- package/dist/surface/index.js.map +1 -0
- package/dist/ui/layers/index.js +4 -3
- package/dist/undo/index.d.ts +5 -2
- package/package.json +24 -7
- package/src/canvas/AGENTS.md +12 -0
- package/src/canvas/CanvasStack.tsx +6 -2
- package/src/canvas/useLayerScheduler.ts +22 -15
- package/src/canvas/useOrbit.test.ts +71 -0
- package/src/canvas/useOrbit.ts +149 -0
- package/src/canvas/usePanZoom.test.ts +80 -0
- package/src/canvas/usePanZoom.ts +16 -2
- package/src/chrome/ChromeRegions.stories.tsx +106 -0
- package/src/chrome/builtins.test.ts +153 -0
- package/src/chrome/builtins.tsx +205 -0
- package/src/chrome/index.ts +25 -0
- package/src/chrome/merge.test.ts +51 -0
- package/src/chrome/merge.ts +52 -0
- package/src/chrome/regions/PaletteRegion.less +14 -0
- package/src/chrome/regions/PaletteRegion.test.tsx +36 -0
- package/src/chrome/regions/PaletteRegion.tsx +41 -0
- package/src/chrome/regions/SidebarRegion.tsx +56 -0
- package/src/chrome/regions/StatusRegion.tsx +31 -0
- package/src/chrome/regions/TitleBarRegion.tsx +41 -0
- package/src/chrome/regions/ToolbarRegion.test.tsx +82 -0
- package/src/chrome/regions/ToolbarRegion.tsx +71 -0
- package/src/chrome/regions/ViewportRegion.less +42 -0
- package/src/chrome/regions/ViewportRegion.tsx +37 -0
- package/src/chrome/regions/regions.test.tsx +81 -0
- package/src/chrome/types.ts +127 -0
- package/src/config/builder.test.ts +87 -0
- package/src/config/builder.ts +224 -0
- package/src/config/fromConfigField.test.ts +93 -0
- package/src/config/fromConfigField.ts +74 -0
- package/src/config/index.ts +22 -0
- package/src/config/resolve.test.ts +120 -0
- package/src/config/resolve.ts +67 -0
- package/src/config/rules.ts +68 -0
- package/src/config/types.ts +108 -0
- package/src/config/useConfigSchema.ts +30 -0
- package/src/config/visible.ts +20 -0
- package/src/controls/ControlPanel.less +4 -58
- package/src/controls/ControlPanel.stories.tsx +86 -0
- package/src/controls/ControlPanel.test.tsx +223 -10
- package/src/controls/ControlPanel.tsx +202 -203
- package/src/dragdrop/DragDropRuntime.tsx +36 -18
- package/src/dragdrop/DragGhost.less +1 -1
- package/src/dragdrop/Palette.less +1 -1
- package/src/index.test.ts +18 -0
- package/src/index.ts +38 -3
- package/src/instrument/SineWave.smoke.test.tsx +9 -1
- package/src/instrument/defineInstrument.test.ts +38 -1
- package/src/instrument/defineInstrument.ts +31 -4
- package/src/instrument/index.ts +1 -0
- package/src/instrument/types.ts +38 -3
- package/src/instrument/validateConfigSchema.test.ts +13 -3
- package/src/instrument/validateConfigSchema.ts +7 -10
- package/src/job/index.ts +3 -0
- package/src/job/types.ts +47 -0
- package/src/job/useJob.test.tsx +210 -0
- package/src/job/useJob.ts +134 -0
- package/src/lab/Lab.chrome.test.tsx +65 -0
- package/src/lab/Lab.stories.tsx +1 -2
- package/src/lab/Lab.tsx +58 -15
- package/src/lab/LabContext.ts +6 -0
- package/src/lab/LabFullChrome.stories.tsx +308 -0
- package/src/lab/LabHeader.test.tsx +42 -0
- package/src/lab/LabHeader.tsx +57 -0
- package/src/lab/LabPalette.test.tsx +35 -0
- package/src/lab/LabPalette.tsx +34 -0
- package/src/lab/LabShell.less +50 -3
- package/src/lab/Workspace.less +1 -30
- package/src/lab/Workspace.surface.test.tsx +49 -0
- package/src/lab/Workspace.test.tsx +20 -10
- package/src/lab/Workspace.tsx +19 -9
- package/src/lab/index.ts +2 -0
- package/src/layers/AGENTS.md +1 -1
- package/src/layers/LayerList.less +31 -7
- package/src/layers/LayerList.tsx +17 -4
- package/src/passthrough/weasel-ui.ts +23 -0
- package/src/primitives/DragHandleGlyph.tsx +29 -0
- package/src/primitives/FloatingPanel.less +28 -0
- package/src/primitives/FloatingPanel.stories.tsx +46 -0
- package/src/primitives/FloatingPanel.test.tsx +240 -0
- package/src/primitives/FloatingPanel.tsx +190 -0
- package/src/primitives/FpsMeter.test.tsx +44 -0
- package/src/primitives/FpsMeter.tsx +20 -9
- package/src/primitives/JobProgress.less +69 -0
- package/src/primitives/JobProgress.stories.tsx +48 -0
- package/src/primitives/JobProgress.tsx +65 -0
- package/src/primitives/Legend.less +47 -0
- package/src/primitives/Legend.stories.tsx +32 -0
- package/src/primitives/Legend.test.tsx +57 -0
- package/src/primitives/Legend.tsx +40 -0
- package/src/primitives/Sidebar.less +32 -2
- package/src/primitives/StatusBar.less +7 -0
- package/src/primitives/StatusBar.tsx +10 -3
- package/src/primitives/Toolbar.less +50 -3
- package/src/primitives/Toolbar.test.tsx +67 -1
- package/src/primitives/Toolbar.tsx +72 -6
- package/src/primitives/ZoomControl.less +31 -0
- package/src/primitives/ZoomControl.tsx +76 -0
- package/src/primitives/floating.entry.test.tsx +14 -0
- package/src/primitives/index.ts +10 -2
- package/src/primitives/useRovingTabIndex.test.ts +27 -0
- package/src/primitives/useRovingTabIndex.ts +79 -0
- package/src/state/store.ts +16 -2
- package/src/state/toolSlot.test.ts +36 -0
- package/src/state/types.ts +8 -2
- package/src/state/view.test.ts +127 -0
- package/src/state/view.ts +18 -0
- package/src/styles.less +6 -0
- package/src/surface/AGENTS.md +64 -0
- package/src/surface/SurfaceContext.ts +5 -0
- package/src/surface/composeRects.test.ts +50 -0
- package/src/surface/composeRects.ts +19 -0
- package/src/surface/deviceRect.test.ts +40 -0
- package/src/surface/deviceRect.ts +19 -0
- package/src/surface/index.ts +7 -0
- package/src/surface/rect.ts +16 -0
- package/src/surface/useSurfaceTile.test.tsx +67 -0
- package/src/surface/useSurfaceTile.ts +32 -0
- package/src/surface/useTiledSurface.test.tsx +231 -0
- package/src/surface/useTiledSurface.ts +161 -0
- package/src/test-setup.ts +26 -0
- package/src/theme/base.less +19 -2
- package/src/theme/interstellar.tokens.json +6 -2
- package/src/tools/types.ts +24 -0
- package/src/trial/Trial.config.test.tsx +113 -0
- package/src/trial/Trial.job.test.tsx +73 -0
- package/src/trial/Trial.less +121 -4
- package/src/trial/Trial.stories.tsx +9 -1
- package/src/trial/Trial.test.tsx +67 -96
- package/src/trial/Trial.tsx +112 -45
- package/src/trial/TrialChrome.tsx +110 -64
- package/src/trial/TrialDragContext.ts +10 -0
- package/src/trial/TrialTitleBar.tsx +41 -0
- package/src/trial/index.ts +1 -15
- package/src/trial/trialChrome.borders.test.ts +38 -0
- package/src/ui/layers/LayerStack.less +9 -9
- package/src/ui/layers/LayerStack.tsx +1 -13
- package/src/ui/properties/CurveField.less +2 -2
- package/src/ui/properties/CurveField.test.tsx +11 -11
- package/src/ui/properties/PropertyGroup.less +2 -2
- package/src/ui/properties/PropertyPanel.less +55 -41
- package/src/ui/properties/SpeechBalloonPanels.stories.tsx +4 -2
- package/src/ui/properties/storyLayouts.tsx +2 -2
- package/dist/_dts/index-iAP6XbH3.d.ts +0 -132
- package/dist/_dts/types-x92Kfeme.d.ts +0 -62
- package/dist/chunk-3TYUJR7Z.js.map +0 -1
- package/dist/chunk-574LJAV4.js.map +0 -1
- package/dist/chunk-5R2ATYPJ.js +0 -201
- package/dist/chunk-5R2ATYPJ.js.map +0 -1
- package/dist/chunk-C6GJKPUI.js +0 -5986
- package/dist/chunk-C6GJKPUI.js.map +0 -1
- package/dist/chunk-DJLDIRFN.js.map +0 -1
- package/dist/chunk-NRKWVTVT.js.map +0 -1
- package/dist/chunk-PMAU3SEE.js +0 -126
- package/dist/chunk-PMAU3SEE.js.map +0 -1
- package/dist/chunk-SFL7NFKN.js.map +0 -1
- package/src/instrument/capabilityDetector.test.ts +0 -64
- package/src/instrument/capabilityDetector.ts +0 -20
- package/src/trial/DefaultSidebar.tsx +0 -29
- package/src/trial/DefaultStatusBar.tsx +0 -19
- package/src/trial/DefaultToolbar.tsx +0 -92
- package/src/trial/slotTypes.ts +0 -53
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { toDeviceRect } from './deviceRect';
|
|
3
|
+
|
|
4
|
+
const onGrid = (v: number, dpr: number) => Math.abs(v * dpr - Math.round(v * dpr)) < 1e-9;
|
|
5
|
+
|
|
6
|
+
describe('toDeviceRect', () => {
|
|
7
|
+
it('measures y from the bottom of the surface', () => {
|
|
8
|
+
const r = toDeviceRect({ x: 0, y: 0, w: 40, h: 10 }, 100, 1);
|
|
9
|
+
expect(r.y).toBe(90);
|
|
10
|
+
expect(r.h).toBe(10);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('leaves x and width alone', () => {
|
|
14
|
+
const r = toDeviceRect({ x: 5, y: 0, w: 40, h: 10 }, 100, 1);
|
|
15
|
+
expect(r.x).toBe(5);
|
|
16
|
+
expect(r.w).toBe(40);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('puts every edge on the device grid', () => {
|
|
20
|
+
const r = toDeviceRect({ x: 33.3, y: 7.7, w: 33.3, h: 21.4 }, 100, 2);
|
|
21
|
+
expect(onGrid(r.x, 2)).toBe(true);
|
|
22
|
+
expect(onGrid(r.x + r.w, 2)).toBe(true);
|
|
23
|
+
expect(onGrid(r.y, 2)).toBe(true);
|
|
24
|
+
expect(onGrid(r.y + r.h, 2)).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('does not strand a column between neighbours', () => {
|
|
28
|
+
const a = toDeviceRect({ x: 0, y: 0, w: 33.3333, h: 10 }, 100, 2);
|
|
29
|
+
const b = toDeviceRect({ x: 33.3333, y: 0, w: 33.3333, h: 10 }, 100, 2);
|
|
30
|
+
const c = toDeviceRect({ x: 66.6666, y: 0, w: 33.3334, h: 10 }, 100, 2);
|
|
31
|
+
expect(a.x + a.w).toBe(b.x);
|
|
32
|
+
expect(b.x + b.w).toBe(c.x);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('snaps a stacked pair without overlapping them', () => {
|
|
36
|
+
const top = toDeviceRect({ x: 0, y: 0, w: 10, h: 33.3333 }, 100, 3);
|
|
37
|
+
const bottom = toDeviceRect({ x: 0, y: 33.3333, w: 10, h: 33.3333 }, 100, 3);
|
|
38
|
+
expect(bottom.y + bottom.h).toBe(top.y);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Rect } from './rect';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A DOM rect as a GL viewport rect: origin at the bottom-left, every edge snapped
|
|
5
|
+
* to the device-pixel grid. Still CSS pixels, because three.js applies its own
|
|
6
|
+
* pixel ratio — snapping here is what stops a tile and its neighbour rounding
|
|
7
|
+
* apart and leaving a hairline column between them.
|
|
8
|
+
*/
|
|
9
|
+
export function toDeviceRect(rect: Rect, surfaceHeight: number, dpr: number): Rect {
|
|
10
|
+
const snap = (v: number) => Math.round(v * dpr) / dpr;
|
|
11
|
+
const x = snap(rect.x);
|
|
12
|
+
const y = snap(surfaceHeight - rect.y - rect.h);
|
|
13
|
+
return {
|
|
14
|
+
x,
|
|
15
|
+
y,
|
|
16
|
+
w: snap(rect.x + rect.w) - x,
|
|
17
|
+
h: snap(surfaceHeight - rect.y) - y,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { composeRects, rectsEqual } from './composeRects';
|
|
2
|
+
export { toDeviceRect } from './deviceRect';
|
|
3
|
+
export type { Box, Rect } from './rect';
|
|
4
|
+
export { SurfaceContext } from './SurfaceContext';
|
|
5
|
+
export { useSurface, useSurfaceOptional, useSurfaceTile } from './useSurfaceTile';
|
|
6
|
+
export type { SurfaceFrame, SurfaceHandle, UseTiledSurfaceOptions } from './useTiledSurface';
|
|
7
|
+
export { useTiledSurface } from './useTiledSurface';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** A box in CSS pixels, measured from the surface's own top-left corner. */
|
|
2
|
+
export interface Rect {
|
|
3
|
+
x: number;
|
|
4
|
+
y: number;
|
|
5
|
+
w: number;
|
|
6
|
+
h: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** The subset of `DOMRect` this package reads. Accepting the subset rather than
|
|
10
|
+
* `DOMRect` is what lets the pure functions be tested without a DOM. */
|
|
11
|
+
export interface Box {
|
|
12
|
+
left: number;
|
|
13
|
+
top: number;
|
|
14
|
+
width: number;
|
|
15
|
+
height: number;
|
|
16
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { render } from '@testing-library/react';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { SurfaceContext } from './SurfaceContext';
|
|
4
|
+
import { useSurface, useSurfaceOptional, useSurfaceTile } from './useSurfaceTile';
|
|
5
|
+
import type { SurfaceHandle } from './useTiledSurface';
|
|
6
|
+
|
|
7
|
+
function fakeHandle(): SurfaceHandle {
|
|
8
|
+
return {
|
|
9
|
+
invalidate: vi.fn(),
|
|
10
|
+
invalidateAll: vi.fn(),
|
|
11
|
+
invalidateRects: vi.fn(),
|
|
12
|
+
registerTile: vi.fn(),
|
|
13
|
+
containerRef: vi.fn(),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function Tile({ id }: { id: string }) {
|
|
18
|
+
const ref = useSurfaceTile(id);
|
|
19
|
+
return <div ref={ref} data-testid={id} />;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('useSurfaceTile', () => {
|
|
23
|
+
it('registers its element with the surface on mount', () => {
|
|
24
|
+
const handle = fakeHandle();
|
|
25
|
+
const { getByTestId } = render(
|
|
26
|
+
<SurfaceContext.Provider value={handle}>
|
|
27
|
+
<Tile id="a" />
|
|
28
|
+
</SurfaceContext.Provider>,
|
|
29
|
+
);
|
|
30
|
+
expect(handle.registerTile).toHaveBeenCalledWith('a', getByTestId('a'));
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('unregisters on unmount', () => {
|
|
34
|
+
const handle = fakeHandle();
|
|
35
|
+
const { unmount } = render(
|
|
36
|
+
<SurfaceContext.Provider value={handle}>
|
|
37
|
+
<Tile id="a" />
|
|
38
|
+
</SurfaceContext.Provider>,
|
|
39
|
+
);
|
|
40
|
+
unmount();
|
|
41
|
+
expect(handle.registerTile).toHaveBeenCalledWith('a', null);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('is inert with no surface above it, so a 2D lab is unaffected', () => {
|
|
45
|
+
expect(() => render(<Tile id="a" />)).not.toThrow();
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('useSurface', () => {
|
|
50
|
+
it('throws outside a provider, because a caller asking for it needs one', () => {
|
|
51
|
+
function Consumer() {
|
|
52
|
+
useSurface();
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
expect(() => render(<Consumer />)).toThrow(/requires a surface/i);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('returns null from the optional form outside a provider', () => {
|
|
59
|
+
let seen: SurfaceHandle | null | undefined;
|
|
60
|
+
function Consumer() {
|
|
61
|
+
seen = useSurfaceOptional();
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
render(<Consumer />);
|
|
65
|
+
expect(seen).toBeNull();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useCallback, useContext } from 'react';
|
|
2
|
+
import { SurfaceContext } from './SurfaceContext';
|
|
3
|
+
import type { SurfaceHandle } from './useTiledSurface';
|
|
4
|
+
|
|
5
|
+
/** The surface above, or null. Use this where a surface is genuinely optional. */
|
|
6
|
+
export function useSurfaceOptional(): SurfaceHandle | null {
|
|
7
|
+
return useContext(SurfaceContext);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** The surface above. Throws where a caller cannot work without one. */
|
|
11
|
+
export function useSurface(): SurfaceHandle {
|
|
12
|
+
const surface = useContext(SurfaceContext);
|
|
13
|
+
if (!surface) throw new Error('[labkit] useSurface requires a surface owner above it');
|
|
14
|
+
return surface;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A ref callback that publishes this element's rect to the surface under `id`.
|
|
19
|
+
*
|
|
20
|
+
* Attach it to whichever element the surface should draw into — that is not
|
|
21
|
+
* necessarily the trial's own element, since a trial may hold a drawn pane beside
|
|
22
|
+
* an undrawn one, or none at all.
|
|
23
|
+
*/
|
|
24
|
+
export function useSurfaceTile(id: string): (el: HTMLElement | null) => void {
|
|
25
|
+
const surface = useSurfaceOptional();
|
|
26
|
+
return useCallback(
|
|
27
|
+
(el: HTMLElement | null) => {
|
|
28
|
+
surface?.registerTile(id, el);
|
|
29
|
+
},
|
|
30
|
+
[surface, id],
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { act, render } from '@testing-library/react';
|
|
2
|
+
import { useEffect } from 'react';
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import type { SurfaceFrame, SurfaceHandle } from './useTiledSurface';
|
|
5
|
+
import { useTiledSurface } from './useTiledSurface';
|
|
6
|
+
|
|
7
|
+
/** jsdom measures everything as zero, so each element is given a box by hand. */
|
|
8
|
+
function stubBox(el: HTMLElement, left: number, top: number, width: number, height: number): void {
|
|
9
|
+
vi.spyOn(el, 'getBoundingClientRect').mockReturnValue({
|
|
10
|
+
left,
|
|
11
|
+
top,
|
|
12
|
+
width,
|
|
13
|
+
height,
|
|
14
|
+
right: left + width,
|
|
15
|
+
bottom: top + height,
|
|
16
|
+
x: left,
|
|
17
|
+
y: top,
|
|
18
|
+
toJSON: () => ({}),
|
|
19
|
+
} as DOMRect);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface HarnessProps {
|
|
23
|
+
frames: SurfaceFrame[];
|
|
24
|
+
onHandle: (h: SurfaceHandle) => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function Harness({ frames, onHandle }: HarnessProps) {
|
|
28
|
+
const surface = useTiledSurface({
|
|
29
|
+
onFrame: (f) => {
|
|
30
|
+
frames.push(f);
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
onHandle(surface);
|
|
35
|
+
}, [surface, onHandle]);
|
|
36
|
+
return (
|
|
37
|
+
<div
|
|
38
|
+
data-testid="stage"
|
|
39
|
+
ref={(el) => {
|
|
40
|
+
if (!el) return;
|
|
41
|
+
stubBox(el, 0, 0, 800, 600);
|
|
42
|
+
surface.containerRef(el);
|
|
43
|
+
}}
|
|
44
|
+
>
|
|
45
|
+
<div
|
|
46
|
+
data-testid="a"
|
|
47
|
+
ref={(el) => {
|
|
48
|
+
if (!el) return;
|
|
49
|
+
stubBox(el, 0, 0, 400, 600);
|
|
50
|
+
surface.registerTile('a', el);
|
|
51
|
+
}}
|
|
52
|
+
/>
|
|
53
|
+
<div
|
|
54
|
+
data-testid="b"
|
|
55
|
+
ref={(el) => {
|
|
56
|
+
if (!el) return;
|
|
57
|
+
stubBox(el, 400, 0, 400, 600);
|
|
58
|
+
surface.registerTile('b', el);
|
|
59
|
+
}}
|
|
60
|
+
/>
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Runs every pending rAF callback. */
|
|
66
|
+
function flushFrames(): void {
|
|
67
|
+
act(() => {
|
|
68
|
+
vi.advanceTimersByTime(64);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
describe('useTiledSurface', () => {
|
|
73
|
+
beforeEach(() => {
|
|
74
|
+
vi.useFakeTimers();
|
|
75
|
+
vi.stubGlobal('requestAnimationFrame', (cb: FrameRequestCallback) =>
|
|
76
|
+
setTimeout(() => cb(0), 16),
|
|
77
|
+
);
|
|
78
|
+
vi.stubGlobal('cancelAnimationFrame', (id: number) => clearTimeout(id));
|
|
79
|
+
});
|
|
80
|
+
afterEach(() => {
|
|
81
|
+
vi.useRealTimers();
|
|
82
|
+
vi.unstubAllGlobals();
|
|
83
|
+
vi.restoreAllMocks();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('coalesces three invalidations in one tick into a single frame', () => {
|
|
87
|
+
const frames: SurfaceFrame[] = [];
|
|
88
|
+
let handle: SurfaceHandle | null = null;
|
|
89
|
+
render(
|
|
90
|
+
<Harness
|
|
91
|
+
frames={frames}
|
|
92
|
+
onHandle={(h) => {
|
|
93
|
+
handle = h;
|
|
94
|
+
}}
|
|
95
|
+
/>,
|
|
96
|
+
);
|
|
97
|
+
flushFrames();
|
|
98
|
+
frames.length = 0;
|
|
99
|
+
|
|
100
|
+
act(() => {
|
|
101
|
+
handle?.invalidate('a');
|
|
102
|
+
handle?.invalidate('b');
|
|
103
|
+
handle?.invalidate('a');
|
|
104
|
+
});
|
|
105
|
+
flushFrames();
|
|
106
|
+
|
|
107
|
+
expect(frames).toHaveLength(1);
|
|
108
|
+
expect([...(frames[0]?.dirty ?? [])].sort()).toEqual(['a', 'b']);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('fires nothing on a clean tick', () => {
|
|
112
|
+
const frames: SurfaceFrame[] = [];
|
|
113
|
+
render(<Harness frames={frames} onHandle={() => {}} />);
|
|
114
|
+
flushFrames();
|
|
115
|
+
frames.length = 0;
|
|
116
|
+
flushFrames();
|
|
117
|
+
expect(frames).toHaveLength(0);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('carries every tile rect, not only the dirty ones', () => {
|
|
121
|
+
const frames: SurfaceFrame[] = [];
|
|
122
|
+
let handle: SurfaceHandle | null = null;
|
|
123
|
+
render(
|
|
124
|
+
<Harness
|
|
125
|
+
frames={frames}
|
|
126
|
+
onHandle={(h) => {
|
|
127
|
+
handle = h;
|
|
128
|
+
}}
|
|
129
|
+
/>,
|
|
130
|
+
);
|
|
131
|
+
flushFrames();
|
|
132
|
+
frames.length = 0;
|
|
133
|
+
|
|
134
|
+
act(() => handle?.invalidate('a'));
|
|
135
|
+
flushFrames();
|
|
136
|
+
|
|
137
|
+
expect([...(frames[0]?.dirty ?? [])]).toEqual(['a']);
|
|
138
|
+
expect(frames[0]?.rects.get('b')).toEqual({ x: 400, y: 0, w: 400, h: 600 });
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('reports the surface size and dpr', () => {
|
|
142
|
+
vi.stubGlobal('devicePixelRatio', 3);
|
|
143
|
+
const frames: SurfaceFrame[] = [];
|
|
144
|
+
let handle: SurfaceHandle | null = null;
|
|
145
|
+
render(
|
|
146
|
+
<Harness
|
|
147
|
+
frames={frames}
|
|
148
|
+
onHandle={(h) => {
|
|
149
|
+
handle = h;
|
|
150
|
+
}}
|
|
151
|
+
/>,
|
|
152
|
+
);
|
|
153
|
+
flushFrames();
|
|
154
|
+
frames.length = 0;
|
|
155
|
+
|
|
156
|
+
act(() => handle?.invalidateAll());
|
|
157
|
+
flushFrames();
|
|
158
|
+
|
|
159
|
+
expect(frames[0]?.dpr).toBe(3);
|
|
160
|
+
expect(frames[0]?.size).toEqual({ width: 800, height: 600 });
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('marks every tile dirty when the dpr changes', () => {
|
|
164
|
+
vi.stubGlobal('devicePixelRatio', 1);
|
|
165
|
+
const frames: SurfaceFrame[] = [];
|
|
166
|
+
let handle: SurfaceHandle | null = null;
|
|
167
|
+
render(
|
|
168
|
+
<Harness
|
|
169
|
+
frames={frames}
|
|
170
|
+
onHandle={(h) => {
|
|
171
|
+
handle = h;
|
|
172
|
+
}}
|
|
173
|
+
/>,
|
|
174
|
+
);
|
|
175
|
+
flushFrames();
|
|
176
|
+
frames.length = 0;
|
|
177
|
+
|
|
178
|
+
vi.stubGlobal('devicePixelRatio', 2);
|
|
179
|
+
act(() => handle?.invalidateRects());
|
|
180
|
+
flushFrames();
|
|
181
|
+
|
|
182
|
+
expect([...(frames[0]?.dirty ?? [])].sort()).toEqual(['a', 'b']);
|
|
183
|
+
expect(frames[0]?.dpr).toBe(2);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('re-measures on invalidateRects, which is how a moved tile is caught', () => {
|
|
187
|
+
const frames: SurfaceFrame[] = [];
|
|
188
|
+
let handle: SurfaceHandle | null = null;
|
|
189
|
+
const { getByTestId } = render(
|
|
190
|
+
<Harness
|
|
191
|
+
frames={frames}
|
|
192
|
+
onHandle={(h) => {
|
|
193
|
+
handle = h;
|
|
194
|
+
}}
|
|
195
|
+
/>,
|
|
196
|
+
);
|
|
197
|
+
flushFrames();
|
|
198
|
+
frames.length = 0;
|
|
199
|
+
|
|
200
|
+
// A sibling reflow slides tile b left. ResizeObserver sees no size change.
|
|
201
|
+
stubBox(getByTestId('b'), 320, 0, 400, 600);
|
|
202
|
+
|
|
203
|
+
act(() => handle?.invalidateRects());
|
|
204
|
+
flushFrames();
|
|
205
|
+
|
|
206
|
+
expect(frames[0]?.rects.get('b')).toEqual({ x: 320, y: 0, w: 400, h: 600 });
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('drops a tile that unregisters', () => {
|
|
210
|
+
const frames: SurfaceFrame[] = [];
|
|
211
|
+
let handle: SurfaceHandle | null = null;
|
|
212
|
+
render(
|
|
213
|
+
<Harness
|
|
214
|
+
frames={frames}
|
|
215
|
+
onHandle={(h) => {
|
|
216
|
+
handle = h;
|
|
217
|
+
}}
|
|
218
|
+
/>,
|
|
219
|
+
);
|
|
220
|
+
flushFrames();
|
|
221
|
+
frames.length = 0;
|
|
222
|
+
|
|
223
|
+
act(() => {
|
|
224
|
+
handle?.registerTile('b', null);
|
|
225
|
+
handle?.invalidate('a');
|
|
226
|
+
});
|
|
227
|
+
flushFrames();
|
|
228
|
+
|
|
229
|
+
expect(frames[0]?.rects.has('b')).toBe(false);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { useVisibleRaf } from '@weasel-js/core';
|
|
2
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
3
|
+
import { composeRects, rectsEqual } from './composeRects';
|
|
4
|
+
import type { Box, Rect } from './rect';
|
|
5
|
+
|
|
6
|
+
/** What a surface owner is handed once per animation frame. `rects` carries every
|
|
7
|
+
* tile, not only the dirty ones: a scissored draw has to know where it is drawing
|
|
8
|
+
* relative to a surface that may have resized under it. */
|
|
9
|
+
export interface SurfaceFrame {
|
|
10
|
+
dirty: ReadonlySet<string>;
|
|
11
|
+
rects: ReadonlyMap<string, Rect>;
|
|
12
|
+
dpr: number;
|
|
13
|
+
size: { width: number; height: number };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** The invalidators and the two ref callbacks that publish geometry. */
|
|
17
|
+
export interface SurfaceHandle {
|
|
18
|
+
/** Mark one tile for redraw. */
|
|
19
|
+
invalidate: (id: string) => void;
|
|
20
|
+
/** Mark every tile — what a resize or a tile-set change means. */
|
|
21
|
+
invalidateAll: () => void;
|
|
22
|
+
/** Re-measure before the next frame. The escape hatch for a host that knows it
|
|
23
|
+
* moved something a ResizeObserver cannot see. */
|
|
24
|
+
invalidateRects: () => void;
|
|
25
|
+
registerTile: (id: string, el: HTMLElement | null) => void;
|
|
26
|
+
containerRef: (el: HTMLElement | null) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface UseTiledSurfaceOptions {
|
|
30
|
+
onFrame: (frame: SurfaceFrame) => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function useTiledSurface({ onFrame }: UseTiledSurfaceOptions): SurfaceHandle {
|
|
34
|
+
const container = useRef<HTMLElement | null>(null);
|
|
35
|
+
const tiles = useRef(new Map<string, HTMLElement>());
|
|
36
|
+
const rects = useRef(new Map<string, Rect>());
|
|
37
|
+
const dirty = useRef(new Set<string>());
|
|
38
|
+
const needsMeasure = useRef(true);
|
|
39
|
+
const observer = useRef<ResizeObserver | null>(null);
|
|
40
|
+
const lastDpr = useRef(0);
|
|
41
|
+
|
|
42
|
+
// Held in a ref so a caller passing an inline closure does not re-create every
|
|
43
|
+
// callback below on each render.
|
|
44
|
+
const onFrameRef = useRef(onFrame);
|
|
45
|
+
onFrameRef.current = onFrame;
|
|
46
|
+
|
|
47
|
+
const measure = useCallback((): boolean => {
|
|
48
|
+
const el = container.current;
|
|
49
|
+
if (!el) return false;
|
|
50
|
+
const boxes = new Map<string, Box>();
|
|
51
|
+
for (const [id, tile] of tiles.current) boxes.set(id, tile.getBoundingClientRect());
|
|
52
|
+
const next = composeRects(el.getBoundingClientRect(), boxes);
|
|
53
|
+
let changed = next.size !== rects.current.size;
|
|
54
|
+
for (const [id, rect] of next) {
|
|
55
|
+
if (!rectsEqual(rects.current.get(id), rect)) changed = true;
|
|
56
|
+
}
|
|
57
|
+
rects.current = next;
|
|
58
|
+
return changed;
|
|
59
|
+
}, []);
|
|
60
|
+
|
|
61
|
+
const frameLoop = useVisibleRaf(
|
|
62
|
+
() => {
|
|
63
|
+
const el = container.current;
|
|
64
|
+
if (!el) {
|
|
65
|
+
dirty.current.clear();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (needsMeasure.current) {
|
|
69
|
+
needsMeasure.current = false;
|
|
70
|
+
if (measure()) for (const id of rects.current.keys()) dirty.current.add(id);
|
|
71
|
+
}
|
|
72
|
+
const dpr = globalThis.devicePixelRatio ?? 1;
|
|
73
|
+
if (dpr !== lastDpr.current) {
|
|
74
|
+
lastDpr.current = dpr;
|
|
75
|
+
for (const id of rects.current.keys()) dirty.current.add(id);
|
|
76
|
+
}
|
|
77
|
+
if (dirty.current.size === 0) return;
|
|
78
|
+
const box = el.getBoundingClientRect();
|
|
79
|
+
onFrameRef.current({
|
|
80
|
+
dirty: new Set(dirty.current),
|
|
81
|
+
rects: new Map(rects.current),
|
|
82
|
+
dpr,
|
|
83
|
+
size: { width: box.width, height: box.height },
|
|
84
|
+
});
|
|
85
|
+
dirty.current.clear();
|
|
86
|
+
},
|
|
87
|
+
// The host attaches its container through `containerRef`, which may land
|
|
88
|
+
// well after this hook's first effect; the gate re-resolves it per request.
|
|
89
|
+
{ target: () => container.current },
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const schedule = useCallback(() => {
|
|
93
|
+
frameLoop.request();
|
|
94
|
+
}, [frameLoop]);
|
|
95
|
+
|
|
96
|
+
const invalidate = useCallback(
|
|
97
|
+
(id: string) => {
|
|
98
|
+
dirty.current.add(id);
|
|
99
|
+
schedule();
|
|
100
|
+
},
|
|
101
|
+
[schedule],
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const invalidateAll = useCallback(() => {
|
|
105
|
+
for (const id of tiles.current.keys()) dirty.current.add(id);
|
|
106
|
+
schedule();
|
|
107
|
+
}, [schedule]);
|
|
108
|
+
|
|
109
|
+
const invalidateRects = useCallback(() => {
|
|
110
|
+
needsMeasure.current = true;
|
|
111
|
+
schedule();
|
|
112
|
+
}, [schedule]);
|
|
113
|
+
|
|
114
|
+
const registerTile = useCallback(
|
|
115
|
+
(id: string, el: HTMLElement | null) => {
|
|
116
|
+
const known = tiles.current.get(id);
|
|
117
|
+
if (known === el) return;
|
|
118
|
+
if (known) observer.current?.unobserve(known);
|
|
119
|
+
if (el) {
|
|
120
|
+
tiles.current.set(id, el);
|
|
121
|
+
observer.current?.observe(el);
|
|
122
|
+
} else {
|
|
123
|
+
tiles.current.delete(id);
|
|
124
|
+
rects.current.delete(id);
|
|
125
|
+
dirty.current.delete(id);
|
|
126
|
+
}
|
|
127
|
+
needsMeasure.current = true;
|
|
128
|
+
schedule();
|
|
129
|
+
},
|
|
130
|
+
[schedule],
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
const containerRef = useCallback(
|
|
134
|
+
(el: HTMLElement | null) => {
|
|
135
|
+
if (container.current === el) return;
|
|
136
|
+
if (container.current) observer.current?.unobserve(container.current);
|
|
137
|
+
container.current = el;
|
|
138
|
+
if (el) observer.current?.observe(el);
|
|
139
|
+
needsMeasure.current = true;
|
|
140
|
+
schedule();
|
|
141
|
+
},
|
|
142
|
+
[schedule],
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
const ro = new ResizeObserver(() => {
|
|
147
|
+
needsMeasure.current = true;
|
|
148
|
+
schedule();
|
|
149
|
+
});
|
|
150
|
+
observer.current = ro;
|
|
151
|
+
if (container.current) ro.observe(container.current);
|
|
152
|
+
for (const el of tiles.current.values()) ro.observe(el);
|
|
153
|
+
return () => {
|
|
154
|
+
ro.disconnect();
|
|
155
|
+
observer.current = null;
|
|
156
|
+
frameLoop.cancel();
|
|
157
|
+
};
|
|
158
|
+
}, [schedule, frameLoop]);
|
|
159
|
+
|
|
160
|
+
return { invalidate, invalidateAll, invalidateRects, registerTile, containerRef };
|
|
161
|
+
}
|
package/src/test-setup.ts
CHANGED
|
@@ -31,6 +31,32 @@ if (typeof globalThis.ResizeObserver === 'undefined') {
|
|
|
31
31
|
} as unknown as typeof ResizeObserver;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
// jsdom ships no PointerEvent, so `fireEvent.pointerDown` builds a bare Event
|
|
35
|
+
// and drops clientX/clientY — a drag handler then reads `undefined` and computes
|
|
36
|
+
// NaN deltas, which the DOM rejects as invalid CSS rather than reporting.
|
|
37
|
+
// MouseEvent already carries the coordinate fields.
|
|
38
|
+
if (typeof globalThis.PointerEvent === 'undefined') {
|
|
39
|
+
class PointerEventPolyfill extends MouseEvent {
|
|
40
|
+
readonly pointerId: number;
|
|
41
|
+
readonly pointerType: string;
|
|
42
|
+
readonly isPrimary: boolean;
|
|
43
|
+
constructor(type: string, init: PointerEventInit = {}) {
|
|
44
|
+
super(type, init);
|
|
45
|
+
this.pointerId = init.pointerId ?? 1;
|
|
46
|
+
this.pointerType = init.pointerType ?? 'mouse';
|
|
47
|
+
this.isPrimary = init.isPrimary ?? true;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
globalThis.PointerEvent = PointerEventPolyfill as unknown as typeof PointerEvent;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// jsdom implements neither side of pointer capture.
|
|
54
|
+
if (typeof Element.prototype.setPointerCapture === 'undefined') {
|
|
55
|
+
Element.prototype.setPointerCapture = () => {};
|
|
56
|
+
Element.prototype.releasePointerCapture = () => {};
|
|
57
|
+
Element.prototype.hasPointerCapture = () => false;
|
|
58
|
+
}
|
|
59
|
+
|
|
34
60
|
afterEach(() => {
|
|
35
61
|
cleanup();
|
|
36
62
|
});
|
package/src/theme/base.less
CHANGED
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
|
|
17
17
|
// Resets and global element defaults.
|
|
18
18
|
.lk-root {
|
|
19
|
+
// labkit's own vocabulary over the theme's raw scale — named for the thing
|
|
20
|
+
// rather than the step, so "the workspace is too tight" has one place to go.
|
|
21
|
+
--lk-workspace-pad: var(--wzl-space-sm);
|
|
22
|
+
|
|
19
23
|
font-family: var(--wzl-font-ui);
|
|
20
24
|
font-size: var(--wzl-font-size);
|
|
21
25
|
font-weight: var(--wzl-font-weight-light);
|
|
@@ -26,8 +30,15 @@
|
|
|
26
30
|
*, *::before, *::after {
|
|
27
31
|
box-sizing: border-box;
|
|
28
32
|
}
|
|
33
|
+
}
|
|
29
34
|
|
|
30
|
-
|
|
35
|
+
// Defaults for labkit's own bare markup. `.lk-root` is inside :where() too, so
|
|
36
|
+
// the whole selector scores (0,0,0): a weasel-ui module class beats it on
|
|
37
|
+
// specificity instead of on which stylesheet happened to be injected last.
|
|
38
|
+
// Deliberate overrides of a weasel-ui component go the other way — see the
|
|
39
|
+
// `.lk-root` prefix convention below.
|
|
40
|
+
:where(.lk-root) {
|
|
41
|
+
:where(button) {
|
|
31
42
|
font: inherit;
|
|
32
43
|
color: inherit;
|
|
33
44
|
background: var(--wzl-surface-raised);
|
|
@@ -55,11 +66,17 @@
|
|
|
55
66
|
}
|
|
56
67
|
}
|
|
57
68
|
|
|
58
|
-
input[type="range"] {
|
|
69
|
+
:where(input[type="range"]) {
|
|
59
70
|
accent-color: var(--wzl-accent);
|
|
60
71
|
}
|
|
61
72
|
}
|
|
62
73
|
|
|
74
|
+
// The `.lk-root` prefix convention: a labkit class handed to a weasel-ui
|
|
75
|
+
// component through `className` lands beside that component's own CSS-module
|
|
76
|
+
// class at equal specificity, so injection order — not intent — decides the
|
|
77
|
+
// winner. Prefixing with `.lk-root` scores (0,2,0) and settles it. Use it only
|
|
78
|
+
// where labkit means to override the component; plain `.lk-*` elsewhere.
|
|
79
|
+
|
|
63
80
|
// Cosmic background applied to the Lab root. The starscape sits as a fixed
|
|
64
81
|
// pseudo-element so panning the trial doesn't shift the field of stars.
|
|
65
82
|
.lk-lab {
|
|
@@ -28,7 +28,9 @@
|
|
|
28
28
|
"fg-subtle": { "$value": "#5a5a78" },
|
|
29
29
|
"accent": { "$value": "#b08adb" },
|
|
30
30
|
"accent-hover": { "$value": "#c6a3ee" },
|
|
31
|
-
"focus-ring": { "$value": "rgba(176, 138, 219, 0.45)" }
|
|
31
|
+
"focus-ring": { "$value": "rgba(176, 138, 219, 0.45)" },
|
|
32
|
+
"shadow": { "$value": "rgba(0, 0, 0, 0.7)" },
|
|
33
|
+
"border-raised": { "$value": "rgba(255, 255, 255, 0.36)" }
|
|
32
34
|
},
|
|
33
35
|
"gradient": {
|
|
34
36
|
"$type": "gradient",
|
|
@@ -50,7 +52,9 @@
|
|
|
50
52
|
"fg-subtle": { "$value": "#a89b85" },
|
|
51
53
|
"accent": { "$value": "#a86f3c" },
|
|
52
54
|
"accent-hover": { "$value": "#c2854f" },
|
|
53
|
-
"focus-ring": { "$value": "rgba(168, 111, 60, 0.35)" }
|
|
55
|
+
"focus-ring": { "$value": "rgba(168, 111, 60, 0.35)" },
|
|
56
|
+
"shadow": { "$value": "rgba(80, 60, 40, 0.18)" },
|
|
57
|
+
"border-raised": { "$value": "#9e8b71" }
|
|
54
58
|
}
|
|
55
59
|
}
|
|
56
60
|
}
|