@scenography/overlay-presets 0.2.0 → 0.3.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 CHANGED
@@ -133,6 +133,7 @@ function cardboardButton(options = {}) {
133
133
  ctx.gallery.setRender({ cardboard: enabled });
134
134
  button.classList.toggle("btn-active", enabled);
135
135
  button.setAttribute("aria-pressed", String(enabled));
136
+ ctx.gallery.emit("cardboard:change", { enabled });
136
137
  };
137
138
  button.addEventListener("click", onClick);
138
139
  return {
@@ -142,6 +143,161 @@ function cardboardButton(options = {}) {
142
143
  };
143
144
  }
144
145
  //#endregion
146
+ //#region src/components/reset-view/reset-view.ts
147
+ /**
148
+ * A "home" button that returns the camera to the scene's start position and
149
+ * orientation (`ctx.camera.reset()`) — for visitors who wander off and want to
150
+ * get back to the entrance. Defaults to a plain glyph; pass `lucideIcon("House")`.
151
+ */
152
+ function resetViewButton(options = {}) {
153
+ const icon = options.icon ?? "⌂";
154
+ return (ctx) => {
155
+ const button = document.createElement("button");
156
+ button.type = "button";
157
+ button.className = options.className ?? "btn btn-circle btn-sm";
158
+ setIcon(button, icon);
159
+ const onClick = () => ctx.gallery.ctx.camera.reset();
160
+ button.addEventListener("click", onClick);
161
+ const unbind = bindLocale(ctx, () => button.setAttribute("aria-label", (0, _scenography_overlay.resolveText)(options.label ?? "Reset view")));
162
+ return {
163
+ element: button,
164
+ destroy: () => {
165
+ button.removeEventListener("click", onClick);
166
+ unbind();
167
+ }
168
+ };
169
+ };
170
+ }
171
+ //#endregion
172
+ //#region src/components/view-mode/view-mode.ts
173
+ /** Lucide icon per mode, shown beside its label in the picker. */
174
+ const MODE_ICON = {
175
+ touch: "Hand",
176
+ vr: "Orbit",
177
+ cardboard: "RectangleGoggles"
178
+ };
179
+ /**
180
+ * A camera-control mode picker: a `ScanEye` button opening a small modal with one
181
+ * entry per mode (Touch / VR / Cardboard). Picking one calls
182
+ * `ctx.viewControls.setMode` — which, inside this click gesture, flips the
183
+ * gyroscope and stereo split (so iOS grants motion here). The active mode is
184
+ * marked and kept in sync via the `controls:change` event, so the settings panel
185
+ * and this picker never disagree.
186
+ */
187
+ function viewModeButton(options) {
188
+ return (ctx) => {
189
+ const viewControls = ctx.gallery.ctx.viewControls;
190
+ const wrap = document.createElement("div");
191
+ const button = document.createElement("button");
192
+ button.type = "button";
193
+ button.className = "btn btn-circle btn-sm";
194
+ setIcon(button, lucideIcon("ScanEye"));
195
+ const dialog = document.createElement("dialog");
196
+ dialog.className = "modal";
197
+ const box = document.createElement("div");
198
+ box.className = "modal-box max-w-xs";
199
+ const close = document.createElement("button");
200
+ close.type = "button";
201
+ close.className = "btn btn-sm btn-circle btn-ghost absolute right-2 top-2";
202
+ close.textContent = "✕";
203
+ box.appendChild(close);
204
+ const heading = document.createElement("h3");
205
+ heading.className = "text-lg font-bold mb-4";
206
+ box.appendChild(heading);
207
+ const list = document.createElement("div");
208
+ list.className = "flex flex-col gap-2";
209
+ box.appendChild(list);
210
+ const cleanups = [];
211
+ const relabels = [];
212
+ const optionButtons = [];
213
+ for (const item of options.modes) {
214
+ const optBtn = document.createElement("button");
215
+ optBtn.type = "button";
216
+ optBtn.className = "btn btn-block justify-start gap-3";
217
+ const iconEl = document.createElement("span");
218
+ iconEl.style.display = "inline-flex";
219
+ setIcon(iconEl, lucideIcon(MODE_ICON[item.mode]));
220
+ const label = document.createElement("span");
221
+ optBtn.append(iconEl, label);
222
+ const onPick = () => {
223
+ viewControls.setMode(item.mode);
224
+ syncActive();
225
+ dialog.close();
226
+ };
227
+ optBtn.addEventListener("click", onPick);
228
+ cleanups.push(() => optBtn.removeEventListener("click", onPick));
229
+ list.appendChild(optBtn);
230
+ optionButtons.push({
231
+ el: optBtn,
232
+ mode: item.mode
233
+ });
234
+ relabels.push(() => label.textContent = (0, _scenography_overlay.resolveText)(item.label));
235
+ }
236
+ const syncActive = () => {
237
+ const current = viewControls.getMode();
238
+ for (const { el, mode } of optionButtons) el.classList.toggle("btn-active", mode === current);
239
+ };
240
+ const backdrop = document.createElement("form");
241
+ backdrop.method = "dialog";
242
+ backdrop.className = "modal-backdrop";
243
+ const backdropBtn = document.createElement("button");
244
+ backdropBtn.textContent = "close";
245
+ backdrop.appendChild(backdropBtn);
246
+ dialog.append(box, backdrop);
247
+ wrap.append(button, dialog);
248
+ const offChange = ctx.gallery.on("controls:change", syncActive);
249
+ const unbind = bindLocale(ctx, () => {
250
+ const title = (0, _scenography_overlay.resolveText)(options.title);
251
+ button.setAttribute("aria-label", title);
252
+ heading.textContent = title;
253
+ close.setAttribute("aria-label", (0, _scenography_overlay.resolveText)(options.closeLabel ?? "Close"));
254
+ for (const relabel of relabels) relabel();
255
+ });
256
+ const onOpen = () => {
257
+ syncActive();
258
+ dialog.showModal();
259
+ };
260
+ const onClose = () => dialog.close();
261
+ button.addEventListener("click", onOpen);
262
+ close.addEventListener("click", onClose);
263
+ return {
264
+ element: wrap,
265
+ destroy: () => {
266
+ button.removeEventListener("click", onOpen);
267
+ close.removeEventListener("click", onClose);
268
+ for (const fn of cleanups) fn();
269
+ offChange();
270
+ unbind();
271
+ }
272
+ };
273
+ };
274
+ }
275
+ //#endregion
276
+ //#region src/components/view-mode/capabilities.ts
277
+ /**
278
+ * Sniff the device's control capabilities. Deliberately conservative: gyro is
279
+ * gated behind a touch (mobile) device too, since desktops expose the
280
+ * `DeviceOrientationEvent` interface but never fire it — pairing it with a coarse
281
+ * pointer avoids offering VR/Cardboard where they can't work.
282
+ */
283
+ function detectControlCapabilities() {
284
+ return {
285
+ touch: typeof matchMedia !== "undefined" ? matchMedia("(pointer: coarse)").matches : typeof navigator !== "undefined" && (navigator.maxTouchPoints ?? 0) > 0,
286
+ gyro: typeof window !== "undefined" && "DeviceOrientationEvent" in window
287
+ };
288
+ }
289
+ /**
290
+ * The modes worth offering on this device, in display order. Touch needs a touch
291
+ * screen; VR and Cardboard additionally need the gyro. Desktop (no touch) gets an
292
+ * empty list — it uses keyboard/mouse and shows no mode picker at all.
293
+ */
294
+ function availableModes(caps) {
295
+ const modes = [];
296
+ if (caps.touch) modes.push("touch");
297
+ if (caps.touch && caps.gyro) modes.push("vr", "cardboard");
298
+ return modes;
299
+ }
300
+ //#endregion
145
301
  //#region src/components/environment/environment.ts
