@scenography/overlay-presets 0.2.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/dist/index.cjs +902 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +337 -0
- package/dist/index.d.mts +337 -0
- package/dist/index.mjs +890 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +46 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,890 @@
|
|
|
1
|
+
import { resolveText } from "@scenography/overlay";
|
|
2
|
+
//#region src/lucide.ts
|
|
3
|
+
/**
|
|
4
|
+
* Render a Lucide icon by name as overlay button content.
|
|
5
|
+
*
|
|
6
|
+
* Lucide is an OPTIONAL peer dependency: it's imported lazily, so the overlay
|
|
7
|
+
* works without it. When lucide isn't installed the icon renders empty — install
|
|
8
|
+
* `lucide`, pass your own `icon` to a prefab, or use `actionButton` if you don't
|
|
9
|
+
* want the dependency.
|
|
10
|
+
*/
|
|
11
|
+
function lucideIcon(name, size = 18) {
|
|
12
|
+
return () => {
|
|
13
|
+
const span = document.createElement("span");
|
|
14
|
+
span.style.display = "inline-flex";
|
|
15
|
+
import("lucide").then((lucide) => {
|
|
16
|
+
const node = lucide[name];
|
|
17
|
+
if (node === void 0) return;
|
|
18
|
+
const svg = lucide.createElement(node);
|
|
19
|
+
svg.setAttribute("width", String(size));
|
|
20
|
+
svg.setAttribute("height", String(size));
|
|
21
|
+
span.replaceChildren(svg);
|
|
22
|
+
}).catch(() => {});
|
|
23
|
+
return span;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
//#region src/util.ts
|
|
28
|
+
/** Replace an element's content with an icon/glyph (string → text, node → child). */
|
|
29
|
+
function setIcon(el, content) {
|
|
30
|
+
const value = typeof content === "function" ? content() : content;
|
|
31
|
+
el.replaceChildren();
|
|
32
|
+
if (typeof value === "string") el.textContent = value;
|
|
33
|
+
else el.appendChild(value);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Run `apply` once and re-run it whenever the overlay's locale changes, so a
|
|
37
|
+
* component's `Text` values stay localized live. Returns an unsubscribe to call
|
|
38
|
+
* on teardown (a no-op when no locale source is configured).
|
|
39
|
+
*/
|
|
40
|
+
function bindLocale(ctx, apply) {
|
|
41
|
+
apply();
|
|
42
|
+
return ctx.locale?.subscribe(apply) ?? (() => {});
|
|
43
|
+
}
|
|
44
|
+
//#endregion
|
|
45
|
+
//#region src/components/fullscreen/fullscreen.ts
|
|
46
|
+
/**
|
|
47
|
+
* A fullscreen toggle. daisyUI prefab (styled with `btn`). Defaults to plain
|
|
48
|
+
* glyphs and pulls in no icon library; pass `icons` (e.g. via `lucideIcon` or
|
|
49
|
+
* your own SVGs) for richer iconography.
|
|
50
|
+
*/
|
|
51
|
+
function fullscreenButton(options = {}) {
|
|
52
|
+
const enter = options.icons?.enter ?? "⛶";
|
|
53
|
+
const exit = options.icons?.exit ?? "✕";
|
|
54
|
+
return (ctx) => {
|
|
55
|
+
const button = document.createElement("button");
|
|
56
|
+
button.type = "button";
|
|
57
|
+
button.className = options.className ?? "btn btn-circle btn-sm";
|
|
58
|
+
const sync = () => {
|
|
59
|
+
const active = document.fullscreenElement !== null;
|
|
60
|
+
setIcon(button, active ? exit : enter);
|
|
61
|
+
button.setAttribute("aria-label", active ? "Exit fullscreen" : "Enter fullscreen");
|
|
62
|
+
};
|
|
63
|
+
const onClick = () => {
|
|
64
|
+
if (document.fullscreenElement) document.exitFullscreen();
|
|
65
|
+
else ctx.surface.requestFullscreen?.();
|
|
66
|
+
};
|
|
67
|
+
button.addEventListener("click", onClick);
|
|
68
|
+
document.addEventListener("fullscreenchange", sync);
|
|
69
|
+
sync();
|
|
70
|
+
return {
|
|
71
|
+
element: button,
|
|
72
|
+
destroy() {
|
|
73
|
+
button.removeEventListener("click", onClick);
|
|
74
|
+
document.removeEventListener("fullscreenchange", sync);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
//#endregion
|
|
80
|
+
//#region src/components/mute/mute.ts
|
|
81
|
+
/**
|
|
82
|
+
* A mute toggle for background audio. daisyUI prefab (styled with `btn`). Drives
|
|
83
|
+
* `ctx.media` master mute, so it silences every source while keeping their
|
|
84
|
+
* individual volumes — one click restores the mix exactly. Defaults to plain
|
|
85
|
+
* glyphs (no icon library); pass `icons` (e.g. `lucideIcon`) for richer icons.
|
|
86
|
+
*/
|
|
87
|
+
function muteButton(options = {}) {
|
|
88
|
+
const on = options.icons?.on ?? "🔊";
|
|
89
|
+
const off = options.icons?.off ?? "🔇";
|
|
90
|
+
return (ctx) => {
|
|
91
|
+
const media = ctx.gallery.ctx.media;
|
|
92
|
+
const button = document.createElement("button");
|
|
93
|
+
button.type = "button";
|
|
94
|
+
button.className = options.className ?? "btn btn-circle btn-sm";
|
|
95
|
+
const sync = () => {
|
|
96
|
+
const muted = media.muted();
|
|
97
|
+
setIcon(button, muted ? off : on);
|
|
98
|
+
button.setAttribute("aria-label", muted ? "Unmute" : "Mute");
|
|
99
|
+
};
|
|
100
|
+
const onClick = () => {
|
|
101
|
+
media.setMuted(!media.muted());
|
|
102
|
+
sync();
|
|
103
|
+
};
|
|
104
|
+
button.addEventListener("click", onClick);
|
|
105
|
+
sync();
|
|
106
|
+
return {
|
|
107
|
+
element: button,
|
|
108
|
+
destroy: () => button.removeEventListener("click", onClick)
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
//#endregion
|
|
113
|
+
//#region src/components/cardboard/cardboard.ts
|
|
114
|
+
/**
|
|
115
|
+
* A cardboard (stereoscopic) toggle. Flips `ctx.gallery.setRender({ cardboard })`
|
|
116
|
+
* to split the view into a left/right eye pair for a phone-in-a-headset viewer,
|
|
117
|
+
* and reflects the on state with daisyUI's `btn-active`. Defaults to a plain
|
|
118
|
+
* glyph; pass `icon` (e.g. `lucideIcon("RectangleGoggles")`).
|
|
119
|
+
*/
|
|
120
|
+
function cardboardButton(options = {}) {
|
|
121
|
+
const icon = options.icon ?? "🥽";
|
|
122
|
+
const label = options.label ?? "Cardboard mode";
|
|
123
|
+
return (ctx) => {
|
|
124
|
+
const button = document.createElement("button");
|
|
125
|
+
button.type = "button";
|
|
126
|
+
button.className = options.className ?? "btn btn-circle btn-sm";
|
|
127
|
+
button.setAttribute("aria-label", label);
|
|
128
|
+
setIcon(button, icon);
|
|
129
|
+
let enabled = false;
|
|
130
|
+
const onClick = () => {
|
|
131
|
+
enabled = !enabled;
|
|
132
|
+
ctx.gallery.setRender({ cardboard: enabled });
|
|
133
|
+
button.classList.toggle("btn-active", enabled);
|
|
134
|
+
button.setAttribute("aria-pressed", String(enabled));
|
|
135
|
+
};
|
|
136
|
+
button.addEventListener("click", onClick);
|
|
137
|
+
return {
|
|
138
|
+
element: button,
|
|
139
|
+
destroy: () => button.removeEventListener("click", onClick)
|
|
140
|
+
};
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
//#endregion
|
|
144
|
+
//#region src/components/environment/environment.ts
|
|
145
|
+
/**
|
|
146
|
+
* An environment panel: a button (e.g. a sun/cloud icon) opening a daisyUI modal
|
|
147
|
+
* with sliders for ambient settings like time of day and sun azimuth. Optional
|
|
148
|
+
* in the default preset — galleries with a procedural sky opt in. daisyUI +
|
|
149
|
+
* Lucide prefab.
|
|
150
|
+
*/
|
|
151
|
+
function environmentPanel(options) {
|
|
152
|
+
const icon = options.icon ?? "☀";
|
|
153
|
+
return (ctx) => {
|
|
154
|
+
const wrap = document.createElement("div");
|
|
155
|
+
const button = document.createElement("button");
|
|
156
|
+
button.type = "button";
|
|
157
|
+
button.className = "btn btn-circle btn-sm";
|
|
158
|
+
setIcon(button, icon);
|
|
159
|
+
const dialog = document.createElement("dialog");
|
|
160
|
+
dialog.className = "modal";
|
|
161
|
+
const box = document.createElement("div");
|
|
162
|
+
box.className = "modal-box max-w-sm";
|
|
163
|
+
const close = document.createElement("button");
|
|
164
|
+
close.type = "button";
|
|
165
|
+
close.className = "btn btn-sm btn-circle btn-ghost absolute right-2 top-2";
|
|
166
|
+
close.textContent = "✕";
|
|
167
|
+
const heading = document.createElement("h3");
|
|
168
|
+
heading.className = "text-lg font-bold mb-4";
|
|
169
|
+
box.append(close, heading);
|
|
170
|
+
const cleanups = [];
|
|
171
|
+
const relabels = [];
|
|
172
|
+
const controls = /* @__PURE__ */ new Map();
|
|
173
|
+
for (const slider of options.sliders) {
|
|
174
|
+
const field = document.createElement("label");
|
|
175
|
+
field.className = "flex flex-col gap-1 mt-2 first:mt-0";
|
|
176
|
+
const head = document.createElement("span");
|
|
177
|
+
head.className = "label-text text-sm flex justify-between";
|
|
178
|
+
const name = document.createElement("span");
|
|
179
|
+
const value = document.createElement("span");
|
|
180
|
+
value.className = "opacity-60";
|
|
181
|
+
head.append(name, value);
|
|
182
|
+
const input = document.createElement("input");
|
|
183
|
+
input.type = "range";
|
|
184
|
+
input.className = "range range-sm";
|
|
185
|
+
input.min = String(slider.min);
|
|
186
|
+
input.max = String(slider.max);
|
|
187
|
+
input.step = String(slider.step ?? .1);
|
|
188
|
+
input.value = String(slider.value);
|
|
189
|
+
value.textContent = input.value;
|
|
190
|
+
const onInput = () => {
|
|
191
|
+
value.textContent = input.value;
|
|
192
|
+
slider.onChange(parseFloat(input.value));
|
|
193
|
+
};
|
|
194
|
+
input.addEventListener("input", onInput);
|
|
195
|
+
cleanups.push(() => input.removeEventListener("input", onInput));
|
|
196
|
+
field.append(head, input);
|
|
197
|
+
box.appendChild(field);
|
|
198
|
+
controls.set(slider.key, {
|
|
199
|
+
input,
|
|
200
|
+
value,
|
|
201
|
+
onChange: slider.onChange
|
|
202
|
+
});
|
|
203
|
+
relabels.push(() => {
|
|
204
|
+
name.textContent = resolveText(slider.label);
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
if (options.presets?.length) {
|
|
208
|
+
const row = document.createElement("div");
|
|
209
|
+
row.className = "flex flex-wrap gap-2 mt-4";
|
|
210
|
+
for (const preset of options.presets) {
|
|
211
|
+
const btn = document.createElement("button");
|
|
212
|
+
btn.type = "button";
|
|
213
|
+
btn.className = "btn btn-xs";
|
|
214
|
+
const apply = () => {
|
|
215
|
+
for (const [key, val] of Object.entries(preset.values)) {
|
|
216
|
+
const ctrl = controls.get(key);
|
|
217
|
+
if (!ctrl) continue;
|
|
218
|
+
ctrl.input.value = String(val);
|
|
219
|
+
ctrl.value.textContent = String(val);
|
|
220
|
+
ctrl.onChange(val);
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
btn.addEventListener("click", apply);
|
|
224
|
+
cleanups.push(() => btn.removeEventListener("click", apply));
|
|
225
|
+
row.appendChild(btn);
|
|
226
|
+
relabels.push(() => {
|
|
227
|
+
btn.textContent = resolveText(preset.label);
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
box.appendChild(row);
|
|
231
|
+
}
|
|
232
|
+
const backdrop = document.createElement("form");
|
|
233
|
+
backdrop.method = "dialog";
|
|
234
|
+
backdrop.className = "modal-backdrop";
|
|
235
|
+
const backdropBtn = document.createElement("button");
|
|
236
|
+
backdropBtn.textContent = "close";
|
|
237
|
+
backdrop.appendChild(backdropBtn);
|
|
238
|
+
dialog.append(box, backdrop);
|
|
239
|
+
wrap.append(button, dialog);
|
|
240
|
+
const unbind = bindLocale(ctx, () => {
|
|
241
|
+
const title = resolveText(options.title);
|
|
242
|
+
button.setAttribute("aria-label", title);
|
|
243
|
+
heading.textContent = title;
|
|
244
|
+
close.setAttribute("aria-label", resolveText(options.closeLabel ?? "Close"));
|
|
245
|
+
for (const relabel of relabels) relabel();
|
|
246
|
+
});
|
|
247
|
+
const onOpen = () => dialog.showModal();
|
|
248
|
+
const onClose = () => dialog.close();
|
|
249
|
+
button.addEventListener("click", onOpen);
|
|
250
|
+
close.addEventListener("click", onClose);
|
|
251
|
+
return {
|
|
252
|
+
element: wrap,
|
|
253
|
+
destroy: () => {
|
|
254
|
+
button.removeEventListener("click", onOpen);
|
|
255
|
+
close.removeEventListener("click", onClose);
|
|
256
|
+
for (const fn of cleanups) fn();
|
|
257
|
+
unbind();
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
//#endregion
|
|
263
|
+
//#region src/components/credits/credits.ts
|
|
264
|
+
let panelCount = 0;
|
|
265
|
+
const buildRow = (item) => {
|
|
266
|
+
const row = document.createElement("div");
|
|
267
|
+
row.className = "text-sm leading-snug";
|
|
268
|
+
const name = document.createElement(item.href ? "a" : "span");
|
|
269
|
+
name.textContent = item.label;
|
|
270
|
+
if (item.href) {
|
|
271
|
+
const link = name;
|
|
272
|
+
link.href = item.href;
|
|
273
|
+
link.target = "_blank";
|
|
274
|
+
link.rel = "noopener noreferrer";
|
|
275
|
+
link.className = "link link-hover font-medium";
|
|
276
|
+
} else name.className = "font-medium";
|
|
277
|
+
row.appendChild(name);
|
|
278
|
+
const meta = [item.author, item.license].filter(Boolean).join(" · ");
|
|
279
|
+
if (meta) {
|
|
280
|
+
const small = document.createElement("span");
|
|
281
|
+
small.className = "opacity-60";
|
|
282
|
+
small.textContent = ` — ${meta}`;
|
|
283
|
+
row.appendChild(small);
|
|
284
|
+
}
|
|
285
|
+
return row;
|
|
286
|
+
};
|
|
287
|
+
const buildSection = (section) => {
|
|
288
|
+
const heading = document.createElement("div");
|
|
289
|
+
heading.className = "font-semibold text-xs uppercase tracking-wide opacity-50 mt-3 first:mt-0";
|
|
290
|
+
heading.textContent = section.label;
|
|
291
|
+
return heading;
|
|
292
|
+
};
|
|
293
|
+
const buildEntries = (entries) => entries.map((entry) => entry.type === "section" ? buildSection(entry) : buildRow(entry));
|
|
294
|
+
/**
|
|
295
|
+
* A credits panel: a button opening a centered daisyUI modal (✕ close,
|
|
296
|
+
* click-outside, Esc) listing attributions. Entries are data (`items`): loose
|
|
297
|
+
* items and sections show first; `tab` entries become a daisyUI tab strip below
|
|
298
|
+
* (first tab open), so an exhibition can credit its author up top and group
|
|
299
|
+
* assets/technology/etc. into tabs with sectioned lists. Defaults to a plain
|
|
300
|
+
* glyph icon; pass `icon` (e.g. `lucideIcon`).
|
|
301
|
+
*/
|
|
302
|
+
function creditsPanel(options) {
|
|
303
|
+
const icon = options.icon ?? "ⓘ";
|
|
304
|
+
return (ctx) => {
|
|
305
|
+
const tabsName = `sg-credits-tabs-${panelCount++}`;
|
|
306
|
+
const wrap = document.createElement("div");
|
|
307
|
+
const button = document.createElement("button");
|
|
308
|
+
button.type = "button";
|
|
309
|
+
button.className = options.className ?? "btn btn-circle btn-sm";
|
|
310
|
+
setIcon(button, icon);
|
|
311
|
+
const dialog = document.createElement("dialog");
|
|
312
|
+
dialog.className = "modal";
|
|
313
|
+
const box = document.createElement("div");
|
|
314
|
+
box.className = "modal-box";
|
|
315
|
+
const close = document.createElement("button");
|
|
316
|
+
close.type = "button";
|
|
317
|
+
close.className = "btn btn-sm btn-circle btn-ghost absolute right-2 top-2";
|
|
318
|
+
close.textContent = "✕";
|
|
319
|
+
close.setAttribute("aria-label", "Close");
|
|
320
|
+
const heading = document.createElement("h3");
|
|
321
|
+
heading.className = "text-lg font-bold mb-3";
|
|
322
|
+
const loose = document.createElement("div");
|
|
323
|
+
loose.className = "flex flex-col gap-1";
|
|
324
|
+
const tablist = document.createElement("div");
|
|
325
|
+
tablist.setAttribute("role", "tablist");
|
|
326
|
+
tablist.className = "tabs tabs-border mt-2";
|
|
327
|
+
box.append(close, heading, loose, tablist);
|
|
328
|
+
const backdrop = document.createElement("form");
|
|
329
|
+
backdrop.method = "dialog";
|
|
330
|
+
backdrop.className = "modal-backdrop";
|
|
331
|
+
const backdropBtn = document.createElement("button");
|
|
332
|
+
backdropBtn.textContent = "close";
|
|
333
|
+
backdrop.appendChild(backdropBtn);
|
|
334
|
+
dialog.append(box, backdrop);
|
|
335
|
+
wrap.append(button, dialog);
|
|
336
|
+
const render = (entries) => {
|
|
337
|
+
const flat = entries.filter((e) => e.type !== "tab");
|
|
338
|
+
const tabs = entries.filter((e) => e.type === "tab");
|
|
339
|
+
loose.replaceChildren(...buildEntries(flat));
|
|
340
|
+
loose.style.display = flat.length ? "" : "none";
|
|
341
|
+
const nodes = [];
|
|
342
|
+
tabs.forEach((tab, i) => {
|
|
343
|
+
const input = document.createElement("input");
|
|
344
|
+
input.type = "radio";
|
|
345
|
+
input.name = tabsName;
|
|
346
|
+
input.role = "tab";
|
|
347
|
+
input.className = "tab";
|
|
348
|
+
input.setAttribute("aria-label", tab.label);
|
|
349
|
+
if (i === 0) input.checked = true;
|
|
350
|
+
const pane = document.createElement("div");
|
|
351
|
+
pane.className = "tab-content pt-3";
|
|
352
|
+
const list = document.createElement("div");
|
|
353
|
+
list.className = "flex flex-col gap-1";
|
|
354
|
+
list.replaceChildren(...buildEntries(tab.children));
|
|
355
|
+
pane.appendChild(list);
|
|
356
|
+
nodes.push(input, pane);
|
|
357
|
+
});
|
|
358
|
+
tablist.replaceChildren(...nodes);
|
|
359
|
+
tablist.style.display = tabs.length ? "" : "none";
|
|
360
|
+
};
|
|
361
|
+
const unbind = bindLocale(ctx, () => {
|
|
362
|
+
const title = resolveText(options.title ?? "Credits");
|
|
363
|
+
button.setAttribute("aria-label", title);
|
|
364
|
+
heading.textContent = title;
|
|
365
|
+
render(typeof options.items === "function" ? options.items() : options.items);
|
|
366
|
+
});
|
|
367
|
+
const onOpen = () => dialog.showModal();
|
|
368
|
+
const onClose = () => dialog.close();
|
|
369
|
+
button.addEventListener("click", onOpen);
|
|
370
|
+
close.addEventListener("click", onClose);
|
|
371
|
+
return {
|
|
372
|
+
element: wrap,
|
|
373
|
+
destroy() {
|
|
374
|
+
button.removeEventListener("click", onOpen);
|
|
375
|
+
close.removeEventListener("click", onClose);
|
|
376
|
+
unbind();
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
//#endregion
|
|
382
|
+
//#region src/components/action-button/action-button.ts
|
|
383
|
+
/**
|
|
384
|
+
* A generic clickable button — the lucide-free alternative to the opinionated
|
|
385
|
+
* prefabs. Supply any `icon` (glyph, your own SVG, a Lucide icon you wired up)
|
|
386
|
+
* and an `onClick`. daisyUI prefab (styled with `btn`).
|
|
387
|
+
*/
|
|
388
|
+
function actionButton(options) {
|
|
389
|
+
return (ctx) => {
|
|
390
|
+
const button = document.createElement("button");
|
|
391
|
+
button.type = "button";
|
|
392
|
+
button.className = options.className ?? "btn btn-sm";
|
|
393
|
+
if (options.icon !== void 0) setIcon(button, options.icon);
|
|
394
|
+
if (options.label !== void 0) {
|
|
395
|
+
const label = document.createElement("span");
|
|
396
|
+
label.textContent = options.label;
|
|
397
|
+
button.appendChild(label);
|
|
398
|
+
}
|
|
399
|
+
if (options.ariaLabel) button.setAttribute("aria-label", options.ariaLabel);
|
|
400
|
+
const onClick = () => options.onClick(ctx);
|
|
401
|
+
button.addEventListener("click", onClick);
|
|
402
|
+
return {
|
|
403
|
+
element: button,
|
|
404
|
+
destroy: () => button.removeEventListener("click", onClick)
|
|
405
|
+
};
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
//#endregion
|
|
409
|
+
//#region src/components/info/info.ts
|
|
410
|
+
/**
|
|
411
|
+
* An info panel: an ⓘ button that opens a centered daisyUI modal with a title
|
|
412
|
+
* and a block of text (with an ✕ close, click-outside and Esc). Content is data,
|
|
413
|
+
* so the app supplies the (already localized) strings. daisyUI + Lucide prefab.
|
|
414
|
+
*/
|
|
415
|
+
function infoPanel(options) {
|
|
416
|
+
return (ctx) => {
|
|
417
|
+
const wrap = document.createElement("div");
|
|
418
|
+
const button = document.createElement("button");
|
|
419
|
+
button.type = "button";
|
|
420
|
+
button.className = "btn btn-circle btn-sm";
|
|
421
|
+
setIcon(button, lucideIcon("Info"));
|
|
422
|
+
const dialog = document.createElement("dialog");
|
|
423
|
+
dialog.className = "modal";
|
|
424
|
+
const box = document.createElement("div");
|
|
425
|
+
box.className = "modal-box max-w-sm";
|
|
426
|
+
const close = document.createElement("button");
|
|
427
|
+
close.type = "button";
|
|
428
|
+
close.className = "btn btn-sm btn-circle btn-ghost absolute right-2 top-2";
|
|
429
|
+
close.textContent = "✕";
|
|
430
|
+
const heading = document.createElement("h3");
|
|
431
|
+
heading.className = "text-lg font-bold mb-3";
|
|
432
|
+
const text = document.createElement("p");
|
|
433
|
+
text.className = "text-sm leading-relaxed";
|
|
434
|
+
box.append(close, heading, text);
|
|
435
|
+
const backdrop = document.createElement("form");
|
|
436
|
+
backdrop.method = "dialog";
|
|
437
|
+
backdrop.className = "modal-backdrop";
|
|
438
|
+
const backdropBtn = document.createElement("button");
|
|
439
|
+
backdropBtn.textContent = "close";
|
|
440
|
+
backdrop.appendChild(backdropBtn);
|
|
441
|
+
dialog.append(box, backdrop);
|
|
442
|
+
wrap.append(button, dialog);
|
|
443
|
+
const unbind = bindLocale(ctx, () => {
|
|
444
|
+
const title = resolveText(options.title);
|
|
445
|
+
button.setAttribute("aria-label", title);
|
|
446
|
+
heading.textContent = title;
|
|
447
|
+
text.textContent = resolveText(options.text);
|
|
448
|
+
close.setAttribute("aria-label", resolveText(options.closeLabel ?? "Close"));
|
|
449
|
+
});
|
|
450
|
+
const onOpen = () => dialog.showModal();
|
|
451
|
+
const onClose = () => dialog.close();
|
|
452
|
+
button.addEventListener("click", onOpen);
|
|
453
|
+
close.addEventListener("click", onClose);
|
|
454
|
+
return {
|
|
455
|
+
element: wrap,
|
|
456
|
+
destroy: () => {
|
|
457
|
+
button.removeEventListener("click", onOpen);
|
|
458
|
+
close.removeEventListener("click", onClose);
|
|
459
|
+
unbind();
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
//#endregion
|
|
465
|
+
//#region src/components/artwork-info/artwork-info.ts
|
|
466
|
+
const AVAILABILITY_COLOR = {
|
|
467
|
+
available: "#22c55e",
|
|
468
|
+
unavailable: "#ef4444"
|
|
469
|
+
};
|
|
470
|
+
const STYLE_ID = "scenography-artwork-info-styles";
|
|
471
|
+
const PANEL_FRACTION = .4;
|
|
472
|
+
const FREE_FRACTION = 1 - PANEL_FRACTION;
|
|
473
|
+
const CENTER_X = .7;
|
|
474
|
+
/**
|
|
475
|
+
* Inject the panel's own CSS once. Self-contained on purpose: a consumer's
|
|
476
|
+
* Tailwind build doesn't scan this package in `node_modules`, so utility classes
|
|
477
|
+
* wouldn't be generated — the panel must not depend on them.
|
|
478
|
+
*/
|
|
479
|
+
function ensureStyles() {
|
|
480
|
+
if (typeof document === "undefined" || document.getElementById(STYLE_ID)) return;
|
|
481
|
+
const style = document.createElement("style");
|
|
482
|
+
style.id = STYLE_ID;
|
|
483
|
+
style.textContent = `.sg-artwork-panel{position:absolute;left:0;top:0;height:100%;width:${PANEL_FRACTION * 100}%;box-sizing:border-box;background:rgba(0,0,0,0.85);color:#fff;padding:2.5rem;z-index:10;overflow-y:auto;display:none;flex-direction:column;gap:0.75rem;pointer-events:auto;font-family:system-ui,sans-serif;}.sg-artwork-panel--open{display:flex;}.sg-artwork-panel__close{align-self:flex-end;background:none;border:none;color:#fff;font-size:1.5rem;line-height:1;cursor:pointer;opacity:0.7;padding:0;}.sg-artwork-panel__close:hover{opacity:1;}.sg-artwork-panel__title{font-size:1.75rem;font-weight:700;margin:0;}.sg-artwork-panel__meta{display:flex;flex-direction:column;gap:0.15rem;font-size:0.9rem;opacity:0.8;}.sg-artwork-panel__status{display:flex;align-items:center;gap:0.5rem;font-size:0.9rem;font-weight:600;}.sg-artwork-panel__dot{width:0.7rem;height:0.7rem;border-radius:50%;flex:none;}.sg-artwork-panel__desc{font-size:1rem;line-height:1.6;margin-top:0.5rem;white-space:pre-line;}.sg-artwork-panel__link{align-self:flex-start;margin-top:0.5rem;display:inline-flex;align-items:center;gap:0.4rem;color:#fff;text-decoration:underline;font-size:0.95rem;}.sg-artwork-panel__link:hover{opacity:0.8;}`;
|
|
484
|
+
document.head.appendChild(style);
|
|
485
|
+
}
|
|
486
|
+
const join = (parts, sep) => parts.filter((p) => Boolean(p)).join(sep);
|
|
487
|
+
/**
|
|
488
|
+
* The artwork info panel. The engine emits `artwork:select { id }` when you click
|
|
489
|
+
* a piece's cartela (or the piece itself); this opens a left-hand panel (40%
|
|
490
|
+
* width, full height, black at 85% alpha) with the full caption and asks the
|
|
491
|
+
* camera to frame the piece in the free right 60%. Closing with ✕ re-centers the
|
|
492
|
+
* piece full-width; if the user walks/turns away the engine emits
|
|
493
|
+
* `artwork:released` and the panel just closes (no camera move).
|
|
494
|
+
*
|
|
495
|
+
* Content is data (`artworks`), unlocalized for now. The panel is mounted on the
|
|
496
|
+
* surface (escaping the overlay grid); the slot element is an inert placeholder.
|
|
497
|
+
*/
|
|
498
|
+
function artworkInfo(options) {
|
|
499
|
+
const byId = new Map(options.artworks.map((a) => [a.id, a]));
|
|
500
|
+
return (ctx) => {
|
|
501
|
+
ensureStyles();
|
|
502
|
+
const camera = ctx.gallery.ctx.camera;
|
|
503
|
+
const panel = document.createElement("div");
|
|
504
|
+
panel.className = "sg-artwork-panel sg-overlay__item";
|
|
505
|
+
const close = document.createElement("button");
|
|
506
|
+
close.type = "button";
|
|
507
|
+
close.className = "sg-artwork-panel__close";
|
|
508
|
+
close.textContent = "✕";
|
|
509
|
+
close.setAttribute("aria-label", options.closeLabel ?? "Close");
|
|
510
|
+
const title = document.createElement("h2");
|
|
511
|
+
title.className = "sg-artwork-panel__title";
|
|
512
|
+
const meta = document.createElement("div");
|
|
513
|
+
meta.className = "sg-artwork-panel__meta";
|
|
514
|
+
const status = document.createElement("div");
|
|
515
|
+
status.className = "sg-artwork-panel__status";
|
|
516
|
+
const statusDot = document.createElement("span");
|
|
517
|
+
statusDot.className = "sg-artwork-panel__dot";
|
|
518
|
+
const statusText = document.createElement("span");
|
|
519
|
+
status.append(statusDot, statusText);
|
|
520
|
+
const desc = document.createElement("p");
|
|
521
|
+
desc.className = "sg-artwork-panel__desc";
|
|
522
|
+
const link = document.createElement("a");
|
|
523
|
+
link.className = "sg-artwork-panel__link";
|
|
524
|
+
link.target = "_blank";
|
|
525
|
+
link.rel = "noopener noreferrer";
|
|
526
|
+
link.textContent = `${options.linkText ?? "View artwork"} ↗`;
|
|
527
|
+
panel.append(close, title, meta, status, desc, link);
|
|
528
|
+
ctx.surface.appendChild(panel);
|
|
529
|
+
let currentId = null;
|
|
530
|
+
const render = (artwork) => {
|
|
531
|
+
title.textContent = artwork.title ?? "";
|
|
532
|
+
const lines = [join([artwork.author, artwork.year?.toString()], ", "), join([artwork.technique, artwork.dimensions], ", ")].filter(Boolean);
|
|
533
|
+
meta.replaceChildren(...lines.map((line) => {
|
|
534
|
+
const span = document.createElement("span");
|
|
535
|
+
span.textContent = line;
|
|
536
|
+
return span;
|
|
537
|
+
}));
|
|
538
|
+
const av = artwork.availability;
|
|
539
|
+
if (av === void 0) status.style.display = "none";
|
|
540
|
+
else {
|
|
541
|
+
status.style.display = "flex";
|
|
542
|
+
const forSale = av === "for-sale";
|
|
543
|
+
statusDot.style.backgroundColor = forSale ? AVAILABILITY_COLOR.available : AVAILABILITY_COLOR.unavailable;
|
|
544
|
+
statusText.textContent = forSale ? options.availabilityText?.forSale ?? "For sale" : options.availabilityText?.privateCollection ?? "Private collection";
|
|
545
|
+
}
|
|
546
|
+
desc.textContent = artwork.description ?? "";
|
|
547
|
+
if (artwork.href) {
|
|
548
|
+
link.href = artwork.href;
|
|
549
|
+
link.style.display = "inline-flex";
|
|
550
|
+
} else link.style.display = "none";
|
|
551
|
+
};
|
|
552
|
+
const hide = () => {
|
|
553
|
+
panel.classList.remove("sg-artwork-panel--open");
|
|
554
|
+
currentId = null;
|
|
555
|
+
};
|
|
556
|
+
const onSelect = (payload) => {
|
|
557
|
+
const id = payload?.id;
|
|
558
|
+
const artwork = id ? byId.get(id) : void 0;
|
|
559
|
+
if (!artwork || !id) return;
|
|
560
|
+
currentId = id;
|
|
561
|
+
render(artwork);
|
|
562
|
+
panel.classList.add("sg-artwork-panel--open");
|
|
563
|
+
camera.focusArtwork(id, {
|
|
564
|
+
centerX: CENTER_X,
|
|
565
|
+
widthFraction: FREE_FRACTION
|
|
566
|
+
});
|
|
567
|
+
};
|
|
568
|
+
const onReleased = (payload) => {
|
|
569
|
+
const id = payload?.id;
|
|
570
|
+
if (id && id === currentId) hide();
|
|
571
|
+
};
|
|
572
|
+
const onClose = () => {
|
|
573
|
+
const id = currentId;
|
|
574
|
+
hide();
|
|
575
|
+
if (id) camera.centerArtwork(id);
|
|
576
|
+
};
|
|
577
|
+
const offSelect = ctx.gallery.on("artwork:select", onSelect);
|
|
578
|
+
const offReleased = ctx.gallery.on("artwork:released", onReleased);
|
|
579
|
+
close.addEventListener("click", onClose);
|
|
580
|
+
const placeholder = document.createElement("div");
|
|
581
|
+
placeholder.style.display = "none";
|
|
582
|
+
return {
|
|
583
|
+
element: placeholder,
|
|
584
|
+
destroy() {
|
|
585
|
+
offSelect();
|
|
586
|
+
offReleased();
|
|
587
|
+
close.removeEventListener("click", onClose);
|
|
588
|
+
panel.remove();
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
//#endregion
|
|
594
|
+
//#region src/components/watermark/watermark.ts
|
|
595
|
+
/**
|
|
596
|
+
* A small, unobtrusive text watermark (a "brand stamp"). No daisyUI/Lucide — just
|
|
597
|
+
* a styled element. Defaults to the text "scenography".
|
|
598
|
+
*/
|
|
599
|
+
function watermark(options = {}) {
|
|
600
|
+
return () => {
|
|
601
|
+
const el = document.createElement("div");
|
|
602
|
+
el.textContent = options.text ?? "scenography";
|
|
603
|
+
if (options.className) el.className = options.className;
|
|
604
|
+
else el.style.cssText = "font:600 13px system-ui,sans-serif;color:#fff;opacity:0.6;";
|
|
605
|
+
return el;
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
//#endregion
|
|
609
|
+
//#region src/components/settings/settings.ts
|
|
610
|
+
/**
|
|
611
|
+
* A settings panel: a ⚙ button that opens a centered daisyUI modal (with an ✕
|
|
612
|
+
* close, click-outside and Esc). Each section lives in its own tab (the first
|
|
613
|
+
* present one opens by default). Sections are data — the app supplies labels,
|
|
614
|
+
* current values and callbacks — so the panel stays generic. Labels are `Text`
|
|
615
|
+
* and relocalize live via the overlay's locale source. daisyUI + Lucide prefab.
|
|
616
|
+
*/
|
|
617
|
+
function settingsPanel(options) {
|
|
618
|
+
return (ctx) => {
|
|
619
|
+
const wrap = document.createElement("div");
|
|
620
|
+
const button = document.createElement("button");
|
|
621
|
+
button.type = "button";
|
|
622
|
+
button.className = "btn btn-circle btn-sm";
|
|
623
|
+
setIcon(button, lucideIcon("Settings"));
|
|
624
|
+
const dialog = document.createElement("dialog");
|
|
625
|
+
dialog.className = "modal";
|
|
626
|
+
const box = document.createElement("div");
|
|
627
|
+
box.className = "modal-box max-w-sm";
|
|
628
|
+
const close = document.createElement("button");
|
|
629
|
+
close.type = "button";
|
|
630
|
+
close.className = "btn btn-sm btn-circle btn-ghost absolute right-2 top-2";
|
|
631
|
+
close.textContent = "✕";
|
|
632
|
+
box.appendChild(close);
|
|
633
|
+
const heading = document.createElement("h3");
|
|
634
|
+
heading.className = "text-lg font-bold mb-4";
|
|
635
|
+
box.appendChild(heading);
|
|
636
|
+
const tablist = document.createElement("div");
|
|
637
|
+
tablist.setAttribute("role", "tablist");
|
|
638
|
+
tablist.className = "tabs tabs-border";
|
|
639
|
+
box.appendChild(tablist);
|
|
640
|
+
const cleanups = [];
|
|
641
|
+
const relabels = [];
|
|
642
|
+
let first = true;
|
|
643
|
+
const addTab = (tab, fallback, content) => {
|
|
644
|
+
const input = document.createElement("input");
|
|
645
|
+
input.type = "radio";
|
|
646
|
+
input.name = "sg-settings-tabs";
|
|
647
|
+
input.role = "tab";
|
|
648
|
+
input.className = "tab";
|
|
649
|
+
if (first) {
|
|
650
|
+
input.checked = true;
|
|
651
|
+
first = false;
|
|
652
|
+
}
|
|
653
|
+
const pane = document.createElement("div");
|
|
654
|
+
pane.className = "tab-content pt-4";
|
|
655
|
+
pane.appendChild(content);
|
|
656
|
+
tablist.append(input, pane);
|
|
657
|
+
relabels.push(() => input.setAttribute("aria-label", resolveText(tab ?? fallback)));
|
|
658
|
+
};
|
|
659
|
+
if (options.language) {
|
|
660
|
+
const lang = options.language;
|
|
661
|
+
const field = document.createElement("label");
|
|
662
|
+
field.className = "form-control w-full";
|
|
663
|
+
const label = document.createElement("span");
|
|
664
|
+
label.className = "label-text mb-1 block";
|
|
665
|
+
const select = document.createElement("select");
|
|
666
|
+
select.className = "select select-bordered select-sm w-full";
|
|
667
|
+
const optionEls = lang.options.map((opt) => {
|
|
668
|
+
const o = document.createElement("option");
|
|
669
|
+
o.value = opt.value;
|
|
670
|
+
select.appendChild(o);
|
|
671
|
+
return {
|
|
672
|
+
o,
|
|
673
|
+
label: opt.label
|
|
674
|
+
};
|
|
675
|
+
});
|
|
676
|
+
select.value = lang.value;
|
|
677
|
+
const onChange = () => lang.onChange(select.value);
|
|
678
|
+
select.addEventListener("change", onChange);
|
|
679
|
+
cleanups.push(() => select.removeEventListener("change", onChange));
|
|
680
|
+
relabels.push(() => {
|
|
681
|
+
label.textContent = resolveText(lang.label);
|
|
682
|
+
for (const { o, label: l } of optionEls) o.textContent = resolveText(l);
|
|
683
|
+
});
|
|
684
|
+
field.append(label, select);
|
|
685
|
+
addTab(lang.tab, lang.label, field);
|
|
686
|
+
}
|
|
687
|
+
if (options.handedness) {
|
|
688
|
+
const hand = options.handedness;
|
|
689
|
+
const pane = document.createElement("div");
|
|
690
|
+
const legend = document.createElement("div");
|
|
691
|
+
legend.className = "label-text mb-1 block";
|
|
692
|
+
pane.appendChild(legend);
|
|
693
|
+
const onChange = (e) => hand.onChange(e.target.value);
|
|
694
|
+
const texts = [];
|
|
695
|
+
for (const opt of hand.options) {
|
|
696
|
+
const row = document.createElement("label");
|
|
697
|
+
row.className = "label cursor-pointer justify-start gap-3 py-1";
|
|
698
|
+
const input = document.createElement("input");
|
|
699
|
+
input.type = "radio";
|
|
700
|
+
input.name = "sg-settings-handedness";
|
|
701
|
+
input.value = opt.value;
|
|
702
|
+
input.className = "radio radio-sm";
|
|
703
|
+
input.checked = opt.value === hand.value;
|
|
704
|
+
input.addEventListener("change", onChange);
|
|
705
|
+
cleanups.push(() => input.removeEventListener("change", onChange));
|
|
706
|
+
const text = document.createElement("span");
|
|
707
|
+
text.className = "label-text text-sm";
|
|
708
|
+
row.append(input, text);
|
|
709
|
+
pane.appendChild(row);
|
|
710
|
+
texts.push({
|
|
711
|
+
el: text,
|
|
712
|
+
label: opt.label
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
relabels.push(() => {
|
|
716
|
+
legend.textContent = resolveText(hand.label);
|
|
717
|
+
for (const { el, label } of texts) el.textContent = resolveText(label);
|
|
718
|
+
});
|
|
719
|
+
addTab(hand.tab, hand.label, pane);
|
|
720
|
+
}
|
|
721
|
+
if (options.graphics) {
|
|
722
|
+
const gfx = options.graphics;
|
|
723
|
+
const pane = document.createElement("div");
|
|
724
|
+
const legend = document.createElement("div");
|
|
725
|
+
legend.className = "label-text mb-1 block";
|
|
726
|
+
pane.appendChild(legend);
|
|
727
|
+
const groupLabels = [];
|
|
728
|
+
for (const group of gfx.choiceGroups ?? []) {
|
|
729
|
+
const field = document.createElement("div");
|
|
730
|
+
field.className = "mb-3";
|
|
731
|
+
const label = document.createElement("span");
|
|
732
|
+
label.className = "label-text text-sm mb-1 block";
|
|
733
|
+
const row = document.createElement("div");
|
|
734
|
+
row.className = "join w-full";
|
|
735
|
+
const name = `sg-settings-${group.key}`;
|
|
736
|
+
const opts = group.options.map((opt) => {
|
|
737
|
+
const input = document.createElement("input");
|
|
738
|
+
input.type = "radio";
|
|
739
|
+
input.name = name;
|
|
740
|
+
input.className = "btn btn-sm join-item flex-1";
|
|
741
|
+
input.value = opt.value;
|
|
742
|
+
input.checked = opt.value === group.value;
|
|
743
|
+
const onChange = () => group.onChange(input.value);
|
|
744
|
+
input.addEventListener("change", onChange);
|
|
745
|
+
cleanups.push(() => input.removeEventListener("change", onChange));
|
|
746
|
+
row.appendChild(input);
|
|
747
|
+
return {
|
|
748
|
+
input,
|
|
749
|
+
label: opt.label
|
|
750
|
+
};
|
|
751
|
+
});
|
|
752
|
+
field.append(label, row);
|
|
753
|
+
pane.appendChild(field);
|
|
754
|
+
groupLabels.push({
|
|
755
|
+
el: label,
|
|
756
|
+
label: group.label,
|
|
757
|
+
opts
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
const list = document.createElement("div");
|
|
761
|
+
list.className = "flex flex-col gap-1";
|
|
762
|
+
pane.appendChild(list);
|
|
763
|
+
const texts = [];
|
|
764
|
+
for (const item of gfx.items) {
|
|
765
|
+
const row = document.createElement("label");
|
|
766
|
+
row.className = "label cursor-pointer justify-start gap-3 py-1";
|
|
767
|
+
const input = document.createElement("input");
|
|
768
|
+
input.type = "checkbox";
|
|
769
|
+
input.className = "toggle toggle-sm";
|
|
770
|
+
input.checked = item.value;
|
|
771
|
+
const onChange = () => gfx.onChange(item.key, input.checked);
|
|
772
|
+
input.addEventListener("change", onChange);
|
|
773
|
+
cleanups.push(() => input.removeEventListener("change", onChange));
|
|
774
|
+
const text = document.createElement("span");
|
|
775
|
+
text.className = "label-text text-sm";
|
|
776
|
+
row.append(input, text);
|
|
777
|
+
list.appendChild(row);
|
|
778
|
+
texts.push({
|
|
779
|
+
el: text,
|
|
780
|
+
label: item.label
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
const sliderLabels = [];
|
|
784
|
+
for (const slider of gfx.sliders ?? []) {
|
|
785
|
+
const field = document.createElement("label");
|
|
786
|
+
field.className = "flex flex-col gap-1 mt-2";
|
|
787
|
+
const head = document.createElement("span");
|
|
788
|
+
head.className = "label-text text-sm flex justify-between";
|
|
789
|
+
const name = document.createElement("span");
|
|
790
|
+
const value = document.createElement("span");
|
|
791
|
+
value.className = "opacity-60";
|
|
792
|
+
head.append(name, value);
|
|
793
|
+
const input = document.createElement("input");
|
|
794
|
+
input.type = "range";
|
|
795
|
+
input.className = "range range-sm";
|
|
796
|
+
input.min = String(slider.min);
|
|
797
|
+
input.max = String(slider.max);
|
|
798
|
+
input.step = String(slider.step ?? .1);
|
|
799
|
+
input.value = String(slider.value);
|
|
800
|
+
value.textContent = input.value;
|
|
801
|
+
const onInput = () => {
|
|
802
|
+
value.textContent = input.value;
|
|
803
|
+
slider.onChange(parseFloat(input.value));
|
|
804
|
+
};
|
|
805
|
+
input.addEventListener("input", onInput);
|
|
806
|
+
cleanups.push(() => input.removeEventListener("input", onInput));
|
|
807
|
+
field.append(head, input);
|
|
808
|
+
pane.appendChild(field);
|
|
809
|
+
sliderLabels.push({
|
|
810
|
+
el: name,
|
|
811
|
+
label: slider.label
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
relabels.push(() => {
|
|
815
|
+
legend.textContent = resolveText(gfx.label);
|
|
816
|
+
for (const { el, label, opts } of groupLabels) {
|
|
817
|
+
el.textContent = resolveText(label);
|
|
818
|
+
for (const { input, label: l } of opts) input.setAttribute("aria-label", resolveText(l));
|
|
819
|
+
}
|
|
820
|
+
for (const { el, label } of texts) el.textContent = resolveText(label);
|
|
821
|
+
for (const { el, label } of sliderLabels) el.textContent = resolveText(label);
|
|
822
|
+
});
|
|
823
|
+
addTab(gfx.tab, gfx.label, pane);
|
|
824
|
+
}
|
|
825
|
+
const backdrop = document.createElement("form");
|
|
826
|
+
backdrop.method = "dialog";
|
|
827
|
+
backdrop.className = "modal-backdrop";
|
|
828
|
+
const backdropBtn = document.createElement("button");
|
|
829
|
+
backdropBtn.textContent = "close";
|
|
830
|
+
backdrop.appendChild(backdropBtn);
|
|
831
|
+
dialog.append(box, backdrop);
|
|
832
|
+
wrap.append(button, dialog);
|
|
833
|
+
const unbind = bindLocale(ctx, () => {
|
|
834
|
+
const title = resolveText(options.title);
|
|
835
|
+
button.setAttribute("aria-label", title);
|
|
836
|
+
heading.textContent = title;
|
|
837
|
+
close.setAttribute("aria-label", resolveText(options.closeLabel ?? "Close"));
|
|
838
|
+
for (const relabel of relabels) relabel();
|
|
839
|
+
});
|
|
840
|
+
const onOpen = () => dialog.showModal();
|
|
841
|
+
const onClose = () => dialog.close();
|
|
842
|
+
button.addEventListener("click", onOpen);
|
|
843
|
+
close.addEventListener("click", onClose);
|
|
844
|
+
return {
|
|
845
|
+
element: wrap,
|
|
846
|
+
destroy: () => {
|
|
847
|
+
button.removeEventListener("click", onOpen);
|
|
848
|
+
close.removeEventListener("click", onClose);
|
|
849
|
+
for (const fn of cleanups) fn();
|
|
850
|
+
unbind();
|
|
851
|
+
}
|
|
852
|
+
};
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
//#endregion
|
|
856
|
+
//#region src/presets/default.ts
|
|
857
|
+
/**
|
|
858
|
+
* The default overlay layout: mute + fullscreen (and a settings ⚙ when given)
|
|
859
|
+
* top-right; an info ⓘ and a credits panel bottom-right when given. Wired with
|
|
860
|
+
* Lucide icons — this is the opinionated, batteries-included layer, so the
|
|
861
|
+
* coupling to daisyUI + Lucide lives here, not in `@scenography/overlay`.
|
|
862
|
+
*/
|
|
863
|
+
function defaultPreset(overlay, options = {}) {
|
|
864
|
+
overlay.add("top-right", muteButton({ icons: {
|
|
865
|
+
on: lucideIcon("Volume2"),
|
|
866
|
+
off: lucideIcon("VolumeX")
|
|
867
|
+
} }));
|
|
868
|
+
overlay.add("top-right", fullscreenButton({ icons: {
|
|
869
|
+
enter: lucideIcon("Maximize"),
|
|
870
|
+
exit: lucideIcon("Minimize")
|
|
871
|
+
} }));
|
|
872
|
+
if (options.settings) overlay.add("top-right", settingsPanel(options.settings));
|
|
873
|
+
if (options.environment) overlay.add("top-right", environmentPanel({
|
|
874
|
+
icon: lucideIcon("CloudSun"),
|
|
875
|
+
...options.environment
|
|
876
|
+
}));
|
|
877
|
+
overlay.add("top-right", cardboardButton({ icon: lucideIcon("RectangleGoggles") }));
|
|
878
|
+
if (options.artworks) overlay.add("middle-left", artworkInfo(options.artworks));
|
|
879
|
+
if (options.info) overlay.add("bottom-right", infoPanel(options.info));
|
|
880
|
+
if (options.credits) overlay.add("bottom-right", creditsPanel({
|
|
881
|
+
items: options.credits,
|
|
882
|
+
title: options.creditsTitle,
|
|
883
|
+
icon: lucideIcon("Copyleft")
|
|
884
|
+
}));
|
|
885
|
+
if (options.watermark !== false) overlay.add("bottom-left", watermark(options.watermark ?? {}));
|
|
886
|
+
}
|
|
887
|
+
//#endregion
|
|
888
|
+
export { actionButton, artworkInfo, cardboardButton, creditsPanel, defaultPreset, environmentPanel, fullscreenButton, infoPanel, lucideIcon, muteButton, settingsPanel, watermark };
|
|
889
|
+
|
|
890
|
+
//# sourceMappingURL=index.mjs.map
|