@weasel-js/labkit 1.7.3 → 1.8.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 +24 -0
- package/dist/_dts/{CanvasStackContext-Cjy6I-Iv.d.ts → CanvasStackContext-B05ZM04l.d.ts} +3 -1
- package/dist/_dts/{Stage-D6t7o0pF.d.ts → Stage-CIJlbjrE.d.ts} +5 -3
- package/dist/_dts/{index-DCMA4Fy1.d.ts → index-8geQNQet.d.ts} +84 -12
- package/dist/_dts/{index-BTmaRy6a.d.ts → index-DRi3V5sN.d.ts} +6 -4
- package/dist/_dts/{types-BUBSIZuV.d.ts → types-DUfQ0isU.d.ts} +5 -1
- package/dist/_dts/{useTrialState-Bh8bzFgE.d.ts → useTrialState-Ar0AFpCe.d.ts} +94 -87
- package/dist/canvas/index.d.ts +5 -5
- package/dist/canvas/index.js +2 -2
- package/dist/chrome/index.d.ts +3 -3
- package/dist/chrome/index.js +7 -7
- package/dist/{chunk-HBGH344L.js → chunk-4RCVWX4M.js} +55 -18
- package/dist/chunk-4RCVWX4M.js.map +1 -0
- package/dist/{chunk-WIG6XHJ7.js → chunk-6IIKLXBZ.js} +13 -4
- package/dist/chunk-6IIKLXBZ.js.map +1 -0
- package/dist/{chunk-F4ACIK7Z.js → chunk-6Q6ACJ3H.js} +3 -3
- package/dist/{chunk-F4ACIK7Z.js.map → chunk-6Q6ACJ3H.js.map} +1 -1
- package/dist/{chunk-PMLZP7RT.js → chunk-7ALJOVGP.js} +5 -9
- package/dist/chunk-7ALJOVGP.js.map +1 -0
- package/dist/{chunk-XE5HJOPL.js → chunk-7FMZT2WL.js} +3 -3
- package/dist/{chunk-XE5HJOPL.js.map → chunk-7FMZT2WL.js.map} +1 -1
- package/dist/{chunk-XVUKDSYV.js → chunk-AVOILDN2.js} +2 -2
- package/dist/{chunk-XVUKDSYV.js.map → chunk-AVOILDN2.js.map} +1 -1
- package/dist/{chunk-2N7UBSS2.js → chunk-J5MDJEOZ.js} +3 -3
- package/dist/{chunk-2N7UBSS2.js.map → chunk-J5MDJEOZ.js.map} +1 -1
- package/dist/{chunk-LPGBC63T.js → chunk-KHB3KRRQ.js} +7 -7
- package/dist/chunk-KHB3KRRQ.js.map +1 -0
- package/dist/{chunk-IBVRMHW5.js → chunk-NHIO2O3O.js} +31 -30
- package/dist/chunk-NHIO2O3O.js.map +1 -0
- package/dist/{chunk-EESJUSYR.js → chunk-QMAY74TP.js} +56 -5
- package/dist/chunk-QMAY74TP.js.map +1 -0
- package/dist/{chunk-4X44DMRS.js → chunk-RN7T7OBO.js} +46 -24
- package/dist/chunk-RN7T7OBO.js.map +1 -0
- package/dist/{chunk-4TJ34FXA.js → chunk-RXO5UEGT.js} +5 -12
- package/dist/chunk-RXO5UEGT.js.map +1 -0
- package/dist/{chunk-E6NARGU7.js → chunk-SQX35X5V.js} +3 -3
- package/dist/{chunk-E6NARGU7.js.map → chunk-SQX35X5V.js.map} +1 -1
- package/dist/{chunk-YCFKW5RX.js → chunk-T5OEUNCB.js} +16 -2
- package/dist/chunk-T5OEUNCB.js.map +1 -0
- package/dist/{chunk-RL2LOLNI.js → chunk-UX66USTN.js} +2 -2
- package/dist/chunk-UX66USTN.js.map +1 -0
- package/dist/{chunk-IRMPZIYU.js → chunk-V4SMGEYY.js} +6732 -6507
- package/dist/chunk-V4SMGEYY.js.map +1 -0
- package/dist/{chunk-E23Q6A3Y.js → chunk-VCK5T6EP.js} +3 -3
- package/dist/{chunk-E23Q6A3Y.js.map → chunk-VCK5T6EP.js.map} +1 -1
- package/dist/{chunk-T6DLGEJB.js → chunk-XWR2P6MR.js} +23 -6
- package/dist/chunk-XWR2P6MR.js.map +1 -0
- package/dist/{chunk-XSJJTXNA.js → chunk-YZA73U4A.js} +19 -19
- package/dist/chunk-YZA73U4A.js.map +1 -0
- package/dist/config/index.d.ts +12 -5
- package/dist/config/index.js +2 -2
- package/dist/controls/index.d.ts +2 -2
- package/dist/controls/index.js +5 -5
- package/dist/dragdrop/index.d.ts +2 -2
- package/dist/dragdrop/index.js +2 -2
- package/dist/index.d.ts +158 -16
- package/dist/index.js +1036 -415
- package/dist/index.js.map +1 -1
- package/dist/layers/index.d.ts +2 -2
- package/dist/layers/index.js +3 -3
- package/dist/loupe/index.d.ts +176 -28
- package/dist/loupe/index.js +547 -141
- package/dist/loupe/index.js.map +1 -1
- package/dist/overview/index.d.ts +1 -1
- package/dist/overview/index.js +5 -5
- package/dist/passthrough/weasel-ui.js +3 -3
- package/dist/primitives/index.d.ts +4 -19
- package/dist/primitives/index.js +7 -7
- package/dist/state/index.d.ts +6 -8
- package/dist/state/index.js +5 -5
- package/dist/styles.css +181 -44
- package/dist/surface/index.d.ts +20 -1
- package/dist/surface/index.js +2 -2
- package/dist/undo/index.d.ts +19 -8
- package/dist/undo/index.js +1 -1
- package/package.json +11 -11
- package/src/annotations/AnnotationOverlay.abandon.test.tsx +4 -0
- package/src/annotations/AnnotationOverlay.tsx +10 -1
- package/src/annotations/Annotations.less +2 -0
- package/src/annotations/Annotations.overlay.test.tsx +29 -0
- package/src/canvas/CanvasStack.test.tsx +35 -0
- package/src/canvas/CanvasStack.tsx +12 -1
- package/src/canvas/useLayerScheduler.ts +14 -0
- package/src/chrome/builtins.test.ts +14 -0
- package/src/chrome/builtins.tsx +17 -2
- package/src/chrome/regions/StatusRegion.tsx +20 -14
- package/src/chrome/regions/regions.test.tsx +28 -0
- package/src/chrome/types.ts +8 -0
- package/src/clock/clock.browser.test.less +5 -0
- package/src/clock/clock.browser.test.tsx +77 -0
- package/src/clock/clock.integration.test.tsx +95 -0
- package/src/clock/clockRegistry.test.ts +33 -0
- package/src/clock/clockRegistry.ts +46 -0
- package/src/clock/hooks.ts +44 -0
- package/src/clock/index.ts +2 -0
- package/src/clock/trialClock.test.ts +265 -0
- package/src/clock/trialClock.ts +245 -0
- package/src/clock/useClockLoop.ts +45 -0
- package/src/config/builder.ts +11 -1
- package/src/config/resolve.test.ts +12 -0
- package/src/config/resolve.ts +5 -0
- package/src/config/types.ts +5 -1
- package/src/controls/ControlMatrix.test.tsx +10 -0
- package/src/controls/ControlMatrix.tsx +110 -58
- package/src/controls/ControlPanel.test.tsx +30 -0
- package/src/index.ts +3 -2
- package/src/instrument/types.ts +21 -1
- package/src/lab/Lab.chrome.test.tsx +8 -1
- package/src/lab/Lab.presentation.test.tsx +113 -0
- package/src/lab/Lab.surface.test.tsx +20 -9
- package/src/lab/Lab.tsx +132 -257
- package/src/lab/LabLightbox.browser.test.less +17 -0
- package/src/lab/LabLightbox.browser.test.tsx +216 -0
- package/src/lab/LabShell.less +10 -3
- package/src/lab/LabSurface.tsx +129 -0
- package/src/lab/Workspace.stories.tsx +18 -0
- package/src/lab/Workspace.surface.test.tsx +4 -0
- package/src/lab/Workspace.test.tsx +37 -1
- package/src/lab/Workspace.tsx +34 -6
- package/src/lab/index.ts +2 -0
- package/src/lab/nebula.ts +30 -0
- package/src/lab/openLab.test.ts +81 -0
- package/src/lab/openLab.ts +119 -0
- package/src/lab/presentation.browser.test.less +5 -0
- package/src/lab/presentation.browser.test.tsx +119 -0
- package/src/lab/presentation.less +64 -0
- package/src/lab/presentation.tsx +47 -0
- package/src/lab/useFocusPick.ts +43 -0
- package/src/lightbox/Lightbox.browser.test.less +14 -0
- package/src/lightbox/Lightbox.browser.test.tsx +113 -0
- package/src/lightbox/Lightbox.less +128 -0
- package/src/lightbox/Lightbox.test.tsx +195 -0
- package/src/lightbox/Lightbox.tsx +205 -0
- package/src/lightbox/LightboxContext.ts +38 -0
- package/src/lightbox/LightboxLayers.tsx +40 -0
- package/src/lightbox/index.ts +8 -0
- package/src/lightbox/modal.ts +94 -0
- package/src/loupe/AGENTS.md +40 -2
- package/src/loupe/CanvasLoupe.tsx +5 -4
- package/src/loupe/DomLoupe.tsx +7 -5
- package/src/loupe/Loupe.less +15 -0
- package/src/loupe/LoupeBubble.tsx +35 -13
- package/src/loupe/SourceLoupe.browser.test.tsx +204 -0
- package/src/loupe/SourceLoupe.tsx +60 -0
- package/src/loupe/TrialLoupe.browser.test.tsx +342 -0
- package/src/loupe/TrialLoupe.tsx +126 -28
- package/src/loupe/canvasLens.ts +20 -17
- package/src/loupe/index.ts +26 -2
- package/src/loupe/sourceLens.test.ts +65 -0
- package/src/loupe/sourceLens.ts +97 -0
- package/src/loupe/types.test.ts +6 -0
- package/src/loupe/types.ts +109 -7
- package/src/loupe/useLoupe.test.tsx +188 -3
- package/src/loupe/useLoupe.ts +106 -11
- package/src/primitives/index.ts +2 -2
- package/src/specimen/Specimen.tsx +0 -7
- package/src/state/helpers.test.ts +4 -11
- package/src/state/helpers.ts +2 -8
- package/src/state/index.ts +0 -2
- package/src/state/openLabStore.test.ts +10 -8
- package/src/state/openLabStore.ts +1 -1
- package/src/state/store.test.ts +2 -2
- package/src/state/store.ts +15 -16
- package/src/state/types.ts +9 -9
- package/src/styles.less +2 -1
- package/src/surface/AGENTS.md +13 -0
- package/src/surface/index.ts +1 -0
- package/src/surface/useSurfaceTile.test.tsx +4 -0
- package/src/surface/useSurfaceTile.ts +17 -1
- package/src/surface/useTiledSurface.test.tsx +65 -0
- package/src/surface/useTiledSurface.ts +68 -1
- package/src/theme/interstellar.theme.json +0 -14
- package/src/trial/Trial.stories.tsx +0 -1
- package/src/trial/Trial.test.tsx +40 -3
- package/src/trial/Trial.tsx +87 -43
- package/src/trial/TrialChrome.tsx +20 -2
- package/src/trial/trialOps.test.ts +1 -2
- package/src/trial/trialOps.ts +2 -3
- package/src/undo/stateOp.ts +21 -0
- package/src/undo/undoStack.ts +18 -6
- package/dist/chunk-4TJ34FXA.js.map +0 -1
- package/dist/chunk-4X44DMRS.js.map +0 -1
- package/dist/chunk-EESJUSYR.js.map +0 -1
- package/dist/chunk-HBGH344L.js.map +0 -1
- package/dist/chunk-IBVRMHW5.js.map +0 -1
- package/dist/chunk-IRMPZIYU.js.map +0 -1
- package/dist/chunk-LPGBC63T.js.map +0 -1
- package/dist/chunk-PMLZP7RT.js.map +0 -1
- package/dist/chunk-RL2LOLNI.js.map +0 -1
- package/dist/chunk-T6DLGEJB.js.map +0 -1
- package/dist/chunk-WIG6XHJ7.js.map +0 -1
- package/dist/chunk-XSJJTXNA.js.map +0 -1
- package/dist/chunk-YCFKW5RX.js.map +0 -1
- package/src/primitives/StatusBar.less +0 -34
- package/src/primitives/StatusBar.stories.tsx +0 -24
- package/src/primitives/StatusBar.test.tsx +0 -25
- package/src/primitives/StatusBar.tsx +0 -28
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { stableStringify } from '@weasel-js/core';
|
|
2
|
+
import type { InstrumentList } from '../instrument/types';
|
|
3
|
+
import { defaultStorage, noneAdapter } from '../state/adapters';
|
|
4
|
+
import { valueRecord } from '../state/labRecords';
|
|
5
|
+
import { type OpenedLabStore, openLabStore } from '../state/openLabStore';
|
|
6
|
+
import { createRecordCache, type RecordCache } from '../state/records';
|
|
7
|
+
import { createLabStore, type LabStore } from '../state/store';
|
|
8
|
+
import type { LabMode, StorageAdapter } from '../state/types';
|
|
9
|
+
import { addTrial as addTrialOp } from '../trial/trialOps';
|
|
10
|
+
|
|
11
|
+
export interface OpenedLab extends OpenedLabStore {
|
|
12
|
+
marks: ReadonlyMap<string, unknown>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** What opening a lab reads from `<Lab>`'s props. */
|
|
16
|
+
export interface OpenLabOptions {
|
|
17
|
+
instruments: InstrumentList;
|
|
18
|
+
defaultInstrument: string;
|
|
19
|
+
mode?: LabMode | undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The trial a presented lab opens on. `config` is overlaid on the
|
|
23
|
+
* instrument's defaults the way `addTrial`'s is, so Reset returns to it;
|
|
24
|
+
* `state` and `view` replace what that config would give. */
|
|
25
|
+
export interface PresentationSeed<TC = Record<string, unknown>, TS = unknown, TV = unknown> {
|
|
26
|
+
/** Default: the lab's `defaultInstrument`. */
|
|
27
|
+
instrument?: string;
|
|
28
|
+
config?: Partial<TC>;
|
|
29
|
+
state?: TS;
|
|
30
|
+
view?: TV;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Where a lab stored under `storageKey` keeps its presentation, apart from
|
|
34
|
+
* the full lab: an embed shares its origin's storage with the lab it shows. */
|
|
35
|
+
export function presentStorageKey(storageKey: string): string {
|
|
36
|
+
return `${storageKey}:present`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const SEED_RECORD = valueRecord(null, 'lk-present-seed');
|
|
40
|
+
|
|
41
|
+
/** Open a presented lab's one trial on `seed`, unless the store already holds
|
|
42
|
+
* a trial opened on this same seed — a returning visitor keeps theirs until
|
|
43
|
+
* the seed itself changes. */
|
|
44
|
+
function seedPresentedTrial(
|
|
45
|
+
store: LabStore,
|
|
46
|
+
records: RecordCache,
|
|
47
|
+
{ instruments, defaultInstrument }: OpenLabOptions,
|
|
48
|
+
seed: PresentationSeed,
|
|
49
|
+
): void {
|
|
50
|
+
const instrument = seed.instrument ?? defaultInstrument;
|
|
51
|
+
const fingerprint = stableStringify({ ...seed, instrument });
|
|
52
|
+
const trials = store.getState().trials;
|
|
53
|
+
if (trials.length > 0 && records.get(SEED_RECORD) === fingerprint) return;
|
|
54
|
+
const record = addTrialOp([], instruments, instrument, { config: seed.config })[0];
|
|
55
|
+
if (!record) return;
|
|
56
|
+
store.setState({ trials: [] });
|
|
57
|
+
store.getState().addTrial({
|
|
58
|
+
...record,
|
|
59
|
+
...('state' in seed ? { state: seed.state } : {}),
|
|
60
|
+
...('view' in seed ? { view: seed.view } : {}),
|
|
61
|
+
});
|
|
62
|
+
records.set(SEED_RECORD, fingerprint);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function seedDefaultTrial(
|
|
66
|
+
store: LabStore,
|
|
67
|
+
instruments: InstrumentList,
|
|
68
|
+
defaultInstrument: string,
|
|
69
|
+
): void {
|
|
70
|
+
if (store.getState().trials.length > 0) return;
|
|
71
|
+
const record = addTrialOp([], instruments, defaultInstrument)[0];
|
|
72
|
+
if (!record) return;
|
|
73
|
+
store.getState().addTrial(record);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** A lab with nothing to load renders at once. Its records hold
|
|
77
|
+
* `usePersistedState` values for the session and write nothing. With a
|
|
78
|
+
* `seed`, the lab is presented and opens on it. */
|
|
79
|
+
export function openUnstoredLab(options: OpenLabOptions, seed?: PresentationSeed): OpenedLab {
|
|
80
|
+
const { instruments, defaultInstrument, mode } = options;
|
|
81
|
+
const store = createLabStore({ initialMode: mode ?? 'auto', instruments });
|
|
82
|
+
const records = createRecordCache({ storage: noneAdapter, prefix: '', writable: false });
|
|
83
|
+
if (seed) seedPresentedTrial(store, records, options, seed);
|
|
84
|
+
else seedDefaultTrial(store, instruments, defaultInstrument);
|
|
85
|
+
return { store, records, close: () => records.close(), marks: new Map() };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function openStoredLab(
|
|
89
|
+
options: OpenLabOptions,
|
|
90
|
+
storageKey: string,
|
|
91
|
+
storage: StorageAdapter | undefined,
|
|
92
|
+
seed?: PresentationSeed,
|
|
93
|
+
): Promise<OpenedLab> {
|
|
94
|
+
const { instruments, defaultInstrument, mode } = options;
|
|
95
|
+
// The store reads the instruments' serializers and defaults while it is being
|
|
96
|
+
// built, so they go in with it rather than being pushed onto it afterwards.
|
|
97
|
+
const opened = await openLabStore({
|
|
98
|
+
storageKey,
|
|
99
|
+
storage: storage ?? (await defaultStorage()),
|
|
100
|
+
initialMode: mode ?? 'auto',
|
|
101
|
+
instruments,
|
|
102
|
+
});
|
|
103
|
+
if (seed) seedPresentedTrial(opened.store, opened.records, options, seed);
|
|
104
|
+
else seedDefaultTrial(opened.store, instruments, defaultInstrument);
|
|
105
|
+
const marks = new Map<string, unknown>();
|
|
106
|
+
await Promise.all(
|
|
107
|
+
opened.store.getState().trials.map(async (trial) => {
|
|
108
|
+
const kept = instruments.find((i) => i.name === trial.instrumentName)?.annotations?.storage;
|
|
109
|
+
if (!kept) return;
|
|
110
|
+
try {
|
|
111
|
+
marks.set(trial.id, await kept.load());
|
|
112
|
+
} catch (error) {
|
|
113
|
+
console.warn(`[labkit] could not load the marks of trial "${trial.id}"`, error);
|
|
114
|
+
marks.set(trial.id, null);
|
|
115
|
+
}
|
|
116
|
+
}),
|
|
117
|
+
);
|
|
118
|
+
return { ...opened, marks };
|
|
119
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import '@weasel-js/theme/tokens.css';
|
|
2
|
+
import 'windease/styles.css';
|
|
3
|
+
import '../styles.less';
|
|
4
|
+
import './presentation.browser.test.less';
|
|
5
|
+
import { act, cleanup, render } from '@testing-library/react';
|
|
6
|
+
import { afterEach, expect, test } from 'vitest';
|
|
7
|
+
import type { Instrument } from '../instrument/types';
|
|
8
|
+
import { Lab } from './Lab';
|
|
9
|
+
import { LabContext, type LabContextValue } from './LabContext';
|
|
10
|
+
import { type Presentation, usePresentation } from './presentation';
|
|
11
|
+
|
|
12
|
+
// What presenting hides and what it fills is layout, and what it hands the page
|
|
13
|
+
// is a computed color-scheme: a browser's to answer.
|
|
14
|
+
|
|
15
|
+
afterEach(cleanup);
|
|
16
|
+
|
|
17
|
+
const Painted: Instrument = {
|
|
18
|
+
name: 'Painted',
|
|
19
|
+
defaultConfig: () => ({}),
|
|
20
|
+
initialState: () => ({}),
|
|
21
|
+
canvas: {
|
|
22
|
+
layers: [
|
|
23
|
+
{
|
|
24
|
+
id: 'fill',
|
|
25
|
+
draw: (ctx) => {
|
|
26
|
+
ctx.fillStyle = 'rgb(0, 128, 255)';
|
|
27
|
+
ctx.fillRect(-1e5, -1e5, 2e5, 2e5);
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
render: () => null,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
let presentation: Presentation | null = null;
|
|
36
|
+
let lab: LabContextValue | null = null;
|
|
37
|
+
function Capture() {
|
|
38
|
+
presentation = usePresentation();
|
|
39
|
+
return (
|
|
40
|
+
<LabContext.Consumer>
|
|
41
|
+
{(value) => {
|
|
42
|
+
lab = value;
|
|
43
|
+
return null;
|
|
44
|
+
}}
|
|
45
|
+
</LabContext.Consumer>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function mount(present: boolean) {
|
|
50
|
+
return render(
|
|
51
|
+
<div className="lk-present-frame">
|
|
52
|
+
<Lab instruments={[Painted]} defaultInstrument="Painted" present={present}>
|
|
53
|
+
<Capture />
|
|
54
|
+
</Lab>
|
|
55
|
+
</div>,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const box = (el: Element | null) => el?.getBoundingClientRect();
|
|
60
|
+
const hasBox = (el: Element | null) => !!el && el.getClientRects().length > 0;
|
|
61
|
+
|
|
62
|
+
test('a presented trial fills the lab and nothing else has a box', async () => {
|
|
63
|
+
const { container } = mount(true);
|
|
64
|
+
await expect
|
|
65
|
+
.poll(() => box(container.querySelector('.lk-trial__stage'))?.width)
|
|
66
|
+
.toBeGreaterThan(0);
|
|
67
|
+
const labEl = container.querySelector('.lk-lab');
|
|
68
|
+
const trial = container.querySelector('.lk-trial[data-lk-presented]');
|
|
69
|
+
const stage = container.querySelector('.lk-trial__stage');
|
|
70
|
+
|
|
71
|
+
expect(box(trial)).toEqual(box(container.querySelector('.lk-lab__body')));
|
|
72
|
+
expect(box(stage)).toEqual(box(trial));
|
|
73
|
+
expect(box(labEl)?.height).toBe(480);
|
|
74
|
+
expect(box(stage)?.height).toBe(480);
|
|
75
|
+
expect(
|
|
76
|
+
getComputedStyle(container.querySelector('.lk-canvas-stack') as Element).backgroundColor,
|
|
77
|
+
).toBe('rgba(0, 0, 0, 0)');
|
|
78
|
+
for (const hidden of ['.lk-viewport-controls', '.lk-shell-header', '.lk-trial__titlebar']) {
|
|
79
|
+
expect(container.querySelector(hidden), hidden).not.toBeNull();
|
|
80
|
+
}
|
|
81
|
+
for (const hidden of [
|
|
82
|
+
'.lk-viewport-controls',
|
|
83
|
+
'.lk-shell-header',
|
|
84
|
+
'.lk-trial__titlebar',
|
|
85
|
+
'.lk-trial__toolbar',
|
|
86
|
+
'.lk-trial__status',
|
|
87
|
+
'.lk-trial__sidebar',
|
|
88
|
+
]) {
|
|
89
|
+
expect(hasBox(container.querySelector(hidden)), hidden).toBe(false);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('a presenting lab hands the page back its own color-scheme and a clear ground', () => {
|
|
94
|
+
mount(true);
|
|
95
|
+
const root = getComputedStyle(document.documentElement);
|
|
96
|
+
expect(root.colorScheme).toBe('normal');
|
|
97
|
+
expect(root.backgroundColor).toBe('rgba(0, 0, 0, 0)');
|
|
98
|
+
expect(getComputedStyle(document.body).backgroundColor).toBe('rgba(0, 0, 0, 0)');
|
|
99
|
+
cleanup();
|
|
100
|
+
expect(getComputedStyle(document.documentElement).colorScheme).not.toBe('normal');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('entering and leaving keeps every trial’s canvas element', async () => {
|
|
104
|
+
const { container } = mount(false);
|
|
105
|
+
await act(async () => lab?.addTrial('Painted'));
|
|
106
|
+
const canvases = () => [...container.querySelectorAll('.lk-canvas-stack__canvas')];
|
|
107
|
+
await expect.poll(() => canvases().length).toBe(2);
|
|
108
|
+
const before = canvases();
|
|
109
|
+
|
|
110
|
+
await act(async () => presentation?.enter());
|
|
111
|
+
const presented = container.querySelector('.lk-trial[data-lk-presented]');
|
|
112
|
+
expect(box(presented)).toEqual(box(container.querySelector('.lk-lab__body')));
|
|
113
|
+
expect(container.querySelectorAll('.lk-trial').length).toBe(2);
|
|
114
|
+
expect(canvases().filter(hasBox)).toHaveLength(1);
|
|
115
|
+
|
|
116
|
+
await act(async () => presentation?.exit());
|
|
117
|
+
expect(canvases()).toEqual(before);
|
|
118
|
+
expect(canvases().filter(hasBox)).toHaveLength(2);
|
|
119
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// An iframe whose root color-scheme differs from its embedder's is painted
|
|
2
|
+
// opaque, so a presenting lab hands the page back its own.
|
|
3
|
+
:root:has(.lk-lab--present) {
|
|
4
|
+
color-scheme: normal;
|
|
5
|
+
background: transparent;
|
|
6
|
+
|
|
7
|
+
body {
|
|
8
|
+
background: transparent;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.lk-lab--present {
|
|
13
|
+
--wzl-backdrop: none;
|
|
14
|
+
background: transparent;
|
|
15
|
+
|
|
16
|
+
.lk-shell-header,
|
|
17
|
+
.lk-shell-footer,
|
|
18
|
+
.lk-lab__body > .lk-palette-region {
|
|
19
|
+
display: none;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// From the lab body down to the presented trial, and from that trial down to
|
|
23
|
+
// its stage, an element holding the next one lends it its box and whatever
|
|
24
|
+
// sits beside them has none. Hidden rather than unmounted, so every canvas
|
|
25
|
+
// keeps its context through a round trip.
|
|
26
|
+
.lk-lab__body *:has([data-lk-presented]),
|
|
27
|
+
[data-lk-presented] *:has(.lk-trial__stage) {
|
|
28
|
+
display: contents;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.lk-lab__body *:has([data-lk-presented]) > :not(:has([data-lk-presented]), [data-lk-presented]),
|
|
32
|
+
[data-lk-presented] > :not(:has(.lk-trial__stage)),
|
|
33
|
+
[data-lk-presented] *:has(.lk-trial__stage) > :not(:has(.lk-trial__stage), .lk-trial__stage) {
|
|
34
|
+
display: none;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
[data-lk-presented] {
|
|
38
|
+
position: absolute;
|
|
39
|
+
inset: 0;
|
|
40
|
+
display: flex;
|
|
41
|
+
border: none;
|
|
42
|
+
border-radius: 0;
|
|
43
|
+
background: transparent;
|
|
44
|
+
box-shadow: none;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.lk-trial__stage {
|
|
48
|
+
flex: 1;
|
|
49
|
+
min-width: 0;
|
|
50
|
+
min-height: 0;
|
|
51
|
+
background: transparent;
|
|
52
|
+
|
|
53
|
+
// The well a canvas sits in is the lab's ground, not the figure's.
|
|
54
|
+
.lk-canvas-stack,
|
|
55
|
+
.lk-stage {
|
|
56
|
+
background: transparent;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Trial chrome drawn over the content, like the rest of it.
|
|
60
|
+
.lk-viewport-controls {
|
|
61
|
+
display: none;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createContext, useContext, useLayoutEffect, useState } from 'react';
|
|
2
|
+
import { useSurfaceOptional } from '../surface/useSurfaceTile';
|
|
3
|
+
|
|
4
|
+
/** A lab's presentation mode: one trial, without the lab's chrome or its own. */
|
|
5
|
+
export interface Presentation {
|
|
6
|
+
active: boolean;
|
|
7
|
+
/** Present the focused trial. Escape returns to the workspace. */
|
|
8
|
+
enter(): void;
|
|
9
|
+
exit(): void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface PresentationContextValue extends Presentation {
|
|
13
|
+
/** The trial being presented, while one is. */
|
|
14
|
+
trialId: string | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const PresentationContext = createContext<PresentationContextValue | null>(null);
|
|
18
|
+
|
|
19
|
+
/** The presentation mode of the `<Lab>` around the caller. */
|
|
20
|
+
export function usePresentation(): Presentation {
|
|
21
|
+
const value = useContext(PresentationContext);
|
|
22
|
+
if (!value) throw new Error('[labkit] usePresentation() must be called inside <Lab>');
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Whether the trial `trialId` is the one its lab is presenting, and a ref for
|
|
27
|
+
* its root: while presented, the lab's shared surface is scoped to it, so the
|
|
28
|
+
* tiles hidden behind it stop painting. */
|
|
29
|
+
export function usePresentedTrial(trialId: string): {
|
|
30
|
+
presented: boolean;
|
|
31
|
+
ref: (el: HTMLElement | null) => void;
|
|
32
|
+
} {
|
|
33
|
+
const presented = useContext(PresentationContext)?.trialId === trialId;
|
|
34
|
+
const surface = useSurfaceOptional();
|
|
35
|
+
const [el, ref] = useState<HTMLElement | null>(null);
|
|
36
|
+
useLayoutEffect(() => {
|
|
37
|
+
if (!presented || !el || !surface) return;
|
|
38
|
+
surface.scope(el);
|
|
39
|
+
return () => surface.scope(null);
|
|
40
|
+
}, [presented, el, surface]);
|
|
41
|
+
return { presented, ref };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Whether the page's URL asks for presentation with `?present`. */
|
|
45
|
+
export function hasPresentParam(): boolean {
|
|
46
|
+
return typeof location !== 'undefined' && new URLSearchParams(location.search).has('present');
|
|
47
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import type { LabStore } from '../state/store';
|
|
3
|
+
|
|
4
|
+
/** Calls `pick` with the id of the trial input lands in — a press, focus
|
|
5
|
+
* moving in, or focus moving into a frame — among the trials `store` owns. */
|
|
6
|
+
export function useFocusPick(
|
|
7
|
+
labBody: HTMLElement | null,
|
|
8
|
+
store: LabStore,
|
|
9
|
+
pick: (trialId: string) => void,
|
|
10
|
+
): void {
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
if (!labBody) return;
|
|
13
|
+
const pickFrom = (node: unknown): void => {
|
|
14
|
+
const owned = new Set(store.getState().trials.map((t) => t.id));
|
|
15
|
+
const nearest = (from: Element | null | undefined) =>
|
|
16
|
+
from?.closest<HTMLElement>('.lk-trial[data-trial-id]') ?? null;
|
|
17
|
+
let trial =
|
|
18
|
+
typeof (node as Element | null)?.closest === 'function' ? nearest(node as Element) : null;
|
|
19
|
+
// A trial can hold a lab of its own, whose trials this lab does not own.
|
|
20
|
+
while (trial && !owned.has(trial.dataset.trialId ?? '')) trial = nearest(trial.parentElement);
|
|
21
|
+
if (trial?.dataset.trialId) pick(trial.dataset.trialId);
|
|
22
|
+
};
|
|
23
|
+
const onInput = (event: Event): void => pickFrom(event.target);
|
|
24
|
+
// Focus moving into a frame fires nothing in this document: the window
|
|
25
|
+
// blurs, and the frame is the active element once it has.
|
|
26
|
+
let pending: ReturnType<typeof setTimeout> | undefined;
|
|
27
|
+
const doc = labBody.ownerDocument;
|
|
28
|
+
const win = doc.defaultView;
|
|
29
|
+
const onBlur = (): void => {
|
|
30
|
+
clearTimeout(pending);
|
|
31
|
+
pending = setTimeout(() => pickFrom(doc.activeElement), 0);
|
|
32
|
+
};
|
|
33
|
+
labBody.addEventListener('pointerdown', onInput, true);
|
|
34
|
+
labBody.addEventListener('focusin', onInput, true);
|
|
35
|
+
win?.addEventListener('blur', onBlur);
|
|
36
|
+
return () => {
|
|
37
|
+
clearTimeout(pending);
|
|
38
|
+
labBody.removeEventListener('pointerdown', onInput, true);
|
|
39
|
+
labBody.removeEventListener('focusin', onInput, true);
|
|
40
|
+
win?.removeEventListener('blur', onBlur);
|
|
41
|
+
};
|
|
42
|
+
}, [labBody, store, pick]);
|
|
43
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// A host that would trap a fixed-position child: a transform makes it the
|
|
2
|
+
// containing block, and the clipping cuts off anything past its edge.
|
|
3
|
+
.lk-lightbox-test-host {
|
|
4
|
+
width: 480px;
|
|
5
|
+
height: 320px;
|
|
6
|
+
overflow: hidden;
|
|
7
|
+
transform: translate(7px, 5px);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.lk-lightbox-test-canvas {
|
|
11
|
+
display: block;
|
|
12
|
+
width: 100%;
|
|
13
|
+
height: 100%;
|
|
14
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import '@weasel-js/theme/tokens.css';
|
|
2
|
+
import '../styles.less';
|
|
3
|
+
import './Lightbox.browser.test.less';
|
|
4
|
+
import { cleanup, render } from '@testing-library/react';
|
|
5
|
+
import { useEffect, useRef } from 'react';
|
|
6
|
+
import { afterEach, expect, test } from 'vitest';
|
|
7
|
+
import { userEvent } from 'vitest/browser';
|
|
8
|
+
import { Workspace } from '../lab/Workspace';
|
|
9
|
+
|
|
10
|
+
// The top layer, its escape from a transformed and clipping ancestor, and what
|
|
11
|
+
// a ResizeObserver reports are all layout — only a real browser has them.
|
|
12
|
+
|
|
13
|
+
afterEach(cleanup);
|
|
14
|
+
|
|
15
|
+
interface Probe {
|
|
16
|
+
mounts: number;
|
|
17
|
+
sizes: Array<{ w: number; h: number }>;
|
|
18
|
+
gl: WebGLRenderingContext | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Stands in for a consumer's expensive tile: a WebGL context it creates once,
|
|
22
|
+
* sized from a ResizeObserver. */
|
|
23
|
+
function GlTile({ probe }: { probe: Probe }) {
|
|
24
|
+
const ref = useRef<HTMLCanvasElement | null>(null);
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
const canvas = ref.current;
|
|
27
|
+
if (!canvas) return;
|
|
28
|
+
probe.mounts += 1;
|
|
29
|
+
probe.gl = canvas.getContext('webgl');
|
|
30
|
+
const ro = new ResizeObserver(([entry]) => {
|
|
31
|
+
if (!entry) return;
|
|
32
|
+
probe.sizes.push({ w: entry.contentRect.width, h: entry.contentRect.height });
|
|
33
|
+
});
|
|
34
|
+
ro.observe(canvas);
|
|
35
|
+
return () => ro.disconnect();
|
|
36
|
+
}, [probe]);
|
|
37
|
+
return <canvas ref={ref} className="lk-lightbox-test-canvas" data-testid="gl" />;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function mount(probe: Probe) {
|
|
41
|
+
return render(
|
|
42
|
+
<div className="lk-lightbox-test-host">
|
|
43
|
+
<Workspace ids={['gl', 'plain']} expandOnDoubleClick={(id) => id === 'gl'}>
|
|
44
|
+
<GlTile probe={probe} />
|
|
45
|
+
<div>plain</div>
|
|
46
|
+
</Workspace>
|
|
47
|
+
</div>,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
test('a double-clicked tile fills the window from the top layer, on the same element and context', async () => {
|
|
52
|
+
const probe: Probe = { mounts: 0, sizes: [], gl: null };
|
|
53
|
+
const { getByTestId } = mount(probe);
|
|
54
|
+
await expect.poll(() => probe.sizes.length).toBeGreaterThan(0);
|
|
55
|
+
const canvas = getByTestId('gl') as HTMLCanvasElement;
|
|
56
|
+
const before = canvas.getBoundingClientRect();
|
|
57
|
+
const gl = probe.gl;
|
|
58
|
+
expect(gl).not.toBeNull();
|
|
59
|
+
|
|
60
|
+
await userEvent.dblClick(canvas);
|
|
61
|
+
|
|
62
|
+
const box = canvas.closest('.lk-lightbox') as HTMLElement;
|
|
63
|
+
expect(box.matches(':popover-open')).toBe(true);
|
|
64
|
+
const after = canvas.getBoundingClientRect();
|
|
65
|
+
// Most of the window, though the host is 480×320, clipped and transformed.
|
|
66
|
+
expect(after.width).toBeGreaterThan(window.innerWidth * 0.9);
|
|
67
|
+
expect(after.height).toBeGreaterThan(window.innerHeight * 0.8);
|
|
68
|
+
expect(after.width).toBeGreaterThan(before.width);
|
|
69
|
+
await expect.poll(() => probe.sizes.at(-1)?.w ?? 0).toBeCloseTo(after.width, 0);
|
|
70
|
+
|
|
71
|
+
// Not remounted: the same element, the same live context, one mount.
|
|
72
|
+
expect(getByTestId('gl')).toBe(canvas);
|
|
73
|
+
expect(canvas.getContext('webgl')).toBe(gl);
|
|
74
|
+
expect(gl?.isContextLost()).toBe(false);
|
|
75
|
+
expect(probe.mounts).toBe(1);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('a click in the dimmed margin closes it, and the tile goes back to its rect', async () => {
|
|
79
|
+
const probe: Probe = { mounts: 0, sizes: [], gl: null };
|
|
80
|
+
const { getByTestId } = mount(probe);
|
|
81
|
+
await expect.poll(() => probe.sizes.length).toBeGreaterThan(0);
|
|
82
|
+
const canvas = getByTestId('gl');
|
|
83
|
+
const before = canvas.getBoundingClientRect();
|
|
84
|
+
await userEvent.dblClick(canvas);
|
|
85
|
+
const box = canvas.closest('.lk-lightbox') as HTMLElement;
|
|
86
|
+
|
|
87
|
+
// The margin hit-tests to the lifted element itself, nothing behind it.
|
|
88
|
+
expect(document.elementFromPoint(4, 4)).toBe(box);
|
|
89
|
+
await userEvent.click(box, { position: { x: 4, y: 4 } });
|
|
90
|
+
|
|
91
|
+
expect(box.matches(':popover-open')).toBe(false);
|
|
92
|
+
expect(box.hasAttribute('popover')).toBe(false);
|
|
93
|
+
const back = canvas.getBoundingClientRect();
|
|
94
|
+
expect(back.width).toBeCloseTo(before.width, 0);
|
|
95
|
+
expect(back.left).toBeCloseTo(before.left, 0);
|
|
96
|
+
expect(probe.mounts).toBe(1);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('Escape closes it and focus returns to the expand button that opened it', async () => {
|
|
100
|
+
const probe: Probe = { mounts: 0, sizes: [], gl: null };
|
|
101
|
+
const { getByTestId, getAllByRole } = mount(probe);
|
|
102
|
+
await expect.poll(() => probe.sizes.length).toBeGreaterThan(0);
|
|
103
|
+
const box = getByTestId('gl').closest('.lk-lightbox') as HTMLElement;
|
|
104
|
+
const expand = getAllByRole('button', { name: 'Expand' })[0] as HTMLElement;
|
|
105
|
+
|
|
106
|
+
await userEvent.click(expand);
|
|
107
|
+
expect(box.matches(':popover-open')).toBe(true);
|
|
108
|
+
expect(document.activeElement?.getAttribute('aria-label')).toBe('Close expanded view');
|
|
109
|
+
|
|
110
|
+
await userEvent.keyboard('{Escape}');
|
|
111
|
+
expect(box.matches(':popover-open')).toBe(false);
|
|
112
|
+
expect(document.activeElement).toBe(expand);
|
|
113
|
+
});
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// Closed, the lightbox is a plain box filling whatever holds it, so the
|
|
2
|
+
// content's own `height: 100%` chain keeps working.
|
|
3
|
+
.lk-lightbox {
|
|
4
|
+
position: relative;
|
|
5
|
+
height: 100%;
|
|
6
|
+
min-height: 0;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.lk-lightbox__frame {
|
|
10
|
+
height: 100%;
|
|
11
|
+
min-height: 0;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.lk-lightbox__button {
|
|
15
|
+
position: absolute;
|
|
16
|
+
top: var(--wzl-space-xs);
|
|
17
|
+
right: var(--wzl-space-xs);
|
|
18
|
+
z-index: 1;
|
|
19
|
+
display: inline-flex;
|
|
20
|
+
align-items: center;
|
|
21
|
+
justify-content: center;
|
|
22
|
+
width: 24px;
|
|
23
|
+
height: 24px;
|
|
24
|
+
padding: 0;
|
|
25
|
+
border: var(--wzl-border-w) solid var(--wzl-line-subtle);
|
|
26
|
+
border-radius: var(--wzl-radius-sm);
|
|
27
|
+
background: var(--wzl-surface);
|
|
28
|
+
color: var(--wzl-fg-muted);
|
|
29
|
+
backdrop-filter: none;
|
|
30
|
+
-webkit-backdrop-filter: none;
|
|
31
|
+
cursor: pointer;
|
|
32
|
+
|
|
33
|
+
&:hover {
|
|
34
|
+
color: var(--wzl-fg);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Out of the way until the tile is hovered or the button has focus. A touch
|
|
39
|
+
// screen has no hover, so there it always shows.
|
|
40
|
+
.lk-lightbox__open {
|
|
41
|
+
opacity: 0;
|
|
42
|
+
transition: opacity var(--wzl-motion-fast);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.lk-lightbox:hover > .lk-lightbox__open,
|
|
46
|
+
.lk-lightbox__open:focus-visible {
|
|
47
|
+
opacity: 1;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@media (hover: none) {
|
|
51
|
+
.lk-lightbox__open {
|
|
52
|
+
opacity: 1;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// The margin around an open lightbox's frame. The frame backing, drawn by
|
|
57
|
+
// whichever element was lifted first, sits at the same inset.
|
|
58
|
+
@lk-lightbox-margin: 44px 3vw 3vh;
|
|
59
|
+
|
|
60
|
+
// Open, the same element covers the window, with its padding as the margin
|
|
61
|
+
// and the frame inside holding the content. The declarations past `inset`
|
|
62
|
+
// undo the user-agent popover rules, which are written for a small box.
|
|
63
|
+
.lk-lightbox.lk-lightbox--expanded {
|
|
64
|
+
position: fixed;
|
|
65
|
+
inset: 0;
|
|
66
|
+
z-index: var(--wzl-z-modal);
|
|
67
|
+
width: auto;
|
|
68
|
+
height: auto;
|
|
69
|
+
max-width: none;
|
|
70
|
+
max-height: none;
|
|
71
|
+
margin: 0;
|
|
72
|
+
border: 0;
|
|
73
|
+
padding: @lk-lightbox-margin;
|
|
74
|
+
overflow: hidden;
|
|
75
|
+
color: inherit;
|
|
76
|
+
background: transparent;
|
|
77
|
+
|
|
78
|
+
// Positioned, so it paints over the backing the scrim draws.
|
|
79
|
+
> .lk-lightbox__frame {
|
|
80
|
+
position: relative;
|
|
81
|
+
overflow: hidden;
|
|
82
|
+
border-radius: var(--wzl-radius-md);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// A layer lifted with a lightbox — a shared buffer drawn for the content —
|
|
87
|
+
// covers the window like it, and takes no input of its own.
|
|
88
|
+
[data-lk-lightbox-layer]:popover-open {
|
|
89
|
+
position: fixed;
|
|
90
|
+
inset: 0;
|
|
91
|
+
width: auto;
|
|
92
|
+
height: auto;
|
|
93
|
+
max-width: none;
|
|
94
|
+
max-height: none;
|
|
95
|
+
margin: 0;
|
|
96
|
+
border: 0;
|
|
97
|
+
padding: 0;
|
|
98
|
+
overflow: visible;
|
|
99
|
+
color: inherit;
|
|
100
|
+
background: transparent;
|
|
101
|
+
pointer-events: none;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.lk-lightbox--expanded::backdrop,
|
|
105
|
+
[data-lk-lightbox-layer]::backdrop {
|
|
106
|
+
background: transparent;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// The lowest lifted element dims the page and backs the frame, so a layer
|
|
110
|
+
// lifted below the content still has something under it.
|
|
111
|
+
[data-lk-lightbox-scrim]::backdrop {
|
|
112
|
+
background: rgb(0 0 0 / 55%);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
[data-lk-lightbox-scrim]::before {
|
|
116
|
+
content: '';
|
|
117
|
+
position: fixed;
|
|
118
|
+
inset: @lk-lightbox-margin;
|
|
119
|
+
border-radius: var(--wzl-radius-md);
|
|
120
|
+
background: var(--wzl-surface);
|
|
121
|
+
box-shadow: 0 8px 32px rgb(0 0 0 / 45%);
|
|
122
|
+
pointer-events: none;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.lk-lightbox--expanded > .lk-lightbox__close {
|
|
126
|
+
top: 10px;
|
|
127
|
+
right: 3vw;
|
|
128
|
+
}
|