146
302
  /**
147
303
  * An environment panel: a button (e.g. a sun/cloud icon) opening a daisyUI modal
@@ -463,6 +619,286 @@ function infoPanel(options) {
463
619
  };
464
620
  }
465
621
  //#endregion
622
+ //#region src/components/help/diagrams.ts
623
+ const CLOSE = `</svg>`;
624
+ const DIAGRAMS = { cardboard: "<svg viewBox=\"0 0 24 24\" width=\"100%\" height=\"100%\" fill=\"currentColor\" role=\"img\" aria-hidden=\"true\"><path d=\"M9.26402604,18 C8.79719433,18.6226198 8.05922695,19 7.26393202,19 L4.5,19 C3.11928813,19 2,17.8807119 2,16.5 L2,9.5 C2,8.27576856 2.8799598,7.25706275 4.04188098,7.04188098 C4.25706275,5.8799598 5.27576856,5 6.5,5 L17.5,5 C18.7242314,5 19.7429373,5.8799598 19.958119,7.04188098 C21.1200402,7.25706275 22,8.27576856 22,9.5 L22,16.5 C22,17.8807119 20.8807119,19 19.5,19 L16.736068,19 C15.940773,19 15.2028057,18.6226198 14.735974,18 L9.26402604,18 Z M9.82576985,17 L14.1742301,17 C13.7685119,16.3807773 13.0746523,16 12.322949,16 L11.677051,16 C10.9253477,16 10.2314881,16.3807773 9.82576985,17 Z M5.08535285,7 L18.9146471,7 C18.7087289,6.41740381 18.1531094,6 17.5,6 L6.5,6 C5.84689059,6 5.29127106,6.41740381 5.08535285,7 Z M8.8027864,16.7763932 C9.34713196,15.6877021 10.4598573,15 11.677051,15 L12.322949,15 C13.5401427,15 14.652868,15.6877021 15.1972136,16.7763932 L15.3944272,17.1708204 C15.6485152,17.6789964 16.1679099,18 16.736068,18 L19.5,18 C20.3284271,18 21,17.3284271 21,16.5 L21,9.5 C21,8.67157288 20.3284271,8 19.5,8 L4.5,8 C3.67157288,8 3,8.67157288 3,9.5 L3,16.5 C3,17.3284271 3.67157288,18 4.5,18 L7.26393202,18 C7.83209011,18 8.35148479,17.6789964 8.60557281,17.1708204 L8.8027864,16.7763932 Z M7.5,15 C6.11928813,15 5,13.8807119 5,12.5 C5,11.1192881 6.11928813,10 7.5,10 C8.88071187,10 10,11.1192881 10,12.5 C10,13.8807119 8.88071187,15 7.5,15 Z M7.5,14 C8.32842712,14 9,13.3284271 9,12.5 C9,11.6715729 8.32842712,11 7.5,11 C6.67157288,11 6,11.6715729 6,12.5 C6,13.3284271 6.67157288,14 7.5,14 Z M16.5,15 C15.1192881,15 14,13.8807119 14,12.5 C14,11.1192881 15.1192881,10 16.5,10 C17.8807119,10 19,11.1192881 19,12.5 C19,13.8807119 17.8807119,15 16.5,15 Z M16.5,14 C17.3284271,14 18,13.3284271 18,12.5 C18,11.6715729 17.3284271,11 16.5,11 C15.6715729,11 15,11.6715729 15,12.5 C15,13.3284271 15.6715729,14 16.5,14 Z\"/>" + CLOSE };
625
+ /** Build the diagram figure node for a simple tab kind (a div wrapping the SVG). */
626
+ function diagramNode(kind) {
627
+ const figure = document.createElement("div");
628
+ figure.className = "text-base-content/70 mx-auto mb-3 h-28 w-full max-w-xs";
629
+ figure.innerHTML = DIAGRAMS[kind];
630
+ return figure;
631
+ }
632
+ const GP_LEFT_STICK = "<path d=\"M182.4,251.5c-28.9,0-52.5,23.5-52.5,52.5s23.5,52.5,52.5,52.5c28.9,0,52.5-23.5,52.5-52.5S211.3,251.5,182.4,251.5z M182.4,331.9c-15.4,0-28-12.5-28-28s12.5-28,28-28c15.4,0,28,12.5,28,28S197.8,331.9,182.4,331.9z\"/>";
633
+ const GP_RIGHT_STICK = "<path d=\"M382.1,303.9c0-28.9-23.5-52.5-52.5-52.5c-28.9,0-52.5,23.5-52.5,52.5s23.5,52.5,52.5,52.5C358.6,356.4,382.1,332.8,382.1,303.9z M329.6,331.9c-15.4,0-28-12.5-28-28s12.5-28,28-28c15.4,0,28,12.5,28,28S345.1,331.9,329.6,331.9z\"/>";
634
+ const GP_DPAD = "<path d=\"M159.7,132.6c-6.8,0-12.3,5.5-12.3,12.3V167h-22.1c-6.8,0-12.3,5.5-12.3,12.3s5.5,12.3,12.3,12.3h22.1v22.1c0,6.8,5.5,12.3,12.3,12.3s12.3-5.5,12.3-12.3v-22.1h22.1c6.8,0,12.3-5.5,12.3-12.3s-5.5-12.3-12.3-12.3H172v-22.1C172,138.1,166.5,132.6,159.7,132.6z\"/>";
635
+ const GP_BTN_TOP = `<circle cx="353.4" cy="147.8" r="16.6"/>`;
636
+ const GP_BTN_LEFT = `<circle cx="319.5" cy="181.8" r="16.6"/>`;
637
+ const GP_BTN_RIGHT = `<circle cx="387.4" cy="181.8" r="16.6"/>`;
638
+ const GP_BTN_BOTTOM = `<circle cx="353.4" cy="215.7" r="16.6"/>`;
639
+ const lit = (part) => `<g class="text-primary" fill="currentColor">${part}</g>`;
640
+ /** The gamepad icon with only the parts used by `highlight` lit in the accent. */
641
+ function gamepadSvg(highlight) {
642
+ const on = (part, when) => highlight === when ? lit(part) : part;
643
+ return "<svg viewBox=\"0 0 512 512\" width=\"100%\" height=\"100%\" fill=\"currentColor\" role=\"img\" aria-hidden=\"true\"><path d=\"M467.8,173.6c-8.4-50-49.7-87-100.6-90.1l-2.1-0.4c-0.7-0.1-1.4-0.2-2.1-0.2H151.5c-53.3,0-98.4,38.1-107.2,90.4L12,346.4c-1,4.7-1.5,9.4-1.5,14.1c0,37.8,30.7,68.5,68.5,68.5c22.8,0,44.1-11.3,56.6-29.9l15.6-21.6c4-5.5,2.7-13.1-2.8-17.1c-5.5-4-13.1-2.7-17.1,2.8l-15.8,21.9c-8.2,12.2-21.9,19.5-36.5,19.5c-24.3,0-44-19.8-44-44c0-3,0.3-6.1,1-9.4l32.3-173.5c6.8-40.7,41.8-70.2,83.1-70.2H362l1.7,0.3c0.5,0.1,0.9,0.1,1.4,0.2c39,2.1,72,31.5,78.5,69.9L476,351.4c0.6,3,0.9,6.1,0.9,9.1c0,24.3-19.8,44-44,44c-14.7,0-28.3-7.3-36.8-19.8l-16.8-23.2c-4-5.5-11.6-6.7-17.1-2.8c-5.5,4-6.7,11.6-2.8,17.1l16.5,22.9c12.8,18.9,34,30.2,56.8,30.2c37.8,0,68.5-30.7,68.5-68.5c0-4.7-0.5-9.5-1.4-13.9L467.8,173.6z\"/>" + on(GP_LEFT_STICK, "move") + on(GP_RIGHT_STICK, "look") + on(GP_DPAD, "move") + GP_BTN_TOP + GP_BTN_LEFT + GP_BTN_RIGHT + on(GP_BTN_BOTTOM, "select") + CLOSE;
644
+ }
645
+ const PHONE_ROTATE = "<svg viewBox=\"-8.04 0 36.945037 32.134997\" width=\"100%\" height=\"100%\" role=\"img\" aria-hidden=\"true\"><g fill=\"currentColor\" transform=\"translate(-614.78856,-194.333)\"><path d=\"m 630.278,196.333 a 0.855,0.855 0 0 1 0.854,0.854 v 26.427 a 0.855,0.855 0 0 1 -0.854,0.854 h -10.345 a 0.855,0.855 0 0 1 -0.854,-0.854 v -26.427 a 0.855,0.855 0 0 1 0.854,-0.854 h 10.345 m 0,-2 h -10.345 a 2.854,2.854 0 0 0 -2.854,2.854 v 26.427 a 2.854,2.854 0 0 0 2.854,2.854 h 10.345 a 2.854,2.854 0 0 0 2.854,-2.854 v -26.427 a 2.854,2.854 0 0 0 -2.854,-2.854 z\"/><path d=\"m 625.106,220.375 a 1.144,1.144 0 1 0 1.143,1.143 1.144,1.144 0 0 0 -1.143,-1.143 z\"/></g><path fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.1371\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m 21.517396,7.653044 c 0.34046,0.09237 0.678922,0.2068044 1.0138,0.3440646 4.368435,1.7905494 6.458225,6.7833844 4.667677,11.1518194 -1.013391,2.472391 -3.052499,4.214862 -5.423632,4.937107 M -0.65235214,23.84002 C -0.99282974,23.74769 -1.3312767,23.633223 -1.6661557,23.495962 -6.0346112,21.705405 -8.1243897,16.712597 -6.3338324,12.344142 -5.3655121,9.981708 -3.4606611,8.2856913 -1.2244333,7.5094329 M -2.1519003,10.593762 -0.93612324,7.6276059 -1.2244333,7.5094329 M 24.686186,25.279184 21.77524,24.086036 m 0,0 -0.05514,-0.02261 1.215708,-2.966184 M -3.9022805,6.41183 -1.2244349,7.5094331\"/></svg>";
646
+ const thumbDot = (cx, cy) => `<g class="text-primary" fill="currentColor"><circle cx="${cx}" cy="${cy}" r="3"/></g>`;
647
+ const grabPhone = (light) => "<svg viewBox=\"-8.04 0 49.591388 22.890837\" width=\"100%\" height=\"100%\" role=\"img\" aria-hidden=\"true\"><g fill=\"currentColor\" transform=\"rotate(-90,219.77734,413.35469)\"><path d=\"m 633.13203,197.18696 a 2.854,2.854 0 0 0 -2.85402,-2.85394 h -8.64185 l 1.67971,1.99997 h 6.96214 a 0.855,0.855 0 0 1 0.85398,0.85397 V 223.614 a 0.855,0.855 0 0 1 -0.85398,0.85398 h -7.1771 l -1.67979,2.00004 h 8.85689 a 2.854,2.854 0 0 0 2.85402,-2.85402 z m -14.05302,1.1933 -1.82611,-2.17421 a 2.854,2.854 0 0 0 -0.17393,0.98091 V 223.614 a 2.854,2.854 0 0 0 0.11352,0.79686 l 1.88652,-2.24615 z\"/></g><path fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.73127\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m -7.1743772,20.405591 6.27449192,1.61962 c 0,0 2.28297208,-1.746 3.01734048,-3.303819 0.6250483,-1.325918 1.2501011,-3.541343 1.2501011,-3.541343 0,0 4.9303549,-3.331849 4.990109,-4.64706 C 8.4038939,9.51548 7.4523448,8.44669 6.489279,8.115078 5.4553799,7.759076 -0.61569418,12.732235 -0.61569418,12.732235 L 1.7109732,10.62309 1.7633602,4.158494 c 0,0 -2.9599019,-0.1661 -5.3375374,2.108198 -1.407702,1.34652 -3.6002,14.138899 -3.6002,14.138899 z\"/><path fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.73127\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m 40.685765,20.076393 -6.274492,1.61962 c 0,0 -2.282972,-1.746 -3.01734,-3.303819 -0.625049,-1.325918 -1.250101,-3.541343 -1.250101,-3.541343 0,0 -4.930355,-3.331849 -4.990109,-4.64706 -0.04623,-1.017509 0.90532,-2.086299 1.868386,-2.417911 1.033899,-0.356002 7.104973,4.617157 7.104973,4.617157 l -2.326667,-2.109145 -0.05239,-6.464596 c 0,0 2.959902,-0.1661 5.337537,2.108198 1.407702,1.34652 3.6002,14.138899 3.6002,14.138899 z\"/>" + (light === "move" ? thumbDot(8, 8) : "") + (light === "look" ? thumbDot(26, 8) : "") + `</svg>`;
648
+ const PHONE = {
649
+ rotate: PHONE_ROTATE,
650
+ hold: grabPhone(),
651
+ move: grabPhone("move"),
652
+ look: grabPhone("look")
653
+ };
654
+ const MOVE_STICK = "<svg viewBox=\"0 0 100 100\" width=\"100%\" height=\"100%\" role=\"img\" aria-hidden=\"true\"><circle cx=\"50\" cy=\"50\" r=\"30\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"6\"/><g class=\"text-primary\" fill=\"currentColor\"><circle cx=\"50\" cy=\"50\" r=\"12\"/></g><g fill=\"currentColor\"><path d=\"M50 12 l-7 10 h14 z\"/><path d=\"M50 88 l-7 -10 h14 z\"/><path d=\"M12 50 l10 -7 v14 z\"/><path d=\"M88 50 l-10 -7 v14 z\"/></g>" + CLOSE;
655
+ const KEY = 152;
656
+ const KEY_HALF = KEY / 2;
657
+ const KEY_UP = [262, 82];
658
+ const KEY_LEFT = [82, 259];
659
+ const KEY_DOWN = [262, 259];
660
+ const KEY_RIGHT = [442, 259];
661
+ const keyRect = ([cx, cy]) => `<rect x="${cx - KEY_HALF}" y="${cy - KEY_HALF}" width="${KEY}" height="${KEY}" rx="24" fill="none" stroke="currentColor" stroke-width="11"/>`;
662
+ const keyLetter = ([cx, cy], ch) => `<text x="${cx}" y="${cy}" font-family="system-ui, sans-serif" font-weight="800" font-size="96" text-anchor="middle" dominant-baseline="central" fill="currentColor" stroke="none">${ch}</text>`;
663
+ const keyArrow = ([cx, cy], dir) => {
664
+ const L = 42;
665
+ const H = 20;
666
+ const line = (d) => `<path d="${d}" fill="none" stroke="currentColor" stroke-width="11" stroke-linecap="round" stroke-linejoin="round"/>`;
667
+ switch (dir) {
668
+ case "up": return line(`M${cx} ${cy + L} V${cy - L}`) + line(`M${cx - H} ${cy - L + H} L${cx} ${cy - L} L${cx + H} ${cy - L + H}`);
669
+ case "down": return line(`M${cx} ${cy - L} V${cy + L}`) + line(`M${cx - H} ${cy + L - H} L${cx} ${cy + L} L${cx + H} ${cy + L - H}`);
670
+ case "left": return line(`M${cx + L} ${cy} H${cx - L}`) + line(`M${cx - L + H} ${cy - H} L${cx - L} ${cy} L${cx - L + H} ${cy + H}`);
671
+ default: return line(`M${cx - L} ${cy} H${cx + L}`) + line(`M${cx + L - H} ${cy - H} L${cx + L} ${cy} L${cx + L - H} ${cy + H}`);
672
+ }
673
+ };
674
+ const AWSD_LAYER = keyRect(KEY_UP) + keyLetter(KEY_UP, "W") + keyRect(KEY_LEFT) + keyLetter(KEY_LEFT, "A") + keyRect(KEY_DOWN) + keyLetter(KEY_DOWN, "S") + keyRect(KEY_RIGHT) + keyLetter(KEY_RIGHT, "D");
675
+ const KEYS = `<svg viewBox="0 0 524 741" width="100%" height="100%" role="img" aria-hidden="true"><g transform="translate(0 400)">${keyRect(KEY_UP) + keyArrow(KEY_UP, "up") + keyRect(KEY_LEFT) + keyArrow(KEY_LEFT, "left") + keyRect(KEY_DOWN) + keyArrow(KEY_DOWN, "down") + keyRect(KEY_RIGHT) + keyArrow(KEY_RIGHT, "right")}</g><g>${AWSD_LAYER}</g>` + CLOSE;
676
+ const MOUSE_BODY = "M8.5 0c-3.033 0-5.5 2.467-5.5 5.5v6c0 3.033 2.467 5.5 5.5 5.5s5.5-2.467 5.5-5.5v-6c0-3.033-2.467-5.5-5.5-5.5zM13 5.5v0.5h-4v-4.949c2.244 0.252 4 2.139 4 4.449zM8 1.051v4.949h-4v-0.5c0-2.31 1.756-4.197 4-4.449zM8.5 16c-2.481 0-4.5-2.019-4.5-4.5v-4.5h9v4.5c0 2.481-2.019 4.5-4.5 4.5z";
677
+ const MOUSE_LEFT_BTN = `M8 1.051v4.949h-4v-0.5c0-2.31 1.756-4.197 4-4.449z`;
678
+ const MOUSE_RIGHT_BTN = `M13 5.5v0.5h-4v-4.949c2.244 0.252 4 2.139 4 4.449z`;
679
+ const mouse = (light) => `<svg viewBox="0 0 17 17" width="100%" height="100%" role="img" aria-hidden="true">` + (light === "left" ? `<g class="text-primary" fill="currentColor"><path d="${MOUSE_LEFT_BTN}"/></g>` : "") + (light === "right" ? `<g class="text-primary" fill="currentColor"><path d="${MOUSE_RIGHT_BTN}"/></g>` : "") + `<path fill="currentColor" fill-rule="evenodd" d="${MOUSE_BODY}"/>` + CLOSE;
680
+ function figureFrom(svg) {
681
+ const figure = document.createElement("div");
682
+ figure.className = "text-base-content/70 h-16 w-16 shrink-0";
683
+ figure.innerHTML = svg;
684
+ return figure;
685
+ }
686
+ /**
687
+ * Icon factories for a tab whose pane is split into per-control sections, in the
688
+ * order its lines document them; `null` for single-diagram tabs. Each factory
689
+ * makes a fresh node (a DOM node can only live in one place).
690
+ */
691
+ function sectionFigures(kind) {
692
+ switch (kind) {
693
+ case "touch": return [
694
+ "rotate",
695
+ "hold",
696
+ "move",
697
+ "look"
698
+ ].map((step) => () => figureFrom(PHONE[step]));
699
+ case "vr": return [() => figureFrom(PHONE.rotate), () => figureFrom(MOVE_STICK)];
700
+ case "keyboard-mouse": return [
701
+ () => figureFrom(KEYS),
702
+ () => figureFrom(mouse("left")),
703
+ () => figureFrom(mouse("right"))
704
+ ];
705
+ case "gamepad": return [
706
+ "move",
707
+ "look",
708
+ "select"
709
+ ].map((h) => () => figureFrom(gamepadSvg(h)));
710
+ default: return null;
711
+ }
712
+ }
713
+ //#endregion
714
+ //#region src/components/help/modality.ts
715
+ /** Map a modality to the help tab that documents it. */
716
+ function modalityTab(modality) {
717
+ return modality;
718
+ }
719
+ function createModalityTracker() {
720
+ let last = typeof matchMedia !== "undefined" && matchMedia("(pointer: coarse)").matches ? "touch" : "keyboard-mouse";
721
+ const onTouch = () => {
722
+ last = "touch";
723
+ };
724
+ const onPointer = (e) => {
725
+ if (e.pointerType === "touch") return;
726
+ last = "keyboard-mouse";
727
+ };
728
+ const onKey = () => {
729
+ last = "keyboard-mouse";
730
+ };
731
+ const onPad = () => {
732
+ last = "gamepad";
733
+ };
734
+ window.addEventListener("touchstart", onTouch, { passive: true });
735
+ window.addEventListener("pointerdown", onPointer, { passive: true });
736
+ window.addEventListener("keydown", onKey);
737
+ window.addEventListener("gamepadconnected", onPad);
738
+ return {
739
+ get: () => last,
740
+ destroy: () => {
741
+ window.removeEventListener("touchstart", onTouch);
742
+ window.removeEventListener("pointerdown", onPointer);
743
+ window.removeEventListener("keydown", onKey);
744
+ window.removeEventListener("gamepadconnected", onPad);
745
+ }
746
+ };
747
+ }
748
+ //#endregion
749
+ //#region src/components/help/help.ts
750
+ /**
751
+ * Build a help tab's pane body and return the text nodes to relocalize. Most tabs
752
+ * are one diagram above a bullet list. Touch and gamepad are split into sections:
753
+ * one icon per control (with just that part lit) next to its instruction, so the
754
+ * visitor sees which part does what; any extra lines (e.g. "connect a controller")
755
+ * stay as notes below.
756
+ */
757
+ function buildPane(pane, tab) {
758
+ const figures = sectionFigures(tab.kind);
759
+ if (figures) return buildSectionedPane(pane, tab.lines, figures);
760
+ pane.appendChild(diagramNode(tab.kind));
761
+ const list = document.createElement(tab.ordered ? "ol" : "ul");
762
+ list.className = `${tab.ordered ? "list-decimal" : "list-disc"} list-outside pl-5 space-y-1.5 text-sm leading-relaxed`;
763
+ const lineEls = tab.lines.map((line) => {
764
+ const el = document.createElement("li");
765
+ list.appendChild(el);
766
+ return {
767
+ el,
768
+ line
769
+ };
770
+ });
771
+ pane.appendChild(list);
772
+ return lineEls;
773
+ }
774
+ function buildSectionedPane(pane, lines, figures) {
775
+ const lineEls = [];
776
+ figures.forEach((makeFigure, i) => {
777
+ const line = lines[i];
778
+ if (!line) return;
779
+ const row = document.createElement("div");
780
+ row.className = "flex items-center gap-4 mb-3";
781
+ row.appendChild(makeFigure());
782
+ const p = document.createElement("p");
783
+ p.className = "text-sm leading-relaxed";
784
+ row.appendChild(p);
785
+ pane.appendChild(row);
786
+ lineEls.push({
787
+ el: p,
788
+ line
789
+ });
790
+ });
791
+ const notes = lines.slice(figures.length);
792
+ if (notes.length > 0) {
793
+ const list = document.createElement("ul");
794
+ list.className = "list-disc list-outside pl-5 space-y-1.5 text-sm leading-relaxed text-base-content/70";
795
+ for (const line of notes) {
796
+ const el = document.createElement("li");
797
+ list.appendChild(el);
798
+ lineEls.push({
799
+ el,
800
+ line
801
+ });
802
+ }
803
+ pane.appendChild(list);
804
+ }
805
+ return lineEls;
806
+ }
807
+ let instanceCount = 0;
808
+ /**
809
+ * A visual controls-help panel: an ⓘ button opening a centered daisyUI modal
810
+ * with one tab per input mode, each showing an SVG diagram and a few localized
811
+ * lines. On open it pre-selects the tab for the mode you're using. Diagrams are
812
+ * built in (standard schemes); the text is app-supplied `Text`, relocalized live.
813
+ */
814
+ function helpPanel(options) {
815
+ const autoSelect = options.autoSelect ?? true;
816
+ const groupName = `sg-help-tabs-${instanceCount += 1}`;
817
+ return (ctx) => {
818
+ const wrap = document.createElement("div");
819
+ const button = document.createElement("button");
820
+ button.type = "button";
821
+ button.className = "btn btn-circle btn-sm";
822
+ setIcon(button, lucideIcon("Info"));
823
+ const dialog = document.createElement("dialog");
824
+ dialog.className = "modal";
825
+ const box = document.createElement("div");
826
+ box.className = "modal-box max-w-md";
827
+ const close = document.createElement("button");
828
+ close.type = "button";
829
+ close.className = "btn btn-sm btn-circle btn-ghost absolute right-2 top-2";
830
+ close.textContent = "✕";
831
+ box.appendChild(close);
832
+ const heading = document.createElement("h3");
833
+ heading.className = "text-lg font-bold mb-4";
834
+ box.appendChild(heading);
835
+ const tablist = document.createElement("div");
836
+ tablist.setAttribute("role", "tablist");
837
+ tablist.className = "tabs tabs-border";
838
+ box.appendChild(tablist);
839
+ const relabels = [];
840
+ const tabInputs = /* @__PURE__ */ new Map();
841
+ let firstInput = null;
842
+ for (const tab of options.tabs) {
843
+ const input = document.createElement("input");
844
+ input.type = "radio";
845
+ input.name = groupName;
846
+ input.role = "tab";
847
+ input.className = "tab";
848
+ if (!firstInput) {
849
+ input.checked = true;
850
+ firstInput = input;
851
+ }
852
+ const pane = document.createElement("div");
853
+ pane.className = "tab-content pt-4";
854
+ const lineEls = buildPane(pane, tab);
855
+ tablist.append(input, pane);
856
+ tabInputs.set(tab.kind, input);
857
+ relabels.push(() => {
858
+ input.setAttribute("aria-label", (0, _scenography_overlay.resolveText)(tab.title));
859
+ for (const { el, line } of lineEls) el.textContent = (0, _scenography_overlay.resolveText)(line);
860
+ });
861
+ }
862
+ const backdrop = document.createElement("form");
863
+ backdrop.method = "dialog";
864
+ backdrop.className = "modal-backdrop";
865
+ const backdropBtn = document.createElement("button");
866
+ backdropBtn.textContent = "close";
867
+ backdrop.appendChild(backdropBtn);
868
+ dialog.append(box, backdrop);
869
+ wrap.append(button, dialog);
870
+ const tracker = createModalityTracker();
871
+ const unbind = bindLocale(ctx, () => {
872
+ const title = (0, _scenography_overlay.resolveText)(options.title);
873
+ button.setAttribute("aria-label", title);
874
+ heading.textContent = title;
875
+ close.setAttribute("aria-label", (0, _scenography_overlay.resolveText)(options.closeLabel ?? "Close"));
876
+ for (const relabel of relabels) relabel();
877
+ });
878
+ const onOpen = () => {
879
+ if (autoSelect) {
880
+ const mode = ctx.gallery.ctx.viewControls.getMode();
881
+ const kind = mode === "cardboard" ? "cardboard" : mode === "vr" ? "vr" : modalityTab(tracker.get());
882
+ const target = tabInputs.get(kind) ?? firstInput;
883
+ if (target) target.checked = true;
884
+ }
885
+ dialog.showModal();
886
+ };
887
+ const onClose = () => dialog.close();
888
+ button.addEventListener("click", onOpen);
889
+ close.addEventListener("click", onClose);
890
+ return {
891
+ element: wrap,
892
+ destroy: () => {
893
+ button.removeEventListener("click", onOpen);
894
+ close.removeEventListener("click", onClose);
895
+ tracker.destroy();
896
+ unbind();
897
+ }
898
+ };
899
+ };
900
+ }
901
+ //#endregion
466
902
  //#region src/components/artwork-info/artwork-info.ts
