@v-office/website-headless 0.0.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.
@@ -0,0 +1,533 @@
1
+ //#region src/enhance/carousel.ts
2
+ /**
3
+ * Per-card image carousel: drives the prev/next chevrons by calling
4
+ * `scrollTo` on the natively-scrolling image track (overflow-x: auto
5
+ * with scroll-snap on the track on touch devices, or scroll-snap
6
+ * none + JS-driven scrollTo on hover-capable devices).
7
+ *
8
+ * Native scroll matters for two reasons:
9
+ * 1. Lazy-loaded `<img loading="lazy">` only triggers load when
10
+ * the image enters the visible viewport via real scroll. With
11
+ * transform-based shifting, the browser doesn't recognize the
12
+ * image as "in view", so subsequent images stay un-loaded
13
+ * (white squares) when the user clicks next.
14
+ * 2. The scroll-snap behavior native scroll provides on touch is
15
+ * what the iOS swipe momentum/snap relies on; transform-based
16
+ * shifting can't reproduce it.
17
+ *
18
+ * Dots are optional and only updated when present.
19
+ */
20
+ function initImageCarousels(selector = ".card-images") {
21
+ document.querySelectorAll(selector).forEach((el) => {
22
+ const track = el.querySelector("[data-img-track]");
23
+ if (!track) return;
24
+ if (track.dataset.imgCarouselBound === "true") return;
25
+ track.dataset.imgCarouselBound = "true";
26
+ const dotsList = el.querySelector("[data-dots]");
27
+ const dots = dotsList ? Array.from(dotsList.querySelectorAll("li")) : [];
28
+ const prevBtn = el.querySelector("[data-img-prev]");
29
+ const nextBtn = el.querySelector("[data-img-next]");
30
+ const total = track.children.length;
31
+ if (total === 0) return;
32
+ const currentIndex = () => {
33
+ const w = track.clientWidth;
34
+ if (w === 0) return 0;
35
+ return Math.round(track.scrollLeft / w);
36
+ };
37
+ const updateButtons = (idx) => {
38
+ if (prevBtn) prevBtn.disabled = idx <= 0;
39
+ if (nextBtn) nextBtn.disabled = idx >= total - 1;
40
+ };
41
+ const updateDots = (idx) => {
42
+ if (!dotsList || !dots.length) return;
43
+ dots.forEach((d, i) => {
44
+ if (i === idx) {
45
+ d.className = "h-1.5 w-4 rounded-full transition-all duration-300 bg-[color:var(--color-white)]";
46
+ d.setAttribute("data-active", "true");
47
+ } else {
48
+ d.className = "h-1.5 w-1.5 rounded-full transition-all duration-300 bg-[color:var(--color-white)]/55";
49
+ d.removeAttribute("data-active");
50
+ }
51
+ });
52
+ dotsList.setAttribute("aria-label", `${total} Bilder, gerade angezeigt: ${idx + 1} von ${total}`);
53
+ };
54
+ const goTo = (idx) => {
55
+ const clamped = Math.max(0, Math.min(total - 1, idx));
56
+ track.scrollLeft = clamped * track.clientWidth;
57
+ updateButtons(clamped);
58
+ updateDots(clamped);
59
+ };
60
+ if (prevBtn) prevBtn.addEventListener("click", (e) => {
61
+ e.preventDefault();
62
+ e.stopPropagation();
63
+ goTo(currentIndex() - 1);
64
+ });
65
+ if (nextBtn) nextBtn.addEventListener("click", (e) => {
66
+ e.preventDefault();
67
+ e.stopPropagation();
68
+ goTo(currentIndex() + 1);
69
+ });
70
+ let engaged = false;
71
+ track.addEventListener("scroll", () => {
72
+ if (!engaged) {
73
+ engaged = true;
74
+ el.classList.add("is-engaged");
75
+ }
76
+ const idx = currentIndex();
77
+ updateButtons(idx);
78
+ updateDots(idx);
79
+ }, { passive: true });
80
+ updateButtons(0);
81
+ updateDots(0);
82
+ });
83
+ }
84
+ const DRAG_THRESHOLD_PX = 2;
85
+ function initRentalCarousel(viewport) {
86
+ if (viewport.dataset.carouselBound === "true") return;
87
+ viewport.dataset.carouselBound = "true";
88
+ const findActiveTrack = () => viewport.querySelector(".rental-track:not([hidden])");
89
+ const tabsBlock = viewport.closest("[data-rental-tabs]");
90
+ const prevBtn = tabsBlock?.querySelector("[data-rental-prev]");
91
+ const nextBtn = tabsBlock?.querySelector("[data-rental-next]");
92
+ /**
93
+ * Sync prev/next button disabled state to the current native scroll
94
+ * position. The native scroll engine reports scrollLeft on every
95
+ * scroll frame; we just compare it to the bounds.
96
+ */
97
+ const updateArrowButtons = () => {
98
+ if (!prevBtn && !nextBtn) return;
99
+ const max = viewport.scrollWidth - viewport.clientWidth;
100
+ if (prevBtn) prevBtn.disabled = viewport.scrollLeft <= 1;
101
+ if (nextBtn) nextBtn.disabled = viewport.scrollLeft >= max - 1;
102
+ };
103
+ /**
104
+ * Step the carousel forward or back by one card. Finds the card
105
+ * closest to the current scroll position, picks the neighbour in
106
+ * `direction`, and asks the browser to smoothly scroll there.
107
+ * scroll-snap CSS handles the snap; we just provide the target.
108
+ *
109
+ * Forward edge case: stepping to the last card would crop its
110
+ * right side (offsetLeft places card-left at port-start, card-
111
+ * right past port-end). When direction is forward AND we're
112
+ * landing on the last card, scroll to the maximum scrollLeft
113
+ * instead so the card is right-aligned (matches the
114
+ * `:last-child { scroll-snap-align: end }` CSS rule).
115
+ */
116
+ const stepBy = (direction) => {
117
+ const track = findActiveTrack();
118
+ if (!track) return;
119
+ const cards = Array.from(track.children);
120
+ if (cards.length === 0) return;
121
+ const cs = getComputedStyle(viewport);
122
+ const padStart = parseFloat(cs.scrollPaddingInlineStart) || 0;
123
+ let currentIdx = 0;
124
+ let bestDist = Math.abs(cards[0].offsetLeft - padStart - viewport.scrollLeft);
125
+ for (let i = 1; i < cards.length; i++) {
126
+ const snapPoint = cards[i].offsetLeft - padStart;
127
+ const d = Math.abs(snapPoint - viewport.scrollLeft);
128
+ if (d < bestDist) {
129
+ bestDist = d;
130
+ currentIdx = i;
131
+ }
132
+ }
133
+ const newIdx = Math.max(0, Math.min(cards.length - 1, currentIdx + direction));
134
+ const max = viewport.scrollWidth - viewport.clientWidth;
135
+ const cardTarget = cards[newIdx].offsetLeft - padStart;
136
+ const target = direction > 0 && newIdx === cards.length - 1 && cardTarget < max ? max : cardTarget;
137
+ viewport.scrollTo({
138
+ left: target,
139
+ behavior: "smooth"
140
+ });
141
+ };
142
+ let isDown = false;
143
+ let dragging = false;
144
+ let startX = 0;
145
+ let startScrollLeft = 0;
146
+ let pointerId = null;
147
+ let suppressNextClick = false;
148
+ let suppressTimer = null;
149
+ const armClickSuppressor = () => {
150
+ suppressNextClick = true;
151
+ if (suppressTimer !== null) window.clearTimeout(suppressTimer);
152
+ suppressTimer = window.setTimeout(() => {
153
+ suppressNextClick = false;
154
+ suppressTimer = null;
155
+ }, 350);
156
+ };
157
+ const onPointerMove = (e) => {
158
+ if (!isDown || pointerId !== e.pointerId) return;
159
+ const dx = e.clientX - startX;
160
+ if (!dragging) {
161
+ if (Math.abs(dx) < DRAG_THRESHOLD_PX) return;
162
+ dragging = true;
163
+ viewport.classList.add("is-dragging");
164
+ const sel = window.getSelection?.();
165
+ if (sel) sel.removeAllRanges();
166
+ }
167
+ e.preventDefault();
168
+ viewport.scrollLeft = startScrollLeft - dx;
169
+ };
170
+ const onPointerEnd = (e) => {
171
+ if (!isDown || pointerId !== e.pointerId) return;
172
+ if (dragging) armClickSuppressor();
173
+ document.removeEventListener("pointermove", onPointerMove);
174
+ document.removeEventListener("pointerup", onPointerEnd);
175
+ document.removeEventListener("pointercancel", onPointerEnd);
176
+ isDown = false;
177
+ dragging = false;
178
+ pointerId = null;
179
+ viewport.classList.remove("is-dragging");
180
+ };
181
+ const onPointerDown = (e) => {
182
+ if (e.pointerType !== "mouse") return;
183
+ if (e.button !== 0) return;
184
+ isDown = true;
185
+ dragging = false;
186
+ startX = e.clientX;
187
+ startScrollLeft = viewport.scrollLeft;
188
+ pointerId = e.pointerId;
189
+ document.addEventListener("pointermove", onPointerMove, { passive: false });
190
+ document.addEventListener("pointerup", onPointerEnd);
191
+ document.addEventListener("pointercancel", onPointerEnd);
192
+ };
193
+ viewport.addEventListener("pointerdown", onPointerDown);
194
+ document.addEventListener("click", (e) => {
195
+ if (!suppressNextClick) return;
196
+ const t = e.target;
197
+ if (!t || !viewport.contains(t)) return;
198
+ e.preventDefault();
199
+ e.stopPropagation();
200
+ suppressNextClick = false;
201
+ if (suppressTimer !== null) {
202
+ window.clearTimeout(suppressTimer);
203
+ suppressTimer = null;
204
+ }
205
+ }, { capture: true });
206
+ viewport.addEventListener("scroll", updateArrowButtons, { passive: true });
207
+ if (prevBtn) prevBtn.addEventListener("click", () => stepBy(-1));
208
+ if (nextBtn) nextBtn.addEventListener("click", () => stepBy(1));
209
+ viewport.addEventListener("rental-track-changed", () => {
210
+ viewport.scrollLeft = 0;
211
+ updateArrowButtons();
212
+ });
213
+ requestAnimationFrame(updateArrowButtons);
214
+ }
215
+ //#endregion
216
+ //#region src/enhance/overlay.ts
217
+ /** Run `mutate` inside a view transition where the browser has one and the
218
+ * visitor has not asked for less motion; run it directly otherwise. */
219
+ function withViewTransition(mutate) {
220
+ const doc = document;
221
+ if (typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches || typeof doc.startViewTransition !== "function") {
222
+ mutate();
223
+ return;
224
+ }
225
+ const transition = doc.startViewTransition(mutate);
226
+ transition.ready.catch(() => {});
227
+ transition.finished.catch(() => {});
228
+ }
229
+ let lastInputWasKeyboard = false;
230
+ let tracked = false;
231
+ const KEYBOARD_KEYS = /* @__PURE__ */ new Set([
232
+ "Tab",
233
+ "Enter",
234
+ " ",
235
+ "Escape",
236
+ "ArrowLeft",
237
+ "ArrowRight",
238
+ "ArrowUp",
239
+ "ArrowDown",
240
+ "Home",
241
+ "End"
242
+ ]);
243
+ /** Install the page's single pair of modality listeners. Safe to call often. */
244
+ function trackInputModality() {
245
+ if (tracked) return;
246
+ tracked = true;
247
+ window.addEventListener("pointerdown", () => {
248
+ lastInputWasKeyboard = false;
249
+ }, { capture: true });
250
+ window.addEventListener("keydown", (e) => {
251
+ if (KEYBOARD_KEYS.has(e.key)) lastInputWasKeyboard = true;
252
+ }, { capture: true });
253
+ }
254
+ /** Focus `el` only when the last real input was a keyboard one. */
255
+ function focusForKeyboard(el) {
256
+ if (lastInputWasKeyboard && el && typeof el.focus === "function") el.focus();
257
+ }
258
+ /** Bind one overlay. Returns silently when the markup is not on this page, so
259
+ * a component whose overlay self-hides costs nothing. */
260
+ function initHistoryOverlay(options) {
261
+ const root = document.querySelector(options.root);
262
+ if (!root) return;
263
+ const overlay = root.querySelector(options.overlay);
264
+ const scroller = root.querySelector(options.scroller);
265
+ const openBtn = root.querySelector(options.open);
266
+ const closeBtn = root.querySelector(options.close);
267
+ if (!overlay || !scroller || !openBtn || !closeBtn) return;
268
+ trackInputModality();
269
+ let returnFocus = null;
270
+ const openModal = () => {
271
+ returnFocus = document.activeElement;
272
+ withViewTransition(() => {
273
+ overlay.removeAttribute("hidden");
274
+ document.body.style.overflow = "hidden";
275
+ });
276
+ history.pushState({ layer: options.historyKey }, "");
277
+ requestAnimationFrame(() => focusForKeyboard(closeBtn));
278
+ };
279
+ const closeModalInternal = () => {
280
+ withViewTransition(() => {
281
+ scroller.scrollTop = 0;
282
+ overlay.setAttribute("hidden", "");
283
+ document.body.style.overflow = "";
284
+ });
285
+ focusForKeyboard(returnFocus);
286
+ };
287
+ const closeModal = () => {
288
+ if (history.state && history.state.layer === options.historyKey) history.back();
289
+ else closeModalInternal();
290
+ };
291
+ openBtn.addEventListener("click", openModal);
292
+ closeBtn.addEventListener("click", closeModal);
293
+ overlay.addEventListener("click", (e) => {
294
+ if (e.target === overlay) closeModal();
295
+ });
296
+ document.addEventListener("keydown", (e) => {
297
+ if (!overlay.hasAttribute("hidden") && e.key === "Escape") {
298
+ e.preventDefault();
299
+ closeModal();
300
+ }
301
+ });
302
+ window.addEventListener("popstate", () => {
303
+ if (!overlay.hasAttribute("hidden")) closeModalInternal();
304
+ });
305
+ }
306
+ //#endregion
307
+ //#region src/enhance/gallery.ts
308
+ /**
309
+ * The gallery's layered overlays, as an engine the component configures.
310
+ *
311
+ * WHAT IT OWNS: opening and closing a stack of layers, the browser-history
312
+ * entry each one pushes, the scroll lock, layered ESC, focus on open and
313
+ * restore on close, the lazily-loaded photo reveal, the scroll-snap viewer's
314
+ * index and its prev/next stepping, and keeping a page-level carousel on the
315
+ * photo the visitor was last looking at.
316
+ *
317
+ * WHAT IT DECIDES: nothing visual. It states **no selector of its own** — every
318
+ * one is passed in, the rule `overlay.ts` already set, so the markup and the
319
+ * string that finds it stay in the same file and a grep for
320
+ * `data-gallery-lightbox` still lands on the component that owns it. It sets no
321
+ * class, reads no colour and renders no markup.
322
+ *
323
+ * WHY THE LAYERS ARE A LIST. The controller this replaces hardcoded its three
324
+ * layers and their nesting order in three separate places — the `popstate`
325
+ * handler, the `keydown` handler and the body-scroll restore — and opened with
326
+ * `if (!lightbox || !lightboxClose || !fullscreen || !fsTrack) return`. A page
327
+ * that opens the viewer straight from the photos, with no intermediate grid,
328
+ * therefore got **no gallery at all** and no error saying so. That is not a
329
+ * hypothetical composition: it is the obvious one for a site whose photos are
330
+ * already a full-width strip. Layers are now whatever the caller lists,
331
+ * outermost first, and a layer whose markup is absent is simply not bound.
332
+ *
333
+ * WHY IT IS A MODULE AND NOT `is:inline`. It was inline from the gallery's first
334
+ * commit with no reason recorded, and the cost was stated in two places: the
335
+ * four-line copy of `withViewTransition` it had to carry, and the third copy of
336
+ * the input-modality listeners `overlay.ts` names in its own header. An
337
+ * `is:inline` script is processed by neither Vite nor Astro and can import
338
+ * nothing, so both copies were forced rather than chosen.
339
+ *
340
+ * The one thing that changes by importing: the emitted script crosses Vite's
341
+ * `assetsInlineLimit` (4096 bytes), so Astro links it as an external module
342
+ * instead of writing it into the page. Any gate that located MARKUP by
343
+ * searching the built HTML was, until it read markup only, able to match the
344
+ * selector literals in the inlined source — see the note in the blueprints'
345
+ * `fact-check.py`. That is fixed there rather than worked around here.
346
+ */
347
+ function bindLazyReveal(root) {
348
+ root.querySelectorAll("img[loading=\"lazy\"]").forEach((img) => {
349
+ if (img.complete) img.dataset["loaded"] = "cache";
350
+ });
351
+ root.classList.add("is-img-fade");
352
+ root.addEventListener("load", (e) => {
353
+ const img = e.target;
354
+ if (img instanceof HTMLImageElement && img.loading === "lazy" && !img.dataset["loaded"]) img.dataset["loaded"] = "fade";
355
+ }, true);
356
+ root.addEventListener("error", (e) => {
357
+ const img = e.target;
358
+ if (img instanceof HTMLImageElement && img.loading === "lazy") img.dataset["loaded"] = "cache";
359
+ }, true);
360
+ }
361
+ /** Bind one gallery. Returns silently when its root is not on this page. */
362
+ function initGallery(options) {
363
+ const root = document.querySelector(options.root);
364
+ if (!root) return;
365
+ trackInputModality();
366
+ bindLazyReveal(root);
367
+ const q = (sel) => sel ? root.querySelector(sel) : null;
368
+ const track = q(options.track);
369
+ const carousel = q(options.carousel);
370
+ const counter = q(options.counter);
371
+ const carouselCounter = q(options.carouselCounter);
372
+ const indexAttr = options.indexAttr ?? "data-image-index";
373
+ const layers = [];
374
+ for (const spec of options.layers) {
375
+ const panel = q(spec.panel);
376
+ if (!panel) continue;
377
+ layers.push({
378
+ name: spec.name,
379
+ panel,
380
+ scroller: q(spec.scroller),
381
+ backdrop: q(spec.backdrop),
382
+ closeBtn: q(spec.close),
383
+ openers: Array.from(root.querySelectorAll(spec.open)),
384
+ isViewer: spec.opensAt === "index",
385
+ warmOnIntent: spec.warmOnIntent === true,
386
+ returnFocus: null
387
+ });
388
+ if (spec.fitImage) {
389
+ const img = panel.querySelector(spec.fitImage);
390
+ if (img) {
391
+ const applyRatio = () => {
392
+ if (!img.naturalWidth || !img.naturalHeight) return;
393
+ img.style.setProperty("--fp-ar", `${img.naturalWidth}/${img.naturalHeight}`);
394
+ };
395
+ if (img.complete) applyRatio();
396
+ img.addEventListener("load", applyRatio, { once: true });
397
+ }
398
+ }
399
+ }
400
+ if (!layers.length) return;
401
+ const total = layers.find((l) => l.isViewer)?.openers.length || root.querySelectorAll(`[${indexAttr}]`).length;
402
+ const isOpen = (l) => !l.panel.hasAttribute("hidden");
403
+ /** Innermost open layer — the one a back gesture or ESC should close. */
404
+ const innermostOpen = () => {
405
+ for (let i = layers.length - 1; i >= 0; i -= 1) {
406
+ const l = layers[i];
407
+ if (l && isOpen(l)) return l;
408
+ }
409
+ return null;
410
+ };
411
+ const currentIdx = () => {
412
+ if (!track) return 0;
413
+ const w = track.clientWidth;
414
+ return w === 0 ? 0 : Math.round(track.scrollLeft / w);
415
+ };
416
+ const setCounter = (el, idx) => {
417
+ if (el) el.textContent = `${idx + 1} / ${total}`;
418
+ };
419
+ const openLayer = (layer, idx) => {
420
+ layer.returnFocus = document.activeElement;
421
+ withViewTransition(() => {
422
+ layer.panel.removeAttribute("hidden");
423
+ document.body.style.overflow = "hidden";
424
+ });
425
+ history.pushState({ layer: `${options.root}:${layer.name}` }, "");
426
+ const landOn = (attempt) => {
427
+ if (!(layer.isViewer && track && typeof idx === "number")) {
428
+ focusForKeyboard(layer.closeBtn);
429
+ return;
430
+ }
431
+ const w = track.clientWidth;
432
+ if (w === 0 && attempt < 5) {
433
+ requestAnimationFrame(() => landOn(attempt + 1));
434
+ return;
435
+ }
436
+ track.scrollTo({
437
+ left: idx * w,
438
+ behavior: "auto"
439
+ });
440
+ setCounter(counter, idx);
441
+ focusForKeyboard(layer.closeBtn);
442
+ };
443
+ requestAnimationFrame(() => landOn(0));
444
+ };
445
+ const closeLayerInternal = (layer) => {
446
+ const lastIdx = layer.isViewer ? currentIdx() : null;
447
+ withViewTransition(() => {
448
+ if (layer.scroller) layer.scroller.scrollTop = 0;
449
+ layer.panel.setAttribute("hidden", "");
450
+ if (!layers.some((l) => l !== layer && isOpen(l))) document.body.style.overflow = "";
451
+ });
452
+ if (lastIdx !== null && carousel && carousel.clientWidth > 0) {
453
+ carousel.scrollTo({
454
+ left: lastIdx * carousel.clientWidth,
455
+ behavior: "auto"
456
+ });
457
+ setCounter(carouselCounter, lastIdx);
458
+ }
459
+ focusForKeyboard(layer.returnFocus);
460
+ };
461
+ const closeLayer = (layer) => {
462
+ if (history.state && history.state.layer === `${options.root}:${layer.name}`) history.back();
463
+ else closeLayerInternal(layer);
464
+ };
465
+ const step = (delta) => {
466
+ if (!track) return;
467
+ const next = Math.min(total - 1, Math.max(0, currentIdx() + delta));
468
+ track.scrollTo({
469
+ left: next * track.clientWidth,
470
+ behavior: "smooth"
471
+ });
472
+ };
473
+ layers.forEach((layer) => {
474
+ let warmed = false;
475
+ const warm = () => {
476
+ if (warmed) return;
477
+ warmed = true;
478
+ const img = layer.panel.querySelector("img");
479
+ if (!img) return;
480
+ const pre = new Image();
481
+ if (img.srcset) pre.srcset = img.srcset;
482
+ if (img.sizes) pre.sizes = img.sizes;
483
+ pre.src = img.src;
484
+ };
485
+ layer.openers.forEach((t) => {
486
+ if (layer.warmOnIntent) {
487
+ t.addEventListener("pointerenter", warm, { once: true });
488
+ t.addEventListener("focus", warm, { once: true });
489
+ }
490
+ t.addEventListener("click", () => {
491
+ const idx = layer.isViewer ? Number(t.getAttribute(indexAttr)) || 0 : void 0;
492
+ openLayer(layer, idx);
493
+ });
494
+ });
495
+ if (layer.closeBtn) layer.closeBtn.addEventListener("click", () => closeLayer(layer));
496
+ if (layer.backdrop) layer.backdrop.addEventListener("click", (e) => {
497
+ if (e.target === layer.backdrop) closeLayer(layer);
498
+ });
499
+ });
500
+ const prevBtn = q(options.trackPrev);
501
+ const nextBtn = q(options.trackNext);
502
+ if (prevBtn) prevBtn.addEventListener("click", () => step(-1));
503
+ if (nextBtn) nextBtn.addEventListener("click", () => step(1));
504
+ if (track && counter) track.addEventListener("scroll", () => setCounter(counter, currentIdx()), { passive: true });
505
+ if (carousel && carouselCounter) {
506
+ const sync = () => {
507
+ const w = carousel.clientWidth;
508
+ if (w === 0) return;
509
+ setCounter(carouselCounter, Math.round(carousel.scrollLeft / w));
510
+ };
511
+ carousel.addEventListener("scroll", sync, { passive: true });
512
+ sync();
513
+ }
514
+ window.addEventListener("popstate", () => {
515
+ const layer = innermostOpen();
516
+ if (layer) closeLayerInternal(layer);
517
+ });
518
+ document.addEventListener("keydown", (e) => {
519
+ const layer = innermostOpen();
520
+ if (!layer) return;
521
+ if (e.key === "Escape") {
522
+ e.preventDefault();
523
+ closeLayer(layer);
524
+ return;
525
+ }
526
+ if (layer.isViewer && (e.key === "ArrowRight" || e.key === "ArrowLeft")) {
527
+ e.preventDefault();
528
+ step(e.key === "ArrowRight" ? 1 : -1);
529
+ }
530
+ });
531
+ }
532
+ //#endregion
533
+ export { focusForKeyboard, initGallery, initHistoryOverlay, initImageCarousels, initRentalCarousel, trackInputModality, withViewTransition };