467
903
  const AVAILABILITY_COLOR = {
468
904
  available: "#22c55e",
@@ -481,7 +917,7 @@ function ensureStyles() {
481
917
  if (typeof document === "undefined" || document.getElementById(STYLE_ID)) return;
482
918
  const style = document.createElement("style");
483
919
  style.id = STYLE_ID;
484
- 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;}`;
920
+ 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;}.sg-artwork-panel__nav{display:flex;gap:0.75rem;margin-top:auto;padding-top:1.5rem;}.sg-artwork-panel__navbtn{flex:1;display:inline-flex;align-items:center;justify-content:center;gap:0.4rem;background:rgba(255,255,255,0.1);border:1px solid rgba(255,255,255,0.25);color:#fff;border-radius:0.5rem;padding:0.6rem 0.8rem;font-size:0.95rem;cursor:pointer;font-family:inherit;}.sg-artwork-panel__navbtn:hover{background:rgba(255,255,255,0.2);}`;
485
921
  document.head.appendChild(style);
486
922
  }
487
923
  const join = (parts, sep) => parts.filter((p) => Boolean(p)).join(sep);
@@ -489,15 +925,19 @@ const join = (parts, sep) => parts.filter((p) => Boolean(p)).join(sep);
489
925
  * The artwork info panel. The engine emits `artwork:select { id }` when you click
490
926
  * a piece's cartela (or the piece itself); this opens a left-hand panel (40%
491
927
  * width, full height, black at 85% alpha) with the full caption and asks the
492
- * camera to frame the piece in the free right 60%. Closing with ✕ re-centers the
493
- * piece full-width; if the user walks/turns away the engine emits
494
- * `artwork:released` and the panel just closes (no camera move).
928
+ * camera to frame the piece in the free right 60%. Prev/next buttons at the
929
+ * bottom cycle through the pieces (wrapping) without leaving the panel. Closing
930
+ * with ✕ re-centers the piece full-width, pulling back to `minCloseDistance` when
931
+ * the framing would otherwise be too close; if the user walks/turns away the
932
+ * engine emits `artwork:released` and the panel just closes (no camera move).
495
933
  *
496
934
  * Content is data (`artworks`), unlocalized for now. The panel is mounted on the
497
935
  * surface (escaping the overlay grid); the slot element is an inert placeholder.
498
936
  */
499
937
  function artworkInfo(options) {
500
- const byId = new Map(options.artworks.map((a) => [a.id, a]));
938
+ const list = options.artworks;
939
+ const byId = new Map(list.map((a) => [a.id, a]));
940
+ const indexOf = new Map(list.map((a, i) => [a.id, i]));
501
941
  return (ctx) => {
502
942
  ensureStyles();
503
943
  const camera = ctx.gallery.ctx.camera;
@@ -525,7 +965,23 @@ function artworkInfo(options) {
525
965
  link.target = "_blank";
526
966
  link.rel = "noopener noreferrer";
527
967
  link.textContent = `${options.linkText ?? "View artwork"} ↗`;
528
- panel.append(close, title, meta, status, desc, link);
968
+ const nav = document.createElement("div");
969
+ nav.className = "sg-artwork-panel__nav";
970
+ const prevBtn = document.createElement("button");
971
+ prevBtn.type = "button";
972
+ prevBtn.className = "sg-artwork-panel__navbtn";
973
+ const prevText = options.prevLabel ?? "Previous";
974
+ prevBtn.textContent = `‹ ${prevText}`;
975
+ prevBtn.setAttribute("aria-label", prevText);
976
+ const nextBtn = document.createElement("button");
977
+ nextBtn.type = "button";
978
+ nextBtn.className = "sg-artwork-panel__navbtn";
979
+ const nextText = options.nextLabel ?? "Next";
980
+ nextBtn.textContent = `${nextText} ›`;
981
+ nextBtn.setAttribute("aria-label", nextText);
982
+ nav.append(prevBtn, nextBtn);
983
+ if (list.length <= 1) nav.style.display = "none";
984
+ panel.append(close, title, meta, status, desc, link, nav);
529
985
  ctx.surface.appendChild(panel);
530
986
  let currentId = null;
531
987
  const render = (artwork) => {
@@ -554,18 +1010,29 @@ function artworkInfo(options) {
554
1010
  panel.classList.remove("sg-artwork-panel--open");
555
1011
  currentId = null;
556
1012
  };
557
- const onSelect = (payload) => {
558
- const id = payload?.id;
559
- const artwork = id ? byId.get(id) : void 0;
560
- if (!artwork || !id) return;
1013
+ const show = (id) => {
1014
+ const artwork = byId.get(id);
1015
+ if (!artwork) return;
561
1016
  currentId = id;
562
1017
  render(artwork);
1018
+ panel.scrollTop = 0;
563
1019
  panel.classList.add("sg-artwork-panel--open");
564
1020
  camera.focusArtwork(id, {
565
1021
  centerX: CENTER_X,
566
1022
  widthFraction: FREE_FRACTION
567
1023
  });
568
1024
  };
1025
+ const step = (delta) => {
1026
+ if (currentId === null || list.length <= 1) return;
1027
+ const i = indexOf.get(currentId);
1028
+ if (i === void 0) return;
1029
+ const next = list[(i + delta + list.length) % list.length];
1030
+ if (next) show(next.id);
1031
+ };
1032
+ const onSelect = (payload) => {
1033
+ const id = payload?.id;
1034
+ if (id) show(id);
1035
+ };
569
1036
  const onReleased = (payload) => {
570
1037
  const id = payload?.id;
571
1038
  if (id && id === currentId) hide();
@@ -573,11 +1040,15 @@ function artworkInfo(options) {
573
1040
  const onClose = () => {
574
1041
  const id = currentId;
575
1042
  hide();
576
- if (id) camera.centerArtwork(id);
1043
+ if (id) camera.centerArtwork(id, { minDistance: options.minCloseDistance ?? 0 });
577
1044
  };
1045
+ const onPrev = () => step(-1);
1046
+ const onNext = () => step(1);
578
1047
  const offSelect = ctx.gallery.on("artwork:select", onSelect);
579
1048
  const offReleased = ctx.gallery.on("artwork:released", onReleased);
580
1049
  close.addEventListener("click", onClose);
1050
+ prevBtn.addEventListener("click", onPrev);
1051
+ nextBtn.addEventListener("click", onNext);
581
1052
  const placeholder = document.createElement("div");
582
1053
  placeholder.style.display = "none";
583
1054
  return {
@@ -586,6 +1057,8 @@ function artworkInfo(options) {
586
1057
  offSelect();
587
1058
  offReleased();
588
1059
  close.removeEventListener("click", onClose);
1060
+ prevBtn.removeEventListener("click", onPrev);
1061
+ nextBtn.removeEventListener("click", onNext);
589
1062
  panel.remove();
590
1063
  }
591
1064
  };
@@ -685,39 +1158,83 @@ function settingsPanel(options) {
685
1158
  field.append(label, select);
686
1159
  addTab(lang.tab, lang.label, field);
687
1160
  }
688
- if (options.handedness) {
689
- const hand = options.handedness;
1161
+ if (options.handedness || options.controls) {
690
1162
  const pane = document.createElement("div");
691
- const legend = document.createElement("div");
692
- legend.className = "label-text mb-1 block";
693
- pane.appendChild(legend);
694
- const onChange = (e) => hand.onChange(e.target.value);
695
- const texts = [];
696
- for (const opt of hand.options) {
697
- const row = document.createElement("label");
698
- row.className = "label cursor-pointer justify-start gap-3 py-1";
699
- const input = document.createElement("input");
700
- input.type = "radio";
701
- input.name = "sg-settings-handedness";
702
- input.value = opt.value;
703
- input.className = "radio radio-sm";
704
- input.checked = opt.value === hand.value;
705
- input.addEventListener("change", onChange);
706
- cleanups.push(() => input.removeEventListener("change", onChange));
707
- const text = document.createElement("span");
708
- text.className = "label-text text-sm";
709
- row.append(input, text);
710
- pane.appendChild(row);
711
- texts.push({
712
- el: text,
713
- label: opt.label
1163
+ if (options.handedness) {
1164
+ const hand = options.handedness;
1165
+ const legend = document.createElement("div");
1166
+ legend.className = "label-text mb-1 block";
1167
+ pane.appendChild(legend);
1168
+ const onChange = (e) => hand.onChange(e.target.value);
1169
+ const texts = [];
1170
+ for (const opt of hand.options) {
1171
+ const row = document.createElement("label");
1172
+ row.className = "label cursor-pointer justify-start gap-3 py-1";
1173
+ const input = document.createElement("input");
1174
+ input.type = "radio";
1175
+ input.name = "sg-settings-handedness";
1176
+ input.value = opt.value;
1177
+ input.className = "radio radio-sm";
1178
+ input.checked = opt.value === hand.value;
1179
+ input.addEventListener("change", onChange);
1180
+ cleanups.push(() => input.removeEventListener("change", onChange));
1181
+ const text = document.createElement("span");
1182
+ text.className = "label-text text-sm";
1183
+ row.append(input, text);
1184
+ pane.appendChild(row);
1185
+ texts.push({
1186
+ el: text,
1187
+ label: opt.label
1188
+ });
1189
+ }
1190
+ relabels.push(() => {
1191
+ legend.textContent = (0, _scenography_overlay.resolveText)(hand.label);
1192
+ for (const { el, label } of texts) el.textContent = (0, _scenography_overlay.resolveText)(label);
714
1193
  });
715
1194
  }
716
- relabels.push(() => {
717
- legend.textContent = (0, _scenography_overlay.resolveText)(hand.label);
718
- for (const { el, label } of texts) el.textContent = (0, _scenography_overlay.resolveText)(label);
719
- });
720
- addTab(hand.tab, hand.label, pane);
1195
+ if (options.controls) {
1196
+ const cfg = options.controls;
1197
+ const viewControls = ctx.gallery.ctx.viewControls;
1198
+ const legend = document.createElement("div");
1199
+ legend.className = `label-text mb-1 block${options.handedness ? " mt-3" : ""}`;
1200
+ pane.appendChild(legend);
1201
+ const list = document.createElement("div");
1202
+ list.className = "flex flex-col gap-1";
1203
+ pane.appendChild(list);
1204
+ const rows = [];
1205
+ for (const item of cfg.items) {
1206
+ const row = document.createElement("label");
1207
+ row.className = "label cursor-pointer justify-start gap-3 py-1";
1208
+ const input = document.createElement("input");
1209
+ input.type = "checkbox";
1210
+ input.className = "toggle toggle-sm";
1211
+ input.checked = viewControls.get()[item.key];
1212
+ const onChange = () => viewControls.setFlag(item.key, input.checked);
1213
+ input.addEventListener("change", onChange);
1214
+ cleanups.push(() => input.removeEventListener("change", onChange));
1215
+ const text = document.createElement("span");
1216
+ text.className = "label-text text-sm";
1217
+ row.append(input, text);
1218
+ list.appendChild(row);
1219
+ rows.push({
1220
+ input,
1221
+ key: item.key,
1222
+ el: text,
1223
+ label: item.label
1224
+ });
1225
+ }
1226
+ const sync = () => {
1227
+ const flags = viewControls.get();
1228
+ for (const { input, key } of rows) input.checked = flags[key];
1229
+ };
1230
+ cleanups.push(ctx.gallery.on("controls:change", sync));
1231
+ relabels.push(() => {
1232
+ legend.textContent = (0, _scenography_overlay.resolveText)(cfg.label);
1233
+ for (const { el, label } of rows) el.textContent = (0, _scenography_overlay.resolveText)(label);
1234
+ });
1235
+ }
1236
+ const section = options.handedness ?? options.controls;
1237
+ addTab(section.tab, section.label, pane);
721
1238
  }
722
1239
  if (options.graphics) {
723
1240
  const gfx = options.graphics;
@@ -870,14 +1387,30 @@ function defaultPreset(overlay, options = {}) {
870
1387
  enter: lucideIcon("Maximize"),
871
1388
  exit: lucideIcon("Minimize")
872
1389
  } }));
1390
+ if (options.resetView) overlay.add("top-right", resetViewButton({
1391
+ icon: lucideIcon("House"),
1392
+ ...typeof options.resetView === "object" ? options.resetView : {}
1393
+ }));
1394
+ if (options.viewMode) {
1395
+ const supported = availableModes(detectControlCapabilities());
1396
+ const modes = options.viewMode.modes.filter((m) => supported.includes(m.mode));
1397
+ if (modes.length >= 2) overlay.add("top-right", viewModeButton({
1398
+ ...options.viewMode,
1399
+ modes
1400
+ }));
1401
+ }
873
1402
  if (options.settings) overlay.add("top-right", settingsPanel(options.settings));
874
1403
  if (options.environment) overlay.add("top-right", environmentPanel({
875
1404
  icon: lucideIcon("CloudSun"),
876
1405
  ...options.environment
877
1406
  }));
878
- overlay.add("top-right", cardboardButton({ icon: lucideIcon("RectangleGoggles") }));
1407
+ if (options.cardboard) overlay.add("top-right", cardboardButton({
1408
+ icon: lucideIcon("RectangleGoggles"),
1409
+ ...typeof options.cardboard === "object" ? options.cardboard : {}
1410
+ }));
879
1411
  if (options.artworks) overlay.add("middle-left", artworkInfo(options.artworks));
880
- if (options.info) overlay.add("bottom-right", infoPanel(options.info));
1412
+ if (options.help) overlay.add("bottom-right", helpPanel(options.help));
1413
+ else if (options.info) overlay.add("bottom-right", infoPanel(options.info));
881
1414
  if (options.credits) overlay.add("bottom-right", creditsPanel({
882
1415
  items: options.credits,
883
1416
  title: options.creditsTitle,
@@ -888,15 +1421,20 @@ function defaultPreset(overlay, options = {}) {
888
1421
  //#endregion
889
1422
  exports.actionButton = actionButton;
890
1423
  exports.artworkInfo = artworkInfo;
1424
+ exports.availableModes = availableModes;
891
1425
  exports.cardboardButton = cardboardButton;
892
1426
  exports.creditsPanel = creditsPanel;
893
1427
  exports.defaultPreset = defaultPreset;
1428
+ exports.detectControlCapabilities = detectControlCapabilities;
894
1429
  exports.environmentPanel = environmentPanel;
895
1430
  exports.fullscreenButton = fullscreenButton;
1431
+ exports.helpPanel = helpPanel;
896
1432
  exports.infoPanel = infoPanel;
897
1433
  exports.lucideIcon = lucideIcon;
898
1434
  exports.muteButton = muteButton;
1435
+ exports.resetViewButton = resetViewButton;
899
1436
  exports.settingsPanel = settingsPanel;
1437
+ exports.viewModeButton = viewModeButton;
900
1438
  exports.watermark = watermark;
901
1439
 
902
1440
  //# sourceMappingURL=index.cjs.map