@michaelyagi/shoji 0.1.0-beta.12 → 0.1.0-beta.13
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/core/shoji-core.js.map +1 -1
- package/dist/core/shoji-core.min.js.map +1 -1
- package/dist/esm/core/index.js.map +1 -1
- package/dist/esm/core/types.d.ts +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/index3.css +1 -1
- package/dist/esm/plugins/autoplay/index.d.ts +40 -8
- package/dist/esm/plugins/autoplay/index.js +93 -21
- package/dist/esm/plugins/autoplay/index.js.map +1 -1
- package/dist/plugins/autoplay.css +1 -1
- package/dist/plugins/autoplay.js +93 -21
- package/dist/plugins/autoplay.js.map +1 -1
- package/dist/plugins/autoplay.min.js +1 -1
- package/dist/plugins/autoplay.min.js.map +1 -1
- package/dist/shoji.css +1 -1
- package/dist/shoji.js +94 -22
- package/dist/shoji.js.map +1 -1
- package/dist/shoji.min.js +1 -1
- package/dist/shoji.min.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shoji-core.js","sources":["../../src/core/bodyScrollLock.ts","../../src/core/EventBus.ts","../../src/core/icons.ts","../../src/core/dom.ts","../../src/core/FocusTrap.ts","../../src/gestures/GestureEngine.ts","../../src/core/zoomTransition.ts","../../src/core/GestureController.ts","../../src/core/LiveRegion.ts","../../src/core/scan.ts","../../src/core/SlideManager.ts","../../src/transitions/presets.ts","../../src/transitions/SlideTransition.ts","../../src/core/Gallery.ts"],"sourcesContent":["/** DESIGN.md §2.6a — reference-counted page scroll lock. */\nlet lockCount = 0;\nlet savedHtmlOverflow = '';\nlet savedHtmlPaddingRight = '';\nlet savedHtmlPaddingLeft = '';\nlet savedScrollX = 0;\nlet savedScrollY = 0;\nlet styleObserver: MutationObserver | null = null;\n// Set by markIntentionalScroll() (ActiveThumbnail's own scrollIntoView while\n// locked, DESIGN.md §2.6a/§4.2) — a real bug/regression: unlockBodyScroll()\n// unconditionally restoring savedScrollX/Y (below) was meant to undo\n// *unrelated* code hijacking scroll during the lock (a router, a stray\n// \"scroll to top\" button), but it just as unconditionally undid Shoji's own\n// legitimate background-page scrolling too, making ActiveThumbnail's\n// scrollIntoView a complete no-op the instant the lightbox that triggers it\n// is also what's holding the lock — every navigation while open, always.\n// Once anything marks an intentional scroll during this lock session, skip\n// the restore entirely rather than try to track a smooth-scroll animation's\n// eventual resting position (no reliable completion signal across browsers)\n// — same \"leave it where legitimate code put it\" reasoning, just without\n// pinpointing the exact pixel.\nlet intentionalScrollOccurred = false;\n\nconst LIGHTBOX_SELECTOR = '.shoji-outer';\n\n/** Call after intentionally scrolling something on the host page while the lock is active (e.g. ActiveThumbnail's own `scrollIntoView`) — tells `unlockBodyScroll()` not to undo it. */\nexport function markIntentionalScroll(): void {\n intentionalScrollOccurred = true;\n}\n\nfunction isRtl(): boolean {\n return getComputedStyle(document.documentElement).direction === 'rtl';\n}\n\n/**\n * Walks up from `node` looking for a real scrollable container — lets\n * `onTouchMove` below allow touch-scrolling through to something like a\n * host-app modal/sidebar opened on top of the lightbox, instead of\n * blocking it just because it's outside `.shoji-outer`. Stops at\n * `document.documentElement` deliberately — that's the exact background\n * scroll this lock exists to block, not a container to exempt from it.\n */\nfunction hasScrollableAncestor(node: Node | null): boolean {\n let el = node instanceof Element ? node : (node?.parentElement ?? null);\n while (el && el !== document.documentElement) {\n const style = getComputedStyle(el);\n if (\n (style.overflowY === 'auto' || style.overflowY === 'scroll') &&\n el.scrollHeight > el.clientHeight\n ) {\n return true;\n }\n el = el.parentElement;\n }\n return false;\n}\n\n// Computed once per gesture, in onTouchStart below, not recomputed on every\n// onTouchMove — a touch's `event.target` stays fixed to the original\n// touchstart target for the whole gesture regardless of where the finger\n// moves (Touch Events spec), so re-walking the ancestor chain and calling\n// getComputedStyle again on every move would just repeat the same, not-free\n// answer for no benefit (CLAUDE.md: no forced synchronous layout in hot\n// paths like gestures/scroll).\nlet touchAllowsScrollThrough = false;\n\nfunction onTouchStart(event: TouchEvent): void {\n const target = event.target;\n const insideLightbox = target instanceof Element && target.closest(LIGHTBOX_SELECTOR) !== null;\n touchAllowsScrollThrough = !insideLightbox && hasScrollableAncestor(target as Node | null);\n}\n\n/**\n * A real gap: `overflow: hidden` on `<html>` doesn't reliably block iOS\n * Safari's own touch-driven rubber-band/bounce scroll in every case — a\n * well-known limitation of this technique across the ecosystem. The common\n * workaround (flipping `body` to `position: fixed` while locked) has its\n * own real tradeoffs: a full reflow, plus manual scroll-position capture/\n * restore that's a frequent source of bugs in other libraries (the exact\n * class of bug this whole lock has already had twice — see the other real\n * bugs in this file's history). Lighter alternative: block the browser's\n * default only for a touchmove that didn't start inside a lightbox,\n * leaving Shoji's own in-dialog gesture handling (which already manages\n * its own `preventDefault`, see `GestureEngine.ts`) completely untouched.\n * Not a passive listener — `preventDefault` is genuinely required here to\n * suppress the browser's native scroll, not just observe it.\n *\n * Also untouched: a touch that started on a genuinely scrollable ancestor\n * outside the lightbox (`touchAllowsScrollThrough`, set in `onTouchStart`\n * above) — a real bug, reported from real usage: this used to block\n * touch-scrolling in *any* host-app UI outside `.shoji-outer`, including\n * something like a Bootstrap modal or sidebar opened on top of the\n * lightbox, not just the page body behind it.\n */\nfunction onTouchMove(event: TouchEvent): void {\n const target = event.target;\n const insideLightbox = target instanceof Element && target.closest(LIGHTBOX_SELECTOR) !== null;\n if (!insideLightbox && !touchAllowsScrollThrough) event.preventDefault();\n}\n\n/**\n * A real gap: this lock is reference-counted against other `Gallery`\n * instances, but has no way to know about unrelated code on the host page\n * — another modal/dropdown library that also sets\n * `document.documentElement.style.overflow`, then clears it back to `''`\n * on its own close, would silently undo this lock mid-lightbox with\n * neither library aware of the other. Watching the one attribute this\n * lock itself owns is the only way to detect and correct that reactively.\n * `takeRecords()` discards the mutation record our own corrective write\n * below just queued — otherwise the observer would fire again for a\n * change it caused itself, forever.\n */\nfunction onStyleMutation(): void {\n if (lockCount === 0) return;\n const html = document.documentElement;\n if (getComputedStyle(html).overflow !== 'hidden') {\n html.style.overflow = 'hidden';\n styleObserver?.takeRecords();\n }\n}\n\nexport function lockBodyScroll(): void {\n if (lockCount === 0) {\n // A real gap: `overflow: hidden` blocks wheel/touch/keyboard-driven\n // scrolling, but not a programmatic `window.scrollTo()`/`scrollTop`\n // write — confirmed directly. If the host's own code scrolls the\n // window while the lightbox is open (a router restoring scroll\n // position, an unrelated \"scroll to top\" button), the page ends up\n // somewhere different than where the viewer left it once the lock\n // releases. Restored unconditionally on unlock below — a scroll\n // *lock* implies the background stays frozen in place for the whole\n // time the lightbox is open, not just protected from direct input.\n savedScrollX = window.scrollX;\n savedScrollY = window.scrollY;\n intentionalScrollOccurred = false;\n\n // A real bug: hiding overflow below reclaims the scrollbar's own\n // gutter, widening <html>'s content box by however many px the\n // scrollbar was — on a host page whose content spans the full viewport\n // width, that reflow is visible as a shift right when the lightbox\n // opens/closes (and reverses on close). Measured *before* overflow is\n // hidden (clientWidth only grows once the gutter is actually reclaimed,\n // so measuring after would always read 0) and only compensated for when\n // a real scrollbar was actually there — a page that never had one gets\n // no padding at all, nothing to compensate for.\n //\n // Two rejected approaches, both real bugs of their own: padding-right\n // on `document.body` compensates the wrong element — a page whose body\n // is narrower than the viewport (a centered, `max-width`-capped layout,\n // this docs site included) never touches the scrollbar's gutter in the\n // first place, so padding body just shrinks its own content box by an\n // *extra* scrollbar-width's worth, a new, self-inflicted reflow.\n // `scrollbar-gutter: stable` compensates the right element (`<html>`,\n // the same one the width is measured on) but paints its own visible,\n // if non-interactive, scrollbar-track styling for as long as the lock\n // is active — trading a brief shift for a permanent scrollbar-shaped\n // strip sitting over the page the whole time the lightbox is open,\n // which reads as more wrong, not less. Padding is genuinely invisible\n // — the same blank space a wider margin would be — and, applied to\n // `<html>` (not `body`), compensates the actual element the scrollbar's\n // gutter belongs to either way.\n const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;\n const rtl = isRtl();\n savedHtmlPaddingRight = document.documentElement.style.paddingRight;\n savedHtmlPaddingLeft = document.documentElement.style.paddingLeft;\n if (scrollbarWidth > 0) {\n // A real gap: a classic scrollbar renders on the *left* on a\n // `direction: rtl` page, not the right — compensating the wrong\n // side would reintroduce the exact reflow this exists to prevent,\n // just mirrored. Checked at lock time, not baked in as a fixed\n // assumption.\n if (rtl) {\n const currentPaddingLeft =\n parseFloat(getComputedStyle(document.documentElement).paddingLeft) || 0;\n document.documentElement.style.paddingLeft = `${currentPaddingLeft + scrollbarWidth}px`;\n } else {\n const currentPaddingRight =\n parseFloat(getComputedStyle(document.documentElement).paddingRight) || 0;\n document.documentElement.style.paddingRight = `${currentPaddingRight + scrollbarWidth}px`;\n }\n }\n\n // A real bug: `document.body.style.overflow = 'hidden'` (this function's\n // original, and until now only, scroll-blocking mechanism) was still\n // set here alongside <html>'s own below — redundant, and not harmless:\n // any value of `overflow` other than `visible` makes an element\n // establish a new block-formatting context, which blocks top-margin\n // collapsing between it and its first child. Confirmed directly via\n // real-browser instrumentation: with body's overflow locked, an h1\n // immediately inside it rendered `bodyMarginTop + h1's own default\n // margin-top` below the viewport top (its margin no longer collapsing\n // into body's own); the instant `unlockBodyScroll()` restored body's\n // overflow, collapsing resumed and the h1 snapped back up by its own\n // margin's worth — reading as the page shifting upward right as the\n // lightbox closes, on *any* page where the first child's margin would\n // otherwise collapse with body's (essentially any page without padding/\n // border on body itself — not a corner case). <html>'s own overflow:\n // hidden below is sufficient on its own to block user-driven scrolling\n // (wheel/touch/keyboard) — confirmed directly — since it's the real\n // scrolling element in standards mode; body's lock was never adding\n // independent protection, only this side effect.\n //\n // <html>'s own overflow, not just body's — mobile viewport-widening\n // bug, see DESIGN.md §2.6a. Both axes, not just overflow-x: setting\n // only one non-'visible' axis forces the browser to silently promote\n // the other from 'visible' to 'auto' (mixing hidden+visible isn't\n // allowed per spec), which revealed a real scrollbar that wasn't\n // there before.\n savedHtmlOverflow = document.documentElement.style.overflow;\n document.documentElement.style.overflow = 'hidden';\n\n // passive: true — onTouchStart only ever reads/caches, never calls\n // preventDefault(), unlike onTouchMove below.\n document.addEventListener('touchstart', onTouchStart, { passive: true });\n document.addEventListener('touchmove', onTouchMove, { passive: false });\n\n // Starts observing only after the writes above have already landed, so\n // it never reacts to its own initial setup — only to a later, external\n // change.\n styleObserver = new MutationObserver(onStyleMutation);\n styleObserver.observe(document.documentElement, {\n attributes: true,\n attributeFilter: ['style'],\n });\n }\n lockCount++;\n}\n\nexport function unlockBodyScroll(): void {\n lockCount = Math.max(0, lockCount - 1);\n if (lockCount === 0) {\n styleObserver?.disconnect();\n styleObserver = null;\n document.removeEventListener('touchstart', onTouchStart);\n document.removeEventListener('touchmove', onTouchMove);\n\n document.documentElement.style.overflow = savedHtmlOverflow;\n document.documentElement.style.paddingRight = savedHtmlPaddingRight;\n document.documentElement.style.paddingLeft = savedHtmlPaddingLeft;\n\n if (!intentionalScrollOccurred) {\n window.scrollTo({ left: savedScrollX, top: savedScrollY, behavior: 'instant' });\n }\n }\n}\n","export type EventMap = Record<string, unknown>;\n\nexport type Unsubscribe = () => void;\n\ntype Listener<T> = (detail: T) => void;\n\n/**\n * Typed pub/sub used by Gallery and every plugin (`ctx.on`). Plugin\n * subscriptions are unsubscribed automatically on destroy by the caller\n * that owns the PluginContext, not by this class.\n */\nexport class EventBus<Events extends EventMap> {\n private listeners = new Map<keyof Events, Set<Listener<unknown>>>();\n\n on<K extends keyof Events>(event: K, fn: Listener<Events[K]>): Unsubscribe {\n let set = this.listeners.get(event);\n if (!set) {\n set = new Set();\n this.listeners.set(event, set);\n }\n set.add(fn as Listener<unknown>);\n return () => this.off(event, fn);\n }\n\n off<K extends keyof Events>(event: K, fn: Listener<Events[K]>): void {\n this.listeners.get(event)?.delete(fn as Listener<unknown>);\n }\n\n emit<K extends keyof Events>(event: K, detail: Events[K]): void {\n const set = this.listeners.get(event);\n if (!set) return;\n for (const fn of set) (fn as Listener<Events[K]>)(detail);\n }\n\n clear(): void {\n this.listeners.clear();\n }\n}\n","/** DESIGN.md §9 — inline SVG, stroke = currentColor, overridable per-button. */\n\nexport const CLOSE_ICON =\n '<svg viewBox=\"0 0 24 24\" width=\"24\" height=\"24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><path d=\"M6 6l12 12M18 6L6 18\"/></svg>';\n\nexport const PREV_ICON =\n '<svg viewBox=\"0 0 24 24\" width=\"24\" height=\"24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M15 6l-6 6 6 6\"/></svg>';\n\nexport const NEXT_ICON =\n '<svg viewBox=\"0 0 24 24\" width=\"24\" height=\"24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M9 6l6 6-6 6\"/></svg>';\n\nexport const CAPTION_ICON =\n '<svg viewBox=\"0 0 24 24\" width=\"24\" height=\"24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"5\" width=\"18\" height=\"14\" rx=\"2\"/><path d=\"M7 10h10M7 14h6\"/></svg>';\n\nexport const CARET_ICON =\n '<svg viewBox=\"0 0 24 24\" width=\"20\" height=\"20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M6 9l6 6 6-6\"/></svg>';\n","import { CAPTION_ICON, CARET_ICON, CLOSE_ICON, NEXT_ICON, PREV_ICON } from './icons';\n\nlet idCounter = 0;\n\nexport interface LightboxLabels {\n close: string;\n previous: string;\n next: string;\n showCaption: string;\n hideCaption: string;\n fullCaption: string; // DESIGN.md §2.3a — aria-label for the caption modal's own dialog role\n moreControls: string; // DESIGN.md §3.1a — the toolbar overflow popover's own toggle/dialog label\n}\n\nexport interface LightboxDom {\n outer: HTMLElement;\n backdrop: HTMLElement;\n dialog: HTMLElement;\n counter: HTMLElement;\n caption: HTMLElement;\n closeButton: HTMLButtonElement;\n prevButton: HTMLButtonElement;\n nextButton: HTMLButtonElement;\n captionToggleButton: HTMLButtonElement; // DESIGN.md §2.3a\n /** DESIGN.md §2.3a — a truncated caption's click/Enter/Space target opens this, showing the full text scrollable, so a long caption never has to cover the nav arrows to be readable. Hidden (`[hidden]`) whenever not open. */\n captionModal: HTMLElement;\n captionModalPanel: HTMLElement;\n captionModalContent: HTMLElement;\n captionModalCloseButton: HTMLButtonElement;\n /** The whole toolbar bar — DESIGN.md §2.3a's toolbar-height measurement observes this, not any one slot, since it's what actually wraps to multiple rows. */\n toolbar: HTMLElement;\n /** DESIGN.md §3 — `ctx.ui.toolbar(slot, ...)` inserts into these; close lives in `right`. */\n toolbarLeft: HTMLElement;\n toolbarCenter: HTMLElement;\n toolbarRight: HTMLElement;\n /** DESIGN.md §3.1a — hidden unless the toolbar actually overflows; toggles `toolbarOverflowPanel`. Lives in `toolbarRight`, just before `closeButton`. */\n toolbarOverflowButton: HTMLButtonElement;\n /** DESIGN.md §3.1a — the floating popover collapsed plugin buttons relocate into once they don't fit. */\n toolbarOverflowPanel: HTMLElement;\n}\n\nfunction iconButton(className: string, icon: string, label: string): HTMLButtonElement {\n const button = document.createElement('button');\n button.type = 'button';\n button.className = className;\n button.ariaLabel = button.title = label;\n button.innerHTML = icon;\n return button;\n}\n\n/**\n * DESIGN.md §2.6 — `.shoji-outer` is the exact element `ctx.ui.outer()` (§3)\n * will return to plugins once the real plugin loader exists. Close/prev/next\n * are core-owned baseline controls, not routed through any plugin API.\n */\nexport function buildLightboxDom(slides: HTMLElement, labels: LightboxLabels): LightboxDom {\n const labelId = `shoji-counter-${++idCounter}`;\n\n const outer = document.createElement('div');\n outer.className = 'shoji-outer';\n\n const backdrop = document.createElement('div');\n backdrop.className = 'shoji-backdrop';\n outer.appendChild(backdrop);\n\n const dialog = document.createElement('div');\n dialog.className = 'shoji-dialog';\n dialog.setAttribute('role', 'dialog');\n dialog.setAttribute('aria-modal', 'true');\n dialog.setAttribute('aria-labelledby', labelId);\n dialog.tabIndex = -1;\n\n const toolbar = document.createElement('div');\n toolbar.className = 'shoji-toolbar';\n const toolbarLeft = document.createElement('div');\n toolbarLeft.className = 'shoji-toolbar-slot shoji-toolbar-left';\n const toolbarCenter = document.createElement('div');\n toolbarCenter.className = 'shoji-toolbar-slot shoji-toolbar-center';\n const toolbarRight = document.createElement('div');\n toolbarRight.className = 'shoji-toolbar-slot shoji-toolbar-right';\n const closeButton = iconButton('shoji-close', CLOSE_ICON, labels.close);\n // Ahead of close so plugin buttons (always inserted right before close) land between the two.\n const captionToggleButton = iconButton(\n 'shoji-toolbar-button shoji-caption-toggle',\n CAPTION_ICON,\n labels.showCaption,\n );\n captionToggleButton.setAttribute('aria-pressed', 'false');\n captionToggleButton.hidden = true;\n // DESIGN.md §3.1a — requested directly: close stays the absolute\n // rightmost control, with the (usually hidden) overflow caret just to its\n // left — `pluginToolbar()` inserts new buttons immediately before the\n // caret now, not before close, so they still land to the left of both\n // fixed controls.\n const toolbarOverflowButton = iconButton(\n 'shoji-toolbar-button shoji-toolbar-overflow',\n CARET_ICON,\n labels.moreControls,\n );\n toolbarOverflowButton.setAttribute('aria-haspopup', 'true');\n toolbarOverflowButton.setAttribute('aria-expanded', 'false');\n toolbarOverflowButton.hidden = true;\n toolbarRight.append(captionToggleButton, toolbarOverflowButton, closeButton);\n\n const toolbarOverflowPanel = document.createElement('div');\n toolbarOverflowPanel.className = 'shoji-toolbar-overflow-panel';\n toolbarOverflowPanel.setAttribute('role', 'dialog');\n toolbarOverflowPanel.setAttribute('aria-label', labels.moreControls);\n toolbarOverflowPanel.tabIndex = -1;\n toolbarOverflowPanel.hidden = true;\n\n // The counter lives in the toolbar's own left slot (flex flow, not\n // independent absolute positioning) specifically so a plugin button\n // sharing that slot (e.g. autoplay's play/pause, ctx.ui.toolbar('left', ...))\n // sits next to it instead of the two overlapping and fighting over clicks.\n const counter = document.createElement('div');\n counter.className = 'shoji-counter';\n counter.id = labelId;\n toolbarLeft.appendChild(counter);\n\n toolbar.append(toolbarLeft, toolbarCenter, toolbarRight);\n dialog.appendChild(toolbar);\n // Appended to `dialog` directly, not nested in `.shoji-toolbar` — the\n // toolbar's own `pointer-events: none` base rule (re-enabled per-slot,\n // not per-descendant) would otherwise need remembering to re-enable here\n // too; sidestepped entirely by living outside it, same placement pattern\n // `.shoji-caption-modal` already uses below.\n dialog.appendChild(toolbarOverflowPanel);\n\n const prevButton = iconButton('shoji-nav shoji-nav-prev', PREV_ICON, labels.previous);\n const nextButton = iconButton('shoji-nav shoji-nav-next', NEXT_ICON, labels.next);\n dialog.appendChild(prevButton);\n dialog.appendChild(nextButton);\n\n dialog.appendChild(slides);\n\n const caption = document.createElement('div');\n caption.className = 'shoji-caption';\n dialog.appendChild(caption);\n\n const captionModal = document.createElement('div');\n captionModal.className = 'shoji-caption-modal';\n captionModal.hidden = true;\n const captionModalPanel = document.createElement('div');\n captionModalPanel.className = 'shoji-caption-modal-panel';\n captionModalPanel.setAttribute('role', 'dialog');\n captionModalPanel.setAttribute('aria-modal', 'true');\n captionModalPanel.setAttribute('aria-label', labels.fullCaption);\n captionModalPanel.tabIndex = -1;\n const captionModalCloseButton = iconButton('shoji-caption-modal-close', CLOSE_ICON, labels.close);\n const captionModalContent = document.createElement('div');\n captionModalContent.className = 'shoji-caption-modal-content';\n captionModalPanel.append(captionModalCloseButton, captionModalContent);\n captionModal.appendChild(captionModalPanel);\n dialog.appendChild(captionModal);\n\n outer.appendChild(dialog);\n\n return {\n outer,\n backdrop,\n dialog,\n counter,\n caption,\n captionModal,\n captionModalPanel,\n captionModalContent,\n captionModalCloseButton,\n closeButton,\n prevButton,\n nextButton,\n captionToggleButton,\n toolbar,\n toolbarLeft,\n toolbarCenter,\n toolbarRight,\n toolbarOverflowButton,\n toolbarOverflowPanel,\n };\n}\n","const FOCUSABLE_SELECTOR =\n 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), video[controls], [tabindex]:not([tabindex=\"-1\"])';\n\n/** DESIGN.md §2.6 — focus trap on open, focus restore on close, Tab cycles within the dialog. */\nexport class FocusTrap {\n private container: HTMLElement | null = null;\n private previouslyFocused: HTMLElement | null = null;\n\n private readonly onKeydown = (event: KeyboardEvent): void => {\n if (event.key !== 'Tab' || !this.container) return;\n\n const focusable = this.getFocusable();\n if (focusable.length === 0) {\n event.preventDefault();\n this.container.focus();\n return;\n }\n\n const first = focusable[0]!;\n const last = focusable[focusable.length - 1]!;\n const active = document.activeElement;\n const activeIsInside = active instanceof HTMLElement && focusable.includes(active);\n\n if (event.shiftKey) {\n if (!activeIsInside || active === first) {\n event.preventDefault();\n last.focus();\n }\n } else if (!activeIsInside || active === last) {\n event.preventDefault();\n first.focus();\n }\n };\n\n private getFocusable(): HTMLElement[] {\n if (!this.container) return [];\n return Array.from(this.container.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR));\n }\n\n activate(container: HTMLElement): void {\n this.previouslyFocused = document.activeElement as HTMLElement | null;\n this.container = container;\n document.addEventListener('keydown', this.onKeydown, true);\n container.focus();\n }\n\n /**\n * DESIGN.md §2.3a — narrows (or widens back) which subtree Tab cycles\n * within, without touching `previouslyFocused`/the listener registration\n * — a caption modal nested inside the already-trapped dialog needs Tab\n * confined to just itself while open, then back to the whole dialog on\n * close, but must NOT trigger `activate()`'s own focus-capture (that's\n * reserved for the real open/close boundary; re-running it mid-session\n * would capture the wrong \"previously focused\" element and restore focus\n * to it too early). `getFocusable()` reads `this.container` fresh on\n * every keydown, so simply reassigning it is enough — no need for a\n * second, independent `FocusTrap` instance, which would double-handle\n * every Tab keypress (two capture-phase `document` listeners, each\n * computing its own overlapping focusable list).\n */\n retarget(container: HTMLElement): void {\n this.container = container;\n container.focus();\n }\n\n deactivate(): void {\n document.removeEventListener('keydown', this.onKeydown, true);\n this.container = null;\n // preventScroll: true — a bare focus() call auto-scrolls the target\n // into view within any scrollable ancestor by default. A real bug:\n // previouslyFocused is captured once at activate() time and never\n // updated afterward (nothing re-focuses the current thumbnail as the\n // viewer navigates), so restoring it on close can drag a host's own\n // scrollable thumbnail strip all the way back to wherever the gallery\n // was originally opened from, discarding whatever it had scrolled to\n // since — e.g. ActiveThumbnail's auto-scroll, entirely undone by a\n // focus restoration that has nothing to do with it.\n this.previouslyFocused?.focus({ preventScroll: true });\n this.previouslyFocused = null;\n }\n}\n","/**\n * DESIGN.md §2.4 — one Pointer-Events-based engine feeding all consumers:\n * core drag-to-navigate, core vertical-swipe-to-close, and (via `onPinch`/\n * `onDoubleTap`/`onWheelZoom`) a future zoom plugin that doesn't exist yet.\n * Pointer Events (not separate mouse/touch listeners) give mouse-drag and\n * touch-swipe parity for free — one code path, one set of thresholds.\n *\n * State machine: `idle → pending → dragging(h|v) → idle`. `pending` is the\n * window before `lockThreshold` is crossed, where it's still ambiguous\n * whether this is a tap, a horizontal drag, or a vertical drag; `dragging`\n * is entered once one axis's delta exceeds the threshold first, and the\n * *other* axis is ignored for the rest of that gesture (an axis lock, not\n * a snapped direction — diagonal drags don't fight the engine). There's no\n * literal blocking `settling` state after release: `onDragEnd` reports the\n * outcome synchronously and the caller owns however long its own\n * snap-back/completion animation takes; a new `pointerdown` before that\n * finishes is allowed to interrupt it (matches real touch-carousel UX —\n * grabbing a still-settling slide feels responsive, not broken).\n *\n * Pinch tracking is a separate concern layered on top of the same pointer\n * map: a second simultaneous pointer suspends whatever single-pointer drag\n * state was in progress (real pinches start from a single touch already\n * down) and switches to distance-ratio tracking until back down to one\n * pointer or zero.\n */\n\nexport type GestureDirection = 'horizontal' | 'vertical';\n\nexport interface GestureEngineOptions {\n /** px of movement before a direction locks in. DESIGN.md §2.4 default: 10. */\n lockThreshold: number;\n /** px of movement past which a released drag counts as \"swiped\" (completes), independent of velocity. DESIGN.md §2.4 default: 50. */\n swipeThreshold: number;\n /** px/ms release velocity past which a drag completes even under swipeThreshold — a fast flick counts. DESIGN.md §2.4 default: 0.3. */\n swipeVelocity: number;\n}\n\nexport interface GestureEngineCallbacks {\n /** Direction has just locked in for this gesture — the first move past lockThreshold. */\n onDragStart?(direction: GestureDirection, event: PointerEvent): void;\n /** delta: signed px moved along the locked axis so far (this move's cumulative total, not a per-frame diff). */\n onDragMove?(direction: GestureDirection, delta: number, event: PointerEvent): void;\n /** Pointer released (or cancelled) after a drag was in progress. velocity is signed px/ms over the whole gesture; completed reflects swipeThreshold/swipeVelocity. cancelled is true for pointercancel (e.g. an OS gesture took over) — always treated as not-completed regardless of delta/velocity. */\n onDragEnd?(\n direction: GestureDirection,\n delta: number,\n velocity: number,\n completed: boolean,\n cancelled: boolean,\n ): void;\n /** A pointerdown that never crossed lockThreshold before release — not a drag. Used to detect double-tap (the engine tracks tap timing/position itself and calls onDoubleTap instead when two land close enough together) as well as being a simple tap signal on its own. */\n onTap?(x: number, y: number, event: PointerEvent): void;\n onDoubleTap?(x: number, y: number, event: PointerEvent): void;\n /** scale is relative to the pinch's own start distance (1 = unchanged), not a cumulative multiplier across multiple pinch gestures. */\n onPinchStart?(centerX: number, centerY: number): void;\n onPinchMove?(scale: number, centerX: number, centerY: number): void;\n onPinchEnd?(): void;\n /** Ctrl+wheel (trackpad pinch reports this way in every major browser) or a genuine ctrl+scroll-wheel. deltaScale is a small increment (positive = zoom in), not an absolute scale — the caller accumulates it. */\n onWheelZoom?(deltaScale: number, x: number, y: number, event: WheelEvent): void;\n /** Only consulted on pointerdown — returning true means this pointer is ignored for the rest of its lifetime (a click on a real button/control shouldn't also start a drag). */\n ignore?(event: PointerEvent): boolean;\n /** Consulted right before capturing a locked drag's pointer — `false` skips capture for this gesture entirely (not just its drag effect). Default `true` if omitted. See onPointerMove's capture branch for why this needs to be a real skip, not just a suppressed effect. */\n shouldCapture?(): boolean;\n}\n\nconst DOUBLE_TAP_MAX_DELAY_MS = 300;\nconst DOUBLE_TAP_MAX_DISTANCE = 30;\nconst TAP_MAX_DELAY_MS = 500; // a pointerdown→up this slow isn't a \"tap\" even if it never crossed lockThreshold\n\ninterface PointerState {\n startX: number;\n startY: number;\n startTime: number;\n lastX: number;\n lastY: number;\n lastTime: number;\n}\n\nexport class GestureEngine {\n private readonly target: HTMLElement;\n private readonly options: GestureEngineOptions;\n private readonly callbacks: GestureEngineCallbacks;\n\n private readonly pointers = new Map<number, PointerState>();\n private primaryPointerId: number | null = null;\n private direction: GestureDirection | null = null;\n private dragStartDistance = 0; // along the locked axis, at the moment direction locked — subtracted so onDragStart's delta is 0\n\n private pinching = false;\n private pinchStartDistance = 0;\n\n /** True once `setPointerCapture` has actually been called for the gesture currently in progress — see `suppressRetargetedClick`'s own doc comment for why this needs tracking separately from `direction`. */\n private capturedThisGesture = false;\n\n // -Infinity, not 0: `event.timeStamp` is time-since-navigation-start, so a\n // real first tap is always some large positive number and 0 would seem\n // like a safe \"no previous tap\" sentinel in practice — but not always: a\n // tap in the first 300ms of the page's life, near the viewport's top-left\n // corner, would satisfy registerTap's isDouble check against this\n // sentinel and get wrongly reported as a double-tap on nothing. -Infinity\n // can never be within DOUBLE_TAP_MAX_DELAY_MS of any real timestamp.\n private lastTapTime = -Infinity;\n private lastTapX = 0;\n private lastTapY = 0;\n\n constructor(\n target: HTMLElement,\n callbacks: GestureEngineCallbacks,\n options?: Partial<GestureEngineOptions>,\n ) {\n this.target = target;\n this.callbacks = callbacks;\n this.options = {\n lockThreshold: options?.lockThreshold ?? 10,\n swipeThreshold: options?.swipeThreshold ?? 50,\n swipeVelocity: options?.swipeVelocity ?? 0.3,\n };\n\n this.target.addEventListener('pointerdown', this.onPointerDown);\n // Not passive: once a horizontal/vertical drag direction has locked in,\n // preventDefault() on move is required so the browser's own\n // scroll/back-navigation gesture doesn't fight the drag — see\n // onPointerMove. Before that lock, moves don't preventDefault, so\n // ordinary page scroll/tap behavior is unaffected until a real drag is\n // actually recognized.\n this.target.addEventListener('pointermove', this.onPointerMove);\n this.target.addEventListener('pointerup', this.onPointerUp);\n this.target.addEventListener('pointercancel', this.onPointerCancel);\n // ctrl+wheel must preventDefault (documented, not passive) — otherwise\n // the browser zooms the whole page instead of this becoming a gesture\n // the caller can hand to a zoom plugin.\n this.target.addEventListener('wheel', this.onWheel, { passive: false });\n }\n\n destroy(): void {\n this.target.removeEventListener('pointerdown', this.onPointerDown);\n this.target.removeEventListener('pointermove', this.onPointerMove);\n this.target.removeEventListener('pointerup', this.onPointerUp);\n this.target.removeEventListener('pointercancel', this.onPointerCancel);\n this.target.removeEventListener('wheel', this.onWheel);\n this.pointers.clear();\n }\n\n private readonly onPointerDown = (event: PointerEvent): void => {\n if (event.isPrimary === false && this.pointers.size === 0) return; // stray non-primary with no primary tracked yet — ignore\n if (this.pointers.size === 0 && this.callbacks.ignore?.(event)) return;\n\n this.pointers.set(event.pointerId, {\n startX: event.clientX,\n startY: event.clientY,\n startTime: event.timeStamp,\n lastX: event.clientX,\n lastY: event.clientY,\n lastTime: event.timeStamp,\n });\n\n if (this.pointers.size === 1) {\n this.primaryPointerId = event.pointerId;\n this.direction = null;\n this.capturedThisGesture = false;\n // Deliberately NOT captured here — see onPointerMove's direction-lock\n // branch for why.\n } else if (this.pointers.size === 2) {\n // A second pointer arriving mid-drag suspends the single-pointer\n // gesture (real pinches start from an already-down single touch) and\n // starts pinch tracking instead.\n this.direction = null;\n this.pinching = true;\n const [a, b] = [...this.pointers.values()];\n this.pinchStartDistance = distanceBetween(a!, b!);\n const center = centerOf(a!, b!);\n this.callbacks.onPinchStart?.(center.x, center.y);\n }\n };\n\n private readonly onPointerMove = (event: PointerEvent): void => {\n const state = this.pointers.get(event.pointerId);\n if (!state) return;\n state.lastX = event.clientX;\n state.lastY = event.clientY;\n state.lastTime = event.timeStamp;\n\n if (this.pinching) {\n if (this.pointers.size < 2) return;\n const [a, b] = [...this.pointers.values()];\n const distance = distanceBetween(a!, b!);\n const scale = this.pinchStartDistance > 0 ? distance / this.pinchStartDistance : 1;\n const center = centerOf(a!, b!);\n this.callbacks.onPinchMove?.(scale, center.x, center.y);\n return;\n }\n\n if (event.pointerId !== this.primaryPointerId) return;\n\n const dx = event.clientX - state.startX;\n const dy = event.clientY - state.startY;\n\n if (this.direction === null) {\n const absX = Math.abs(dx);\n const absY = Math.abs(dy);\n if (Math.max(absX, absY) < this.options.lockThreshold) return; // still pending — too small to call yet\n this.direction = absX > absY ? 'horizontal' : 'vertical';\n this.dragStartDistance = this.direction === 'horizontal' ? dx : dy;\n // Captured only once a real drag is confirmed (direction locked), not\n // on every pointerdown — capturing eagerly retargets the browser's\n // synthesized mousedown/click to the capturing element, breaking any\n // click-target-based logic downstream on an ordinary tap. Deferring\n // capture to here fixes that — but a *confirmed* drag still captures,\n // and a captured pointer's release still fires a real click\n // afterward, still retargeted to `target`. See\n // `suppressRetargetedClick` below for that other half of the fix.\n if (this.callbacks.shouldCapture?.() ?? true) {\n this.target.setPointerCapture(event.pointerId);\n this.capturedThisGesture = true;\n }\n this.callbacks.onDragStart?.(this.direction, event);\n }\n\n if (this.direction === 'horizontal') event.preventDefault(); // stop page/back-gesture scroll from fighting the drag\n\n const delta = (this.direction === 'horizontal' ? dx : dy) - this.dragStartDistance;\n this.callbacks.onDragMove?.(this.direction, delta, event);\n };\n\n private readonly onPointerUp = (event: PointerEvent): void => {\n this.finishPointer(event, false);\n };\n\n private readonly onPointerCancel = (event: PointerEvent): void => {\n this.finishPointer(event, true);\n };\n\n private finishPointer(event: PointerEvent, cancelled: boolean): void {\n const state = this.pointers.get(event.pointerId);\n if (!state) return;\n // pointerup/pointercancel carry their own release coordinates — these\n // don't necessarily match the last pointermove (coalesced/throttled\n // move events, or a release with no intervening move at all), so total\n // delta/velocity/tap-position must be computed from *this* event's\n // position, not whatever onPointerMove last recorded.\n state.lastX = event.clientX;\n state.lastY = event.clientY;\n state.lastTime = event.timeStamp;\n this.pointers.delete(event.pointerId);\n\n if (this.pinching) {\n if (this.pointers.size < 2) {\n this.pinching = false;\n this.callbacks.onPinchEnd?.();\n }\n // Still >= 2 pointers (a third was down) — stay in pinch mode; not\n // handled further here, an unusual case not worth extra state for.\n return;\n }\n\n if (event.pointerId !== this.primaryPointerId) return;\n\n if (this.direction === null) {\n // Never crossed lockThreshold — a tap, not a drag (unless it was\n // cancelled, or took too long to plausibly be an intentional tap).\n if (!cancelled && event.timeStamp - state.startTime <= TAP_MAX_DELAY_MS) {\n this.registerTap(state.lastX, state.lastY, event);\n }\n } else {\n const elapsed = Math.max(1, state.lastTime - state.startTime); // avoid divide-by-zero on a same-tick move\n const totalDelta =\n (this.direction === 'horizontal'\n ? state.lastX - state.startX\n : state.lastY - state.startY) - this.dragStartDistance;\n const velocity = totalDelta / elapsed;\n const completed =\n !cancelled &&\n (Math.abs(totalDelta) >= this.options.swipeThreshold ||\n Math.abs(velocity) >= this.options.swipeVelocity);\n this.callbacks.onDragEnd?.(this.direction, totalDelta, velocity, completed, cancelled);\n if (this.capturedThisGesture) this.suppressRetargetedClick();\n }\n\n this.primaryPointerId = null;\n this.direction = null;\n this.capturedThisGesture = false;\n }\n\n /**\n * A captured pointer's release still fires a real `click`, retargeted to\n * `target` regardless of where the pointer visually ends up — misread by\n * Gallery's click-outside-to-close as landing nowhere recognizable.\n * Consumes exactly one `click` on `target` in the capture phase, then\n * removes itself; a fallback timeout also removes it in case no `click`\n * ever comes, so it can't swallow a later, unrelated one.\n */\n private suppressRetargetedClick(): void {\n let done = false;\n const cleanup = (event?: MouseEvent): void => {\n if (done) return;\n done = true;\n event?.stopPropagation();\n this.target.removeEventListener('click', cleanup, { capture: true });\n };\n this.target.addEventListener('click', cleanup, { capture: true });\n setTimeout(cleanup, 500);\n }\n\n private registerTap(x: number, y: number, event: PointerEvent): void {\n const now = event.timeStamp;\n const isDouble =\n now - this.lastTapTime <= DOUBLE_TAP_MAX_DELAY_MS &&\n Math.hypot(x - this.lastTapX, y - this.lastTapY) <= DOUBLE_TAP_MAX_DISTANCE;\n if (isDouble) {\n this.callbacks.onDoubleTap?.(x, y, event);\n this.lastTapTime = 0; // consumed — a third tap starts fresh, isn't a triple-double\n } else {\n this.callbacks.onTap?.(x, y, event);\n this.lastTapTime = now;\n this.lastTapX = x;\n this.lastTapY = y;\n }\n }\n\n private readonly onWheel = (event: WheelEvent): void => {\n if (!event.ctrlKey) return;\n event.preventDefault();\n // Browsers report trackpad pinch as ctrl+wheel with deltaY roughly\n // proportional to pinch speed; negate so pinching out (deltaY < 0,\n // conventionally) reads as zooming in (positive deltaScale).\n const deltaScale = -event.deltaY * 0.01;\n this.callbacks.onWheelZoom?.(deltaScale, event.clientX, event.clientY, event);\n };\n}\n\nfunction distanceBetween(\n a: { lastX: number; lastY: number },\n b: { lastX: number; lastY: number },\n): number {\n return Math.hypot(a.lastX - b.lastX, a.lastY - b.lastY);\n}\n\nfunction centerOf(\n a: { lastX: number; lastY: number },\n b: { lastX: number; lastY: number },\n): { x: number; y: number } {\n return { x: (a.lastX + b.lastX) / 2, y: (a.lastY + b.lastY) / 2 };\n}\n","/** The drag's exact on-screen appearance at the moment it completed a close — `translateY(px)`/`scale`/`opacity`, all as last shown live (translateY clamped, see `GestureController.takeFrozenDragTransform`). `zoomOut()` jumps `target` here instantly (after measuring its natural box — see `ZoomTransitionTarget.dragStart`) before transitioning away from it, so the close continues in one motion from exactly where the drag left off. */\nexport interface FrozenDragTransform {\n translateY: number;\n scale: number;\n opacity: number;\n}\n\nexport interface ZoomTransitionTarget {\n /** The thumbnail element to animate to/from. */\n origin: HTMLElement;\n /** The `.shoji-slide-media` element being animated — not `.shoji-slide` itself, whose transform is already owned by pool-offset positioning. */\n target: HTMLElement;\n /** `item.width / item.height`, when known, else the origin thumbnail's own `naturalWidth`/`naturalHeight` ratio — just a *shape* guess; see `effectiveTargetBox`. */\n aspectRatio?: number;\n /** The real photo's true pixel size — `item.width`/`item.height` only, never the thumbnail's own natural size (would under-cap a large photo). Caps `containedBox` below at native resolution, mirroring `.shoji-slide-img`'s own `max-width/max-height: 100%`, instead of always growing toward filling the dialog. */\n naturalSize?: { width: number; height: number };\n /**\n * A completed drag-close's own last appearance (`GestureController.\n * takeFrozenDragTransform`) — `zoomOut()` (the only consumer) jumps\n * `target` here instantly, *after* measuring its natural box for the\n * landing math, then transitions away from it. Order matters: applying\n * this before measuring would make `effectiveTargetBox()` read the\n * child `<img>`'s rect *already* shrunk by this scale, double-counting\n * it once `zoomOut()`'s own transform (computed from that shrunk\n * measurement) then wholesale replaces — not composes with — it.\n */\n dragStart?: FrozenDragTransform;\n /**\n * The zoomed `<img>`'s own real on-screen rect (`getBoundingClientRect()`)\n * at the moment a button-close (or any non-drag close) starts, if it's\n * currently zoomed in — captured by `Gallery.beginClose()` from\n * `registerZoomStartProvider()` *before* the Zoom plugin's own\n * `beforeClose` handler resets it back to neutral (so `zoomOut()`'s own\n * measurement below stays correct — same ordering requirement `dragStart`\n * has, and the same reason).\n *\n * A rect, not the Zoom plugin's raw scale/pan numbers — an earlier version\n * passed those through directly and re-applied them to `target`\n * (`.shoji-slide-media`) the same way `dragStart` reapplies its own\n * translateY/scale. That doesn't work here: Zoom's own pan math is\n * relative to the `<img>` itself with `transform-origin: 0 0`\n * (`zoom/index.ts`), a *different* element with a *different* origin than\n * `target`'s `transform-origin: center` below — reapplying the same\n * numbers onto the wrong element/origin pair visibly shifted the image\n * toward its top-left corner the instant close started, worse the more\n * zoomed in it was. A rect sidesteps this entirely: `zoomOut()` computes\n * the jump the same center-to-center way it computes everything else\n * (`transformBetween`), from `target`'s own freshly-measured natural box\n * to this rect — origin-and-coordinate-system-agnostic, so it doesn't\n * matter what internal convention produced the rect.\n *\n * `zoomOut()` jumps `target` here instantly, then transitions away from\n * it, so closing while zoomed continues in one motion from the zoomed-in\n * view instead of snapping back to neutral first and only then shrinking\n * to the thumbnail. Mutually exclusive with `dragStart` in practice — you\n * can't complete a vertical drag-close while the zoom plugin is also\n * mid-pan, since `GestureController` suspends its own drag handling\n * entirely while zoomed (`isZoomed()`, DESIGN.md §4.6).\n */\n zoomStart?: Box;\n}\n\nfunction prefersReducedMotion(): boolean {\n return (\n typeof window.matchMedia === 'function' &&\n window.matchMedia('(prefers-reduced-motion: reduce)').matches\n );\n}\n\n/** `\"300ms\"` / `\"0.3s\"` → milliseconds. Reads the *actual* computed value, not an assumed default, so a host overriding `--shoji-duration` still gets a correct fallback timeout below. */\nfunction parseCssTime(value: string): number {\n const first = value.split(',')[0]?.trim() ?? '';\n if (first.endsWith('ms')) return parseFloat(first);\n if (first.endsWith('s')) return parseFloat(first) * 1000;\n return 0;\n}\n\nexport interface Box {\n left: number;\n top: number;\n width: number;\n height: number;\n}\n\n/** The `object-fit: contain` box for `aspectRatio` within `container`, computed analytically since the real image may not be loaded yet. `naturalSize` caps the result at the photo's own true pixel size, never stretching past native resolution. Exported — `SlideManager.ts`'s open-transition placeholder (§2.3) reuses this same math. */\nexport function containedBox(\n container: Box,\n aspectRatio: number,\n naturalSize?: { width: number; height: number },\n): Box {\n const containerRatio = container.width / container.height;\n let width = aspectRatio > containerRatio ? container.width : container.height * aspectRatio;\n let height = aspectRatio > containerRatio ? container.width / aspectRatio : container.height;\n if (naturalSize && naturalSize.width > 0 && naturalSize.height > 0) {\n const cap = Math.min(1, naturalSize.width / width, naturalSize.height / height);\n width *= cap;\n height *= cap;\n }\n return {\n left: container.left + (container.width - width) / 2,\n top: container.top + (container.height - height) / 2,\n width,\n height,\n };\n}\n\n/**\n * `target`'s effective visual box, not necessarily its own\n * `getBoundingClientRect()`. `target` is always its full flex-box size; the\n * photo inside is usually *smaller*, letterboxed to its own aspect ratio —\n * a real bug used the container's full rect, making the animation scale\n * from/to a box far bigger than the photo ever renders at, visibly\n * shrinking it to something much smaller than the thumbnail whenever the\n * aspect ratios didn't match (the common case). Preferred sources: the real\n * rendered media child, if attached; else an analytical contain-box from\n * `aspectRatio`; else the container's own rect as a last resort.\n *\n * A real bug this guards against: on a fresh open with nothing decoded yet,\n * `target`'s only child at this point is the loading spinner (§2.3) — a\n * fixed ~40px circle, not the photo. Trusting its rect as \"the real\n * rendered media child\" made the FLIP transition scale toward/from a box\n * far smaller than the actual photo, so the scale factor came out inverted\n * (>1 instead of <1) and the spinner rendered wildly oversized for the\n * transition's first frames before shrinking back down — never the actual\n * photo, which hadn't swapped in yet either way.\n *\n * Same reasoning excludes `.shoji-slide-open-placeholder` (§2.3's low-res\n * open() stand-in): it's a real, non-spinner `<img>`, but its own rendered\n * rect reflects *its* crop/aspect ratio, not the real photo's — trusting it\n * would scale toward the placeholder's shape instead of the real photo's,\n * only to visibly jump again once the real photo swaps in and this\n * transition (already run) no longer applies. Excluding it falls through to\n * the analytical `aspectRatio`-based box below, sized for the real photo\n * regardless of what shape the placeholder happens to be.\n */\nfunction effectiveTargetBox(\n target: HTMLElement,\n aspectRatio?: number,\n naturalSize?: { width: number; height: number },\n): Box {\n const child = target.firstElementChild;\n const isPlaceholder =\n child instanceof HTMLElement &&\n (child.classList.contains('shoji-slide-spinner') ||\n child.classList.contains('shoji-slide-open-placeholder'));\n if (child instanceof HTMLElement && !isPlaceholder) {\n const rect = child.getBoundingClientRect();\n if (rect.width > 0 && rect.height > 0) return rect;\n }\n const containerRect = target.getBoundingClientRect();\n if (aspectRatio && containerRect.width > 0 && containerRect.height > 0) {\n return containedBox(containerRect, aspectRatio, naturalSize);\n }\n return containerRect;\n}\n\n/**\n * Center-to-center translate + a single uniform scale that lands `target`'s\n * effective box (`effectiveTargetBox` above) *within* `origin`'s box,\n * without distorting it. `null` if either has no real size — nothing sane\n * to animate. One scale factor, not independent scaleX/scaleY: origin and\n * the photo essentially never share an exact aspect ratio, and scaling each\n * axis independently to force a rect match visibly squeezes/stretches the\n * image. `Math.min` keeps the box fully contained within origin's rect\n * (same tradeoff `object-fit: contain` makes); center always lands exactly\n * on origin's center, only the unconstrained axis's edges fall short.\n */\n/**\n * The actual translate3d/scale3d math `computeTransform` below needs —\n * pulled out on its own so `zoomOut`'s `zoomStart` jump (a *second* use of\n * the exact same center-to-center box-fitting math, landing on a captured\n * live rect instead of `origin`'s) can reuse it directly, rather than\n * re-deriving Zoom's own transform-origin/coordinate conventions on a\n * different element — see `ZoomTransitionTarget.zoomStart`'s own doc\n * comment for why that direct-replication approach doesn't work.\n */\nfunction transformBetween(from: Box, to: Box): string {\n const scale = Math.min(to.width / from.width, to.height / from.height);\n const translateX = to.left + to.width / 2 - (from.left + from.width / 2);\n const translateY = to.top + to.height / 2 - (from.top + from.height / 2);\n // translate3d/scale3d, not translate()/scale() — forces the GPU\n // compositing path instead of a main-thread-painted 2D transform, the\n // same fix already validated for the Zoom plugin's own scale animation\n // (DESIGN.md §4.6): this is the identical technique (a large photo\n // scaled via `transform`) on the same element, just driven by open/close\n // instead of pinch/toolbar zoom.\n return `translate3d(${translateX}px, ${translateY}px, 0) scale3d(${scale}, ${scale}, 1)`;\n}\n\n/**\n * `origin`'s effective visual box, not necessarily its own\n * `getBoundingClientRect()`. In selector/DOM-markup mode without Layout,\n * `origin` is typically a plain `<a>` wrapping a `<img>` — an inline element\n * by default, whose own box is sized to a text-line-height sliver (its\n * `font-size`/`line-height`), not the image it visually contains, even\n * though the image itself paints at full size. A real bug: the zoom\n * open/close transform used that sliver directly as the FLIP target,\n * landing the animation at a tiny, essentially-random offset instead of the\n * real thumbnail's visible position and size. Layout tiles aren't affected\n * (`display: block`, sized to match their child image exactly) — preferring\n * the child's rect there returns the same box either way.\n *\n * `querySelector('img')`, not `firstElementChild` — matches `scan.ts`'s own\n * `scanImage()` convention for locating a thumbnail inside arbitrary host\n * markup, where the `<img>` isn't necessarily the first child (a badge or\n * caption element placed before it in the DOM would otherwise get measured\n * instead, no more correct than the wrapper's own sliver).\n */\nfunction effectiveOriginBox(origin: HTMLElement): Box {\n const img = origin.querySelector('img');\n if (img) {\n const rect = img.getBoundingClientRect();\n if (rect.width > 0 && rect.height > 0) return rect;\n }\n return origin.getBoundingClientRect();\n}\n\nfunction computeTransform(\n origin: HTMLElement,\n target: HTMLElement,\n aspectRatio?: number,\n naturalSize?: { width: number; height: number },\n): string | null {\n const originRect = effectiveOriginBox(origin);\n const targetRect = effectiveTargetBox(target, aspectRatio, naturalSize);\n if (\n targetRect.width === 0 ||\n targetRect.height === 0 ||\n originRect.width === 0 ||\n originRect.height === 0\n ) {\n return null;\n }\n return transformBetween(targetRect, originRect);\n}\n\n/**\n * Waits for `target`'s own transition (on `property`, default `'transform'`)\n * to end (with a safety-net timeout in case transitionend never fires — an\n * interrupted/removed element, a browser quirk, or `--shoji-duration: 0ms`\n * under `prefers-reduced-motion`, which some browsers never fire a real\n * transitionend for at all) then calls `cb` exactly once. Exported: the\n * gesture-driven drag-to-navigate/drag-to-close settle animations (§2.4,\n * `Gallery.ts`) reuse this same wait-with-fallback logic rather than\n * duplicating it — same instant-jump-then-transition FLIP family of moves\n * as the zoom transition, just on a different element/property pairing.\n * `close()`'s own controls-fade-before-zoom-out sequencing (§2.6a) reuses it\n * a second way, waiting on `opacity` instead.\n */\nexport function waitForTransitionEnd(\n target: HTMLElement,\n cb: () => void,\n property = 'transform',\n): void {\n const durationMs = parseCssTime(getComputedStyle(target).transitionDuration);\n let done = false;\n const finish = (): void => {\n if (done) return;\n done = true;\n target.removeEventListener('transitionend', onEnd);\n cb();\n };\n const onEnd = (event: Event): void => {\n if (event.target === target && (event as TransitionEvent).propertyName === property) finish();\n };\n target.addEventListener('transitionend', onEnd);\n setTimeout(finish, durationMs + 100);\n}\n\n/**\n * A real bug: unconditionally clearing `transform` here could wipe out a\n * value another plugin (rotateFlip) legitimately set on the *same* element\n * in the time between this animation starting and its cleanup callback\n * firing (transitionend, or the fallback timeout — either can land well\n * after a quick click elsewhere). Only clears `transform` if it still\n * matches what this animation itself last wrote — nothing else has touched\n * it since — otherwise leaves it alone. `transition`/`transformOrigin`\n * are always safe to clear; nothing else in this codebase sets them.\n *\n * A second real bug, found via reopening the lightbox: `expectedTransform`\n * must be the value *read back* from `target.style.transform` right after\n * assigning it, not the raw string `computeTransform()` produced. Setting\n * `element.style.transform` to a string containing an arbitrary JS float\n * (e.g. `scale(0.10416666666666667)`, `computeTransform`'s un-rounded\n * `Math.min(...)` result) and reading it back gives a *differently\n * formatted* string — the browser's CSSOM serializer rounds/reformats\n * numeric values on its own (observed in Chromium: `scale(0.104167)`).\n * Comparing the raw JS string against that reformatted one here always\n * failed for any scale factor without a short, clean decimal, silently\n * skipping the clear — permanently leaving `zoomOut`'s shrink transform\n * applied to `.shoji-slide-media` after the lightbox closed. The next\n * `open()`'s `computeTransform` then measured that *already-shrunk*\n * element's `getBoundingClientRect()` as if it were the natural size,\n * computing a near-1 (barely visible) scale instead of a real zoom-in —\n * reads as \"doesn't zoom, just appears,\" and compounds on every further\n * open/close cycle since the stuck transform is never cleared either way.\n * `zoomIn`'s own `'none'` literal never hit this — a fixed string round-trips\n * through the CSSOM unchanged, unlike an arbitrary computed float.\n *\n * `opacity` is cleared unconditionally, same as `transition`/\n * `transformOrigin` — a completed drag-close (`Gallery.beginClose`) bakes\n * the drag's own dim onto `target` before this animation starts; nothing\n * else in this codebase sets `.shoji-slide-media`'s opacity, so there's\n * nothing else's value this could ever clobber.\n */\nfunction clearInlineTransform(target: HTMLElement, expectedTransform: string): void {\n target.style.transition = '';\n target.style.transformOrigin = '';\n target.style.willChange = '';\n target.style.opacity = '';\n if (target.style.transform === expectedTransform) target.style.transform = '';\n}\n\n/**\n * FLIP-style open: `target` starts visually at `origin`'s position/size (an\n * instant, untransitioned jump), then transitions to its natural layout —\n * reads as \"growing out of the thumbnail.\" Fire-and-forget: cleans up its\n * own inline styles once the transition ends, nothing to await.\n */\nexport function zoomIn({ origin, target, aspectRatio, naturalSize }: ZoomTransitionTarget): void {\n if (prefersReducedMotion()) return;\n const transform = computeTransform(origin, target, aspectRatio, naturalSize);\n if (!transform) return;\n\n target.style.transition = 'none';\n // computeTransform's translateX/Y is center-to-center math — must pair with\n // a center transform-origin (the CSS default), not 'top left', or the\n // scaled box ends up offset from origin by however far origin's center\n // sits from its own top-left corner.\n target.style.transformOrigin = 'center';\n // Promotes target onto its own compositing layer before the animated\n // transform starts, not mid-animation — same GPU-seam mitigation as the\n // Zoom plugin (DESIGN.md §4.6). Cleared in clearInlineTransform once the\n // transition ends, not left on permanently: this is the offset-0 pool\n // slot, alive for the gallery's whole lifetime, not a class scoped to\n // only-while-zoomed.\n target.style.willChange = 'transform';\n target.style.transform = transform;\n void target.offsetHeight; // force the instant jump to commit before transitioning away from it\n target.style.transition = 'transform var(--shoji-duration) var(--shoji-easing)';\n target.style.transform = 'none';\n\n waitForTransitionEnd(target, () => clearInlineTransform(target, 'none'));\n}\n\n/**\n * Reverse of `zoomIn`: `target` transitions from its natural position down\n * to `origin`'s rect — \"shrinking back into the thumbnail.\" `onComplete`\n * always fires exactly once, synchronously if there's nothing to animate\n * (reduced motion, or no valid rect), otherwise once the transition ends —\n * callers use this to know when it's safe to actually hide/finalize.\n */\nexport function zoomOut(\n { origin, target, aspectRatio, naturalSize, dragStart, zoomStart }: ZoomTransitionTarget,\n onComplete: () => void,\n): void {\n if (prefersReducedMotion()) {\n onComplete();\n return;\n }\n // Measured before dragStart/zoomStart is ever applied to `target` — see\n // ZoomTransitionTarget.dragStart's doc comment for why the order matters.\n const transform = computeTransform(origin, target, aspectRatio, naturalSize);\n if (!transform) {\n onComplete();\n return;\n }\n\n // computeTransform's translateX/Y is center-to-center math — must pair with\n // a center transform-origin (the CSS default), not 'top left', or the\n // scaled box ends up offset from origin by however far origin's center\n // sits from its own top-left corner.\n target.style.transformOrigin = 'center';\n target.style.willChange = 'transform'; // see zoomIn's doc comment on this line\n if (dragStart) {\n // Instant jump to exactly where the drag left off — same FLIP\n // technique `zoomIn()` uses to jump onto origin's box before\n // transitioning away from it, just jumping to wherever the drag left\n // off instead of a fixed point. translate3d/scale3d (not the 2D forms),\n // matching the landing transform's own function list below — mismatched\n // function types between the transition's start/end values force the\n // browser into matrix-decomposition interpolation instead of simple\n // per-parameter interpolation, and lose the GPU-compositing path this\n // codebase otherwise always uses for this element (DESIGN.md §2.3b).\n target.style.transition = 'none';\n target.style.transform = `translate3d(0px, ${dragStart.translateY}px, 0px) scale3d(${dragStart.scale}, ${dragStart.scale}, 1)`;\n target.style.opacity = String(dragStart.opacity);\n void target.offsetHeight; // commit the jump before transitioning away from it\n } else if (zoomStart) {\n // Same FLIP jump as dragStart above, computed the same center-to-center\n // way as `transform` above (not dragStart's raw translate/scale reuse\n // — see ZoomTransitionTarget.zoomStart's doc comment for why that\n // doesn't work here). `target`'s own effective box, re-measured fresh:\n // cheap, and guaranteed unchanged since the read inside\n // computeTransform() above — nothing's touched `target` in between.\n // Still correct even if `target` is currently rotated/flipped (the\n // RotateFlip plugin, applied directly to this same element, unlike\n // Zoom's own scale/pan — see below): both this measurement and\n // `zoomStart` itself were captured at the same rotation, so the\n // scale/translate between their two (equally rotated) bounding boxes\n // still isolates the zoom-only delta correctly.\n const targetRect = effectiveTargetBox(target, aspectRatio, naturalSize);\n if (targetRect.width > 0 && targetRect.height > 0) {\n // Composed onto whatever's already there, never replacing it outright\n // — a real bug, reported from real usage: closing while both rotated/\n // flipped *and* zoomed wholesale-overwrote RotateFlip's own\n // scaleX/scaleY/rotate() with this jump's plain translate/scale,\n // un-rotating *instantly* right here, before the real transition\n // below even starts — a snap to neutral rotation, then a separate\n // zoom-out, instead of the smooth combined un-rotate-while-shrinking\n // motion closing while rotated (without zoomStart at all) already\n // gets for free: transitioning target's transform from *whatever it\n // currently is* to the final plain shrink value is what makes that\n // work, and it needs target's rotation to still be sitting there\n // when the transition starts, not already erased by this jump.\n const existing = target.style.transform;\n const jump = transformBetween(targetRect, zoomStart);\n target.style.transition = 'none';\n target.style.transform = existing && existing !== 'none' ? `${existing} ${jump}` : jump;\n void target.offsetHeight;\n }\n }\n target.style.transition = 'transform var(--shoji-duration) var(--shoji-easing)';\n void target.offsetHeight;\n target.style.transform = transform;\n // Read back what the browser actually stored, not the raw string just\n // assigned — see clearInlineTransform's own doc comment for why the two\n // can differ (CSSOM float reformatting) and why that difference matters.\n const appliedTransform = target.style.transform;\n\n waitForTransitionEnd(target, () => {\n clearInlineTransform(target, appliedTransform);\n onComplete();\n });\n}\n","import {\n GestureEngine,\n type GestureDirection,\n type GestureEngineOptions,\n} from '../gestures/GestureEngine';\nimport type { SlideManager } from './SlideManager';\nimport { waitForTransitionEnd, type FrozenDragTransform } from './zoomTransition';\n\n/** DESIGN.md §2.4 — the drag-released settle easing, distinct from `--shoji-easing`'s open/close/zoom curve. */\nconst DRAG_SETTLE_TRANSITION = 'transform var(--shoji-duration) var(--shoji-momentum-easing)';\nconst DRAG_FEEDBACK_TRANSITION =\n 'transform var(--shoji-duration) var(--shoji-momentum-easing), opacity var(--shoji-duration) var(--shoji-momentum-easing)';\n/** px of vertical drag at which live close-feedback maxes out — independent of `swipeThreshold`, which decides actual completion. */\nconst VERTICAL_FEEDBACK_DISTANCE = 160;\n\n/**\n * A real interactive control, plus `.shoji-caption` — a click/drag starting\n * there should select/scroll its text natively, not get captured as a\n * gesture. Also used by `Gallery.ts`'s `isBackdropClick`, which used to have\n * its own narrower list (missing select/input/textarea/a[href]) — a\n * plugin-mounted non-button control got misread as a backdrop click and\n * closed the gallery. One shared selector so that can't recur.\n *\n * `.shoji-caption-modal` (DESIGN.md §2.3a) — a real bug, reported from real\n * usage: `GestureEngine`'s own tap recognition (`ignore` gates it before\n * any pointer tracking starts, `GestureEngine.ts`) runs entirely on\n * pointer events, independent of whatever a click listener's own\n * `stopPropagation()` does — a tap anywhere inside the caption modal\n * (its content area specifically; the close button was already covered as\n * a real `<button>`) still read as a legitimate tap on the dialog and fired\n * the `tap` bus event, which the Autoplay plugin listens to for its own\n * tap-to-toggle-play/pause. Excluding the whole modal here is what\n * actually stops that, not anything at the click-event layer.\n */\nexport const INTERACTIVE_CONTROL_SELECTOR =\n 'button, video, input, select, textarea, a[href], [data-shoji-no-drag], .shoji-caption, .shoji-caption-modal, .shoji-toolbar-overflow-panel';\n\n/**\n * `INTERACTIVE_CONTROL_SELECTOR` minus `video` — everything else there still\n * needs a real click/drag to behave natively (select/scroll caption text,\n * a plugin's own control, etc.), same reasoning as that selector's own doc\n * comment. `video` is carved out separately, below: unlike those, a native\n * `<video controls>`'s own interactive surface (tap-to-toggle, the\n * scrub-bar drag) isn't the *whole* element — DESIGN.md §2.4/§4.3's\n * swipe-to-navigate-video-slides feature needs gestures to reach the rest\n * of it, not be blocked outright the way the shared selector above still\n * blocks it for backdrop-click/caption-selection purposes.\n */\nconst GESTURE_IGNORE_SELECTOR =\n 'button, input, select, textarea, a[href], [data-shoji-no-drag], .shoji-caption, .shoji-caption-modal, .shoji-toolbar-overflow-panel';\n\n/**\n * A native `<video controls>`'s own scrub-bar/tap-to-toggle chrome is\n * browser-rendered, not real DOM Shoji can measure — there's no element to\n * exclude by selector the way `GESTURE_IGNORE_SELECTOR` excludes a real\n * `<button>`. Approximated instead as a fixed bottom margin\n * (`--shoji-video-gesture-margin`, shoji.css), reserved for the browser's\n * own touch handling; a touch starting above it is fair game for Shoji's\n * own gestures, same as anywhere on a photo slide.\n */\nfunction isInVideoControlsMargin(event: PointerEvent, video: HTMLVideoElement): boolean {\n const rect = video.getBoundingClientRect();\n const margin = parseFloat(\n getComputedStyle(video).getPropertyValue('--shoji-video-gesture-margin'),\n );\n return event.clientY >= rect.bottom - (Number.isFinite(margin) ? margin : 0);\n}\n\n/**\n * A real bug, found auditing this feature end to end with a real browser\n * (not caught by any jsdom unit test — real `click`-after-drag retargeting\n * only happens in a real browser, same gap `GestureEngine`'s own\n * `suppressRetargetedClick` doc comment describes): a drag starting inside\n * a video's reserved bottom margin is correctly never tracked as a Shoji\n * gesture (see `isInVideoControlsMargin` above) — left alone for the\n * browser's own scrub-bar handling. But the pointer is free to move\n * anywhere before release, the same way a real scrub-bar drag routinely\n * does, and an un-captured pointer's trailing `click` lands wherever it\n * actually ends up — often well past the video's own rendered bounds, in\n * `.shoji-slide-media`'s empty letterbox padding around it. That's not a\n * real interactive control (`INTERACTIVE_CONTROL_SELECTOR`), so `Gallery.ts`'s\n * `isBackdropClick` read it as a genuine backdrop click and closed the\n * gallery — defeating \"left alone for the browser's own controls\" the\n * moment a real scrub-bar interaction moved vertically at all. Confirmed\n * directly: a drag starting 5px above a video's bottom edge, moving 200px\n * down, closed the gallery even though `isInVideoControlsMargin` correctly\n * ignored the pointerdown. Same fix as a captured drag's own retargeted\n * click — swallow exactly one trailing click, capture phase, so it never\n * reaches `isBackdropClick` at all. A fallback timeout also removes the\n * listener in case no `click` ever comes, so it can't swallow a later,\n * unrelated one.\n */\nfunction suppressNextClick(dialog: HTMLElement): void {\n let done = false;\n const cleanup = (event?: MouseEvent): void => {\n if (done) return;\n done = true;\n event?.stopPropagation();\n dialog.removeEventListener('click', cleanup, { capture: true });\n };\n dialog.addEventListener('click', cleanup, { capture: true });\n setTimeout(cleanup, 500);\n}\n\n/**\n * `.shoji-caption--video` (shoji.css, DESIGN.md §2.3a/§4-video) sets\n * `pointer-events: none` on a video slide's caption, letting a click reach\n * the video/provider controls underneath it — which also means it never\n * appears in `event.composedPath()` there at all, so\n * `GESTURE_IGNORE_SELECTOR`'s selector-based check can't see it, the same\n * gap `Gallery.ts`'s `isBackdropClick()` already solves the same way:\n * checked by coordinates instead, the one place this function has to be.\n * A real bug this fixes: swipe-to-navigate/drag-to-close over a provider\n * video's own letterboxed margins (below) would otherwise engage right\n * through the caption sitting over them.\n */\nfunction isOverVideoCaption(event: PointerEvent, caption: HTMLElement): boolean {\n if (caption.hidden || !caption.classList.contains('shoji-caption--video')) return false;\n const rect = caption.getBoundingClientRect();\n return (\n event.clientX >= rect.left &&\n event.clientX <= rect.right &&\n event.clientY >= rect.top &&\n event.clientY <= rect.bottom\n );\n}\n\n/** A click/drag starting on a real control (or, on a video slide, its click-through caption — see `isOverVideoCaption`) shouldn't also be captured as a gesture — see `GESTURE_IGNORE_SELECTOR`/`isInVideoControlsMargin`. Ignoring specifically for the video-margin reason also arms `suppressNextClick` — see its own doc comment. */\nfunction shouldIgnoreGesture(\n event: PointerEvent,\n caption: HTMLElement,\n dialog: HTMLElement,\n): boolean {\n if (isOverVideoCaption(event, caption)) return true;\n const path = event.composedPath();\n const video = path.find((node): node is HTMLVideoElement => node instanceof HTMLVideoElement);\n if (video) {\n if (!isInVideoControlsMargin(event, video)) return false;\n suppressNextClick(dialog);\n return true;\n }\n return path.some((node) => node instanceof Element && node.matches(GESTURE_IGNORE_SELECTOR));\n}\n\n/** What `GestureController` needs from `Gallery` — narrow on purpose, so this module never reaches into Gallery internals beyond this contract. */\nexport interface GestureControllerHost {\n dialog: HTMLElement;\n /** Read live, not snapshotted — `shouldIgnoreGesture()` needs its current bounding box/visibility on every gesture start, not whatever it was when `GestureController` was constructed. See `isOverVideoCaption`. */\n caption: HTMLElement;\n slides: SlideManager;\n canGoNext(): boolean;\n canGoPrev(): boolean;\n next(): void;\n prev(): void;\n close(): void;\n /** Same effect as `close()`, but for a completed vertical swipe — skips the separate controls-fade pause in front of the zoom-out (see `finishVerticalDrag`). */\n closeFromSwipe(frozenDrag: FrozenDragTransform): void;\n /** Live, reversible — crossing the close threshold mid-drag hides the toolbar/nav/counter/caption (a \"let go now and this closes\" cue); dragging back under it before release reveals them again. See `applyVerticalDragFeedback`. */\n setControlsHiddenForDrag(hidden: boolean): void;\n /** `closable: false` (GalleryOptions) — false suspends vertical drag-to-close entirely: no live feedback, release never calls `close()`. */\n canClose(): boolean;\n onActivity(): void;\n /** DESIGN.md §4-zoom — true while zoomed; suspends drag-to-navigate/close. */\n isZoomed(): boolean;\n}\n\n/** Relays for gestures core has no built-in behavior for — DESIGN.md §2.4; the zoom plugin (§4-zoom) consumes these via the gallery event bus. */\nexport interface GestureRelayCallbacks {\n onTap(x: number, y: number): void;\n onDoubleTap(x: number, y: number): void;\n onPinchStart(centerX: number, centerY: number): void;\n onPinchMove(scale: number, centerX: number, centerY: number): void;\n onPinchEnd(): void;\n onWheelZoom(deltaScale: number, x: number, y: number): void;\n}\n\n/**\n * DESIGN.md §2.4 — owns the gesture-driven side of the lightbox: horizontal\n * drag-to-navigate and vertical drag-to-close, built on `GestureEngine`\n * (`src/gestures/`) plus the live-feedback/settle-animation mechanics\n * specific to this lightbox's DOM. Split out of `Gallery.ts` (CLAUDE.md:\n * files ≤ ~400 lines) — `Gallery` only constructs this, forwards the host\n * callbacks it needs, and relays no-built-in-effect gestures onto its bus.\n */\nexport class GestureController {\n private readonly engine: GestureEngine;\n /** Same distance `GestureEngine` itself uses to decide a release would complete the close — reused here so the live controls-hide cue in `applyVerticalDragFeedback` actually means what it visually claims. */\n private readonly controlsHideThreshold: number;\n private controlsHiddenForDrag = false;\n private lastDragDelta = 0;\n\n constructor(\n private readonly host: GestureControllerHost,\n relay: GestureRelayCallbacks,\n options?: Partial<GestureEngineOptions>,\n ) {\n this.controlsHideThreshold = options?.swipeThreshold ?? 50;\n this.engine = new GestureEngine(\n host.dialog,\n {\n ignore: (event) => shouldIgnoreGesture(event, host.caption, host.dialog),\n shouldCapture: () => !host.isZoomed(),\n onDragStart: () => host.onActivity(),\n onDragMove: this.onDragMove,\n onDragEnd: this.onDragEnd,\n onTap: (x, y) => relay.onTap(x, y),\n onDoubleTap: (x, y) => relay.onDoubleTap(x, y),\n onPinchStart: (centerX, centerY) => relay.onPinchStart(centerX, centerY),\n onPinchMove: (scale, centerX, centerY) => relay.onPinchMove(scale, centerX, centerY),\n onPinchEnd: () => relay.onPinchEnd(),\n onWheelZoom: (deltaScale, x, y) => relay.onWheelZoom(deltaScale, x, y),\n },\n options,\n );\n }\n\n destroy(): void {\n this.engine.destroy();\n }\n\n /** Horizontal follows the finger 1:1; vertical drives close-feedback instead. Axis-locked, one branch per drag. */\n private readonly onDragMove = (direction: GestureDirection, delta: number): void => {\n if (this.host.isZoomed()) return; // the zoom plugin owns pan while zoomed\n if (direction === 'horizontal') {\n this.host.slides.setDragOffset(delta, null);\n } else if (this.host.canClose()) {\n this.applyVerticalDragFeedback(delta);\n }\n };\n\n private readonly onDragEnd = (\n direction: GestureDirection,\n delta: number,\n _velocity: number,\n completed: boolean,\n ): void => {\n if (this.host.isZoomed()) return;\n if (direction === 'horizontal') {\n this.finishHorizontalDrag(delta, completed);\n } else {\n this.finishVerticalDrag(completed);\n }\n };\n\n /** `completed` decides intent; `canGoNext`/`canGoPrev` guard whether that's actually possible (`loop: false` at an end item). */\n private finishHorizontalDrag(delta: number, completed: boolean): void {\n const goingNext = delta < 0;\n const canMove = goingNext ? this.host.canGoNext() : this.host.canGoPrev();\n\n if (!completed || delta === 0 || !canMove) {\n this.settleDragOffset(0, () => {});\n return;\n }\n\n const width = this.host.dialog.getBoundingClientRect().width || 1;\n const target = goingNext ? -width : width;\n this.settleDragOffset(target, () => {\n // Instant, untransitioned reset in the same synchronous tick as the\n // index change — same FLIP-family trick as zoomTransition.ts's\n // instant-jump: the slot that just animated into the center position\n // is about to receive that exact same \"new current\" item's content\n // (its structural offset already matches, since preload keeps it\n // rendered), so resetting to offset 0 while simultaneously swapping\n // content is visually seamless, not a jump.\n this.host.slides.setDragOffset(0, null);\n if (goingNext) this.host.next();\n else this.host.prev();\n });\n }\n\n private settleDragOffset(targetPx: number, onSettled: () => void): void {\n const settleEl = this.host.slides.getSlotRoot(0);\n this.host.slides.setDragOffset(targetPx, DRAG_SETTLE_TRANSITION);\n if (!settleEl) {\n onSettled();\n return;\n }\n waitForTransitionEnd(settleEl, onSettled);\n }\n\n /**\n * Purely presentational — scales/fades the *image* as it's dragged away,\n * without closing until release decides. Applied to `host.slides.element`,\n * not `host.dialog` — requested directly: toolbar/nav/counter/caption are\n * siblings, not descendants, so they stay anchored, not moving with it.\n */\n private applyVerticalDragFeedback(delta: number): void {\n const progress = Math.min(Math.abs(delta) / VERTICAL_FEEDBACK_DISTANCE, 1);\n const slides = this.host.slides.element;\n this.lastDragDelta = delta;\n slides.style.transition = '';\n slides.style.transform = `translateY(${delta}px) scale(${1 - progress * 0.15})`;\n slides.style.opacity = String(1 - progress * 0.6);\n\n // Requested directly: past the same distance a release would close,\n // controls disappear — a \"let go now\" cue, distinct from the image's own\n // dim above (capped at 0.4, never fully hides). Reversible: retreating\n // under it before release reveals them. Only on a crossing, not every move.\n const pastThreshold = Math.abs(delta) >= this.controlsHideThreshold;\n if (pastThreshold !== this.controlsHiddenForDrag) {\n this.controlsHiddenForDrag = pastThreshold;\n this.host.setControlsHiddenForDrag(pastThreshold);\n }\n }\n\n private clearVerticalDragFeedback(animate: boolean): void {\n const slides = this.host.slides.element;\n slides.style.transition = animate ? DRAG_FEEDBACK_TRANSITION : '';\n slides.style.transform = '';\n slides.style.opacity = '';\n }\n\n private finishVerticalDrag(completed: boolean): void {\n const wasHiddenForDrag = this.controlsHiddenForDrag;\n this.controlsHiddenForDrag = false;\n if (completed && this.host.canClose()) {\n this.host.closeFromSwipe(this.takeFrozenDragTransform());\n } else {\n // Not completed, or closable: false suspended the drag entirely (no\n // feedback was ever applied by onDragMove) — either way, a clean\n // reset (harmless no-op in the suspended case, nothing to clear).\n this.clearVerticalDragFeedback(true);\n // Not closing — controls must come back if the cue above left them\n // hidden, regardless of exactly where release landed (e.g.\n // swipeVelocity deciding \"not completed\" past this threshold).\n if (wasHiddenForDrag) this.host.setControlsHiddenForDrag(false);\n }\n }\n\n /**\n * Reads the drag's last live appearance, instantly resets `.shoji-slides`\n * back to neutral (nothing left on it to visibly snap — `Gallery` bakes\n * these same values onto the photo itself in the same synchronous tick,\n * so the rendered result is unchanged, just re-homed), and returns them\n * for that hand-off. Translate is the raw, unclamped drag distance —\n * requested directly: the close animation must continue from exactly\n * where the drag left off, full stop, not recenter first. (A previous\n * version clamped this to the same 160px the dim/scale feedback ramps\n * over, to bound how far away the close animation could start — but\n * clamping is itself an instant correction: for any drag past that\n * distance, release visibly snapped the photo from wherever it actually\n * was back to the clamped point, before the real shrink-to-thumbnail\n * motion continued from there. That snap — not the shrink itself — is\n * what reads as \"jumps to a small image in the middle of the screen.\"\n * Reported from real usage, confirmed on video: released past the clamp,\n * the photo visibly jumped from off-screen back to near-center in a\n * single frame.)\n */\n private takeFrozenDragTransform(): FrozenDragTransform {\n const progress = Math.min(Math.abs(this.lastDragDelta) / VERTICAL_FEEDBACK_DISTANCE, 1);\n const frozen: FrozenDragTransform = {\n translateY: this.lastDragDelta,\n scale: 1 - progress * 0.15,\n opacity: 1 - progress * 0.6,\n };\n this.clearVerticalDragFeedback(false);\n return frozen;\n }\n}\n","/** DESIGN.md §2.6 — visually-hidden `aria-live` region announcing slide changes. */\nexport class LiveRegion {\n readonly element: HTMLElement;\n\n constructor() {\n this.element = document.createElement('div');\n this.element.className = 'shoji-live-region';\n this.element.setAttribute('role', 'status');\n this.element.setAttribute('aria-live', 'polite');\n }\n\n announce(text: string): void {\n // Clear-then-set (with a forced reflow between) so repeated identical\n // announcements still fire in real assistive tech, which otherwise\n // ignores a text node set to the same value it already held.\n this.element.textContent = '';\n void this.element.offsetWidth;\n this.element.textContent = text;\n }\n}\n","import type { GalleryItem, GalleryItemInput, MediaSource, VideoDescriptor } from './types';\n\n/** DESIGN.md §2.1 zero-config quickstart. */\nexport const DEFAULT_SELECTOR =\n ':scope > a, :scope > [data-shoji-src], :scope > [data-shoji-video]';\n\nexport interface ScannedItem {\n element: HTMLElement;\n item: GalleryItem;\n}\n\nexport function scanContainer(\n container: HTMLElement,\n selector: string = DEFAULT_SELECTOR,\n): ScannedItem[] {\n const elements = Array.from(container.querySelectorAll<HTMLElement>(selector));\n const scanned: ScannedItem[] = [];\n for (const element of elements) {\n const item = scanVideo(element) ?? scanImage(element);\n if (item) scanned.push({ element, item });\n }\n return scanned;\n}\n\nfunction attr(el: Element, name: string): string | undefined {\n return el.getAttribute(name) ?? undefined;\n}\n\nfunction numAttr(el: Element, name: string): number | undefined {\n const raw = el.getAttribute(name);\n if (raw === null) return undefined;\n const n = Number(raw);\n return Number.isFinite(n) ? n : undefined;\n}\n\n/**\n * `caption`/`id` first, then any other `data-shoji-*` attribute with no\n * matching `GalleryItem` field lands in `item.data`, keyed verbatim (not\n * camelCased) — DESIGN.md §2.1. By this point every named field above is\n * already set, so `key in item` alone tells known suffixes (src/thumb/\n * alt/.../poster) apart from custom ones — except attributes that fold into\n * a *nested* field (`video-id`/`video-provider` → `item.video.id`/\n * `.provider`, both already applied in `scanVideo` by the time this runs)\n * or have no field of their own at all (`no-drag`, GestureController's\n * drag-opt-out); `key in item` can't see either case, so both are excluded\n * by name instead.\n */\nconst NO_OWN_DATA_FIELD = new Set(['no-drag', 'video-id', 'video-provider']);\n\nfunction applyCommon(element: HTMLElement, item: GalleryItem, src: string): void {\n const caption = attr(element, 'data-shoji-caption');\n if (caption) item.caption = caption;\n item.id = attr(element, 'data-shoji-id') ?? src;\n\n let data: Record<string, string> | undefined;\n for (const a of element.attributes) {\n if (!a.name.startsWith('data-shoji-')) continue;\n const key = a.name.slice(11);\n if (!NO_OWN_DATA_FIELD.has(key) && !(key in item)) (data ??= {})[key] = a.value;\n }\n if (data) item.data = data;\n}\n\nfunction scanImage(element: HTMLElement): GalleryItem | undefined {\n const src = attr(element, 'href') ?? attr(element, 'data-shoji-src');\n if (!src) return undefined;\n\n const item: GalleryItem = { src };\n const img = element.querySelector('img');\n const thumb = img?.getAttribute('src') ?? attr(element, 'data-shoji-thumb');\n if (thumb) item.thumb = thumb;\n const alt = img?.getAttribute('alt') ?? attr(element, 'data-shoji-alt');\n if (alt) item.alt = alt;\n const width = numAttr(element, 'data-shoji-width');\n if (width !== undefined) item.width = width;\n const height = numAttr(element, 'data-shoji-height');\n if (height !== undefined) item.height = height;\n // On the wrapping element, not the inner <img> (that's the thumb; `src`\n // above is the full-resolution image, so it needs its own attribute).\n const srcset = attr(element, 'data-shoji-srcset');\n if (srcset) item.srcset = srcset;\n const sizes = attr(element, 'data-shoji-sizes');\n if (sizes) item.sizes = sizes;\n applyCommon(element, item, src);\n return item;\n}\n\n/**\n * DESIGN.md §2.1 — a nested `<video>` is always html5; `data-shoji-video`\n * checks known provider hosts first. `data-shoji-video-id`/\n * `data-shoji-video-provider` (both optional) can override what would\n * otherwise be parsed out of that URL — see `resolveExplicitVideo`.\n */\nfunction scanVideo(element: HTMLElement): GalleryItem | undefined {\n const videoIdAttr = attr(element, 'data-shoji-video-id');\n const videoEl = element.querySelector('video');\n if (videoEl) {\n if (videoIdAttr) {\n console.warn(\n `Shoji: data-shoji-video-id=\"${videoIdAttr}\" ignored — no effect on a nested <video> (always html5).`,\n );\n }\n const sources: MediaSource[] = Array.from(videoEl.querySelectorAll('source'))\n .map((s) => ({ src: s.getAttribute('src') ?? '', type: s.getAttribute('type') ?? '' }))\n .filter((s) => s.src);\n const src = videoEl.getAttribute('src') ?? sources[0]?.src;\n if (!src) return undefined;\n\n const item: GalleryItem = { src, video: { provider: 'html5' } };\n if (sources.length) item.sources = sources;\n const poster = videoEl.getAttribute('poster');\n if (poster) item.poster = poster;\n // Same bug as the data-shoji-video branch below: without this, these\n // fall through into item.data.width/height as strings instead.\n const width = numAttr(element, 'data-shoji-width');\n if (width !== undefined) item.width = width;\n const height = numAttr(element, 'data-shoji-height');\n if (height !== undefined) item.height = height;\n applyCommon(element, item, src);\n return item;\n }\n\n const videoUrl = attr(element, 'data-shoji-video');\n if (videoUrl) {\n const providerAttr = attr(element, 'data-shoji-video-provider');\n const resolved = resolveExplicitVideo(videoUrl, providerAttr, videoIdAttr);\n const item: GalleryItem = { src: videoUrl, video: resolved.video };\n if (resolved.sources) item.sources = resolved.sources;\n const poster = attr(element, 'data-shoji-poster');\n if (poster) item.poster = poster;\n // A real bug: unlike `scanImage`, this never read these — they fell\n // through into `item.data.width`/`item.data.height` as strings instead\n // of `item.width`/`item.height`, silently losing `applyAspect()`'s\n // aspect-ratio and the open-placeholder's explicit natural-size box for\n // every DOM-mode (`data-shoji-*`) provider-video item.\n const width = numAttr(element, 'data-shoji-width');\n if (width !== undefined) item.width = width;\n const height = numAttr(element, 'data-shoji-height');\n if (height !== undefined) item.height = height;\n applyCommon(element, item, videoUrl);\n return item;\n }\n\n if (videoIdAttr) {\n console.warn(\n `Shoji: data-shoji-video-id=\"${videoIdAttr}\" ignored — requires a data-shoji-video URL.`,\n );\n }\n return undefined;\n}\n\nconst KNOWN_VIDEO_PROVIDERS = new Set(['youtube', 'vimeo', 'html5']);\n\nfunction html5Fallback(url: string): { video: VideoDescriptor; sources: MediaSource[] } {\n return { video: { provider: 'html5' }, sources: [{ src: url, type: guessVideoType(url) }] };\n}\n\n/**\n * `data-shoji-video-provider` declares the provider outright — trusts an id\n * even off a non-YouTube/non-Vimeo host (e.g. an internal proxy URL).\n * Mirrors dynamic mode's `video: { provider: '...' }`.\n */\nfunction resolveExplicitVideo(\n url: string,\n providerAttr: string | undefined,\n idAttr: string | undefined,\n): { video: VideoDescriptor; sources?: MediaSource[] } {\n if (providerAttr && !KNOWN_VIDEO_PROVIDERS.has(providerAttr)) {\n console.warn(`Shoji: data-shoji-video-provider=\"${providerAttr}\" isn't recognized — ignored.`);\n providerAttr = undefined;\n }\n\n if (providerAttr === 'html5') {\n if (idAttr)\n console.warn(`Shoji: data-shoji-video-id=\"${idAttr}\" ignored — provider html5 has no id.`);\n return html5Fallback(url);\n }\n\n const youtube = resolveDetectedProvider(\n 'youtube',\n url,\n providerAttr,\n idAttr,\n isYouTubeUrl,\n detectYouTubeId,\n );\n if (youtube) return youtube;\n const vimeo = resolveDetectedProvider(\n 'vimeo',\n url,\n providerAttr,\n idAttr,\n isVimeoUrl,\n detectVimeoId,\n );\n if (vimeo) return vimeo;\n\n if (idAttr) {\n console.warn(\n `Shoji: data-shoji-video-id=\"${idAttr}\" ignored — data-shoji-video=\"${url}\" isn't a recognized YouTube/Vimeo URL; falling back to html5.`,\n );\n }\n return html5Fallback(url);\n}\n\n/** Shared logic for youtube/vimeo, the only two providers with URL-based host detection. `undefined` means \"not this provider, try the next one\"; anything else is final, explicit trusting `idAttr` off any host, unset falling back to html5 quietly. */\nfunction resolveDetectedProvider(\n provider: 'youtube' | 'vimeo',\n url: string,\n providerAttr: string | undefined,\n idAttr: string | undefined,\n isHost: (url: string) => boolean,\n detectId: (url: string) => string | undefined,\n): { video: VideoDescriptor; sources?: MediaSource[] } | undefined {\n const explicit = providerAttr === provider;\n if (!explicit && !isHost(url)) return undefined;\n const id = idAttr ?? detectId(url);\n if (!id) {\n if (!explicit) return html5Fallback(url);\n console.warn(\n `Shoji: data-shoji-video-provider=\"${provider}\" but no id could be parsed from \"${url}\".`,\n );\n }\n return { video: { provider, id, url } };\n}\n\nconst YOUTUBE_HOSTS = new Set(['youtu.be', 'youtube.com', 'youtube-nocookie.com']);\nconst VIMEO_HOSTS = new Set(['vimeo.com', 'player.vimeo.com']);\n\n/** Strips the `www.`/`m.`/`music.` subdomain YouTube/Vimeo serve the same content under either way. */\nfunction normalizeVideoHost(hostname: string): string {\n return hostname.replace(/^(www|m|music)\\./, '');\n}\n\n/** Host-only, weaker than `detectYouTubeId` on purpose — tells \"wrong site\" (a real misconfig with an explicit id set) apart from \"right site, unparsed path\" (what an explicit id is for). */\nfunction isYouTubeUrl(url: string): boolean {\n try {\n return YOUTUBE_HOSTS.has(normalizeVideoHost(new URL(url, window.location.href).hostname));\n } catch {\n return false;\n }\n}\n\n/** Same reasoning as `isYouTubeUrl`, for Vimeo's two hosts (`vimeo.com` for an ordinary page link, `player.vimeo.com` for an already-embed-shaped URL). */\nfunction isVimeoUrl(url: string): boolean {\n try {\n return VIMEO_HOSTS.has(normalizeVideoHost(new URL(url, window.location.href).hostname));\n } catch {\n return false;\n }\n}\n\n/**\n * `youtu.be/{id}`, `watch?v=`, `/embed/`, `/shorts/`, `/live/` (streams/\n * premieres), `/v/` (legacy), on `youtube.com` or `youtube-nocookie.com`\n * (the privacy-enhanced embed domain) — `www.`/`m.`/`music.` subdomains all\n * normalize the same way. `undefined` otherwise.\n */\nfunction detectYouTubeId(url: string): string | undefined {\n let parsed: URL;\n try {\n parsed = new URL(url, window.location.href);\n } catch {\n return undefined;\n }\n const host = normalizeVideoHost(parsed.hostname);\n if (host === 'youtu.be') return parsed.pathname.slice(1).split('/')[0] || undefined;\n if (!YOUTUBE_HOSTS.has(host)) return undefined;\n if (parsed.pathname === '/watch') return parsed.searchParams.get('v') ?? undefined;\n const match = parsed.pathname.match(/^\\/(?:embed|shorts|live|v)\\/([^/?]+)/);\n return match?.[1];\n}\n\n/** First numeric path segment on a recognized Vimeo host — covers `vimeo.com/{id}`, `vimeo.com/{id}/{hash}` (unlisted-video privacy hash, kept in `url` not the id), `player.vimeo.com/video/{id}`, and an id buried deeper (`vimeo.com/channels/x/{id}`). `undefined` on an unrecognized host too. */\nfunction detectVimeoId(url: string): string | undefined {\n let parsed: URL;\n try {\n parsed = new URL(url, window.location.href);\n } catch {\n return undefined;\n }\n if (!VIMEO_HOSTS.has(normalizeVideoHost(parsed.hostname))) return undefined;\n return parsed.pathname.match(/\\/(\\d+)(?:\\/|$)/)?.[1];\n}\n\n/** youtube/vimeo, in host-detection preference order — same pair `resolveDetectedProvider` covers for selector mode. */\nconst DETECTABLE_PROVIDERS = [\n { provider: 'youtube' as const, isHost: isYouTubeUrl, detectId: detectYouTubeId },\n { provider: 'vimeo' as const, isHost: isVimeoUrl, detectId: detectVimeoId },\n];\n\n/**\n * Dynamic mode skips `scanContainer()` entirely, so it never gets the same\n * src-based detection `data-shoji-video`/`data-shoji-video-id` have.\n * Mirrors it here: `video: true` infers the whole descriptor from `src`\n * (youtube or vimeo if the host matches, else html5); `{provider:'youtube'|\n * 'vimeo'}` with no `id` fills in just the id. An explicit `id` always\n * wins. Warns (doesn't throw) when a detectable-provider item ends up with\n * no id — the renderer guards this at render time too, showing a\n * placeholder instead of a broken embed.\n */\nexport function resolveDynamicVideoItems(items: GalleryItemInput[]): GalleryItem[] {\n return items.map((item): GalleryItem => {\n if (item.video === true) {\n const detected = DETECTABLE_PROVIDERS.find((p) => p.isHost(item.src));\n const video: VideoDescriptor = detected\n ? { provider: detected.provider, id: detected.detectId(item.src) }\n : { provider: 'html5' };\n if ((video.provider === 'youtube' || video.provider === 'vimeo') && !video.id) {\n console.warn(\n `Shoji: item \"${item.id ?? item.src}\" has video: true, but no id could be parsed from src (\"${item.src}\") — the embed won't load. Use an explicit video.id instead.`,\n );\n }\n return { ...item, video };\n }\n // TS narrows `item.video` past this point, but not `item`'s own type —\n // hence the cast (the map's `: GalleryItem` annotation is what actually\n // catches a mistake here).\n const resolved = item as GalleryItem;\n const provider = resolved.video?.provider;\n if (provider !== 'youtube' && provider !== 'vimeo') return resolved;\n if (resolved.video && 'id' in resolved.video && resolved.video.id) return resolved;\n const id = DETECTABLE_PROVIDERS.find((p) => p.provider === provider)!.detectId(resolved.src);\n if (!id) {\n console.warn(\n `Shoji: item \"${resolved.id ?? resolved.src}\" has video.provider: '${provider}' with no id, and src (\"${resolved.src}\") isn't a recognized ${provider === 'youtube' ? 'YouTube' : 'Vimeo'} URL — the embed won't load. Set video.id explicitly.`,\n );\n return resolved;\n }\n return { ...resolved, video: { provider, id, url: resolved.video?.url } };\n });\n}\n\nfunction guessVideoType(url: string): string {\n const clean = url.split(/[?#]/)[0] ?? url;\n const ext = clean.slice(clean.lastIndexOf('.') + 1).toLowerCase();\n if (ext === 'webm') return 'video/webm';\n if (ext === 'ogg' || ext === 'ogv') return 'video/ogg';\n return 'video/mp4';\n}\n","import type { VideoProviderRenderer } from './plugin';\nimport type { GalleryItem } from './types';\nimport { containedBox } from './zoomTransition';\n\ninterface Slot {\n root: HTMLElement;\n media: HTMLElement;\n /** Which pool position (`-preload…+preload`) this slot represents — reassigned by `render()`, not fixed at construction (see the class comment below). */\n offset: number;\n /** The index this slot currently wants to show, whether or not content has arrived — also how a later-resolving decode (`pending` below) finds the right slot. */\n assignedIndex: number | null;\n /** False from the moment a slot is (re)assigned a new index until that index's content has actually swapped in — see `isActiveReady()`. */\n ready: boolean;\n}\n\n/** A fully-built, ready-to-display node for a given item index — survives slot reassignment (see `cache` below), unlike `Slot.ready`, which resets the moment its slot moves on to a different index. */\ninterface CacheEntry {\n node: HTMLElement;\n item: GalleryItem;\n}\n\nexport interface SlideManagerOptions {\n preload: number;\n videoProviders: Map<string, VideoProviderRenderer>;\n}\n\n/**\n * DESIGN.md §2.3 — only `currentIndex ± preload` exist in the DOM. Content\n * already resident and ready is never moved between slots — the slot that\n * already holds it just gets its own `offset` relabeled to its new pool\n * position; only content resident nowhere yet gets built fresh into\n * whatever's left over. O(preload) re-render cost either way, but never a\n * DOM reparent — which matters for a provider video's `<iframe>` (§4.3):\n * most browsers reload an iframe moved to a new parent, so a live embed can\n * only safely change position by relabeling, never by being moved.\n */\nexport class SlideManager {\n readonly element: HTMLElement;\n private readonly slots: Slot[];\n private readonly preload: number;\n private readonly videoProviders: Map<string, VideoProviderRenderer>;\n private dragOffsetPx = 0;\n\n /** Ready nodes keyed by item index — trimmed to `centerIndex ± preload` each `render()`, same window the slots cover, so an evicted entry's video/iframe resources get released even if no slot ever reclaims it. */\n private readonly cache = new Map<number, CacheEntry>();\n\n /**\n * Image decodes in flight, keyed by item index, not by whichever slot\n * started them — a real bug this fixes: navigating before a preload\n * decode resolved used to reassign that slot, abandoning the decode and\n * starting a duplicate elsewhere, so a slow preload could never finish,\n * only restart. `reveal()` looks up which slot currently wants this\n * index at resolve time, so a reshuffled pool still lands it correctly.\n */\n private readonly pending = new Map<number, HTMLImageElement>();\n\n constructor(options: SlideManagerOptions) {\n this.element = document.createElement('div');\n this.element.className = 'shoji-slides';\n this.preload = options.preload;\n this.videoProviders = options.videoProviders;\n\n const count = options.preload * 2 + 1;\n this.slots = Array.from({ length: count }, (_, i) => {\n const offset = i - options.preload;\n const root = document.createElement('div');\n root.className = 'shoji-slide';\n\n const media = document.createElement('div');\n media.className = 'shoji-slide-media';\n root.appendChild(media);\n\n this.element.appendChild(root);\n return { root, media, offset, assignedIndex: null, ready: false };\n });\n this.applyTransforms(null);\n }\n\n /** The active slot's `.shoji-slide-media` — not `.shoji-slide`, whose transform is owned by pool-offset positioning. */\n getActiveMedia(): HTMLElement | null {\n return this.slots.find((slot) => slot.offset === 0)?.media ?? null;\n }\n\n /** False while the active slot's content is still decoding — Gallery.ts uses this right after `render()` to show the loading state immediately, without waiting for `onLoad`. */\n isActiveReady(): boolean {\n return this.slots.find((slot) => slot.offset === 0)?.ready ?? false;\n }\n\n /**\n * DESIGN.md §2.4 — live drag-to-navigate feedback: every slot's\n * structural `offset * 100%` position gets this same `px` added on top,\n * so dragging left/right visually slides the whole pool together (the\n * adjacent preloaded slot already exists at `±100%`) — no new DOM, no\n * content re-render mid-drag. `transition`: `null` during the live drag\n * (1:1 with the pointer); a real value only for the post-release settle\n * animation, reset back to `null` once that transition ends.\n */\n setDragOffset(px: number, transition: string | null): void {\n this.dragOffsetPx = px;\n this.applyTransforms(transition);\n }\n\n /** The pool slot's `.shoji-slide` root at a structural offset — what the drag settle animation (§2.4) waits for `transitionend` on. */\n getSlotRoot(offset: number): HTMLElement | null {\n return this.slots.find((slot) => slot.offset === offset)?.root ?? null;\n }\n\n private applyTransforms(transition: string | null): void {\n for (const slot of this.slots) {\n slot.root.style.transition = transition ?? '';\n slot.root.style.transform = `translateX(calc(${slot.offset * 100}% + ${this.dragOffsetPx}px))`;\n }\n }\n\n /** Re-renders whichever slots need a different item; `onLoad` fires per index once its media settles. `openPlaceholderSrc` (only from `Gallery.open()`) swaps the centerIndex slot's spinner for a low-res placeholder once it decodes, if not already ready. `loop` matches `Gallery.ts`'s own `nextIndex()`/`prevIndex()` wrap-around — without it, the pool slot just past the last (or before the first) item has no item to preload at all, so a boundary drag reveals nothing even though the completed navigation itself already wraps correctly. */\n render(\n items: readonly GalleryItem[],\n centerIndex: number,\n onLoad: (index: number) => void,\n openPlaceholderSrc?: string,\n loop = false,\n ): void {\n // A real bug found wiring this up, in two parts:\n // 1. Wrapping is only safe from an *index-collision* standpoint when\n // there are enough real items to fill the whole pool without\n // revisiting one twice. `items.length === this.slots.length`\n // (`2*preload + 1`) looks like exactly enough — but that's the\n // degenerate case where the pool *is* the whole gallery: the \"prev\"\n // slot at the first item and the \"next\" slot at the last item both\n // wrap to the *other* end, making the first and last items adjacent\n // in the pool even though nothing actually collides on one index.\n // Confirmed directly, reproduced in CI: a real fixture built\n // specifically to keep two provider-video items apart (never within\n // `preload` of each other, so at most one `.shoji-slide-provider-\n // video` is ever resident) broke exactly this way once its\n // `demo/assets/`-dependent photo tiles were empty (gitignored, so a\n // fresh CI checkout has none) — collapsing it down to precisely\n // `items.length === this.slots.length`, wrapping the two video items\n // into each other's pool despite the fixture's own spacer tile still\n // sitting between them in the array. Requiring *strictly* more items\n // than pool slots (`>`, not `>=`) is what actually guarantees the\n // first and last items are never both in the wrapped pool at once.\n // 2. Below that threshold, an out-of-range offset just shows nothing at\n // that edge, same as `loop: false` — correct for a gallery too small\n // to usefully \"loop\" through anyway.\n const canWrap = loop && items.length > this.slots.length;\n const targets = this.slots.map((_, i) => {\n const offset = i - this.preload;\n const rawIndex = centerIndex + offset;\n const index = canWrap ? ((rawIndex % items.length) + items.length) % items.length : rawIndex;\n const item = index >= 0 && index < items.length ? items[index] : undefined;\n return { offset, index, item };\n });\n\n // Trim the cache to the same window the slots above now cover — the\n // *wrapped* set of indices, not a raw numeric range, matching `loop`;\n // a raw-range check would otherwise immediately evict a just-cached\n // wrapped entry (e.g. index 0, preloaded one slot past the last item)\n // since it falls far outside `centerIndex ± preload` as plain numbers.\n // Releases a dropped video here rather than just letting the reference\n // disappear (which would leave it paused-in-place, never released).\n const windowIndices = new Set(targets.map((t) => t.index));\n for (const [index, entry] of this.cache) {\n if (!windowIndices.has(index)) {\n releaseVideoNode(entry.node);\n this.cache.delete(index);\n }\n }\n\n // Phase 1 — claim in place, before anything gets destructively\n // released. A target already resident and ready in *some* slot (not\n // necessarily the one whose own offset happens to match) stays exactly\n // where it is — only that slot's `offset` moves (see the class comment\n // above). Still mid-decode elsewhere isn't claimed here (no `ready`\n // slot yet) — falls through to phase 2, where `ensureImageDecoding`'s\n // in-flight guard reconciles it once the one real decode resolves.\n const claimedIndices = new Set<number>();\n const claimedSlots = new Set<Slot>();\n for (const target of targets) {\n if (!target.item) continue;\n const owner = this.slots.find((s) => s.assignedIndex === target.index && s.ready);\n if (!owner) continue;\n claimedIndices.add(target.index);\n claimedSlots.add(owner);\n if (owner.offset === target.offset) continue; // already exactly right, nothing to do\n owner.offset = target.offset;\n onLoad(target.index);\n }\n\n // Phase 2 — whatever's left: slots phase 1 didn't claim are free to\n // clear (out-of-range target) or take on fresh content (a genuinely\n // new index, or one already mid-decode elsewhere).\n const freeSlots = this.slots.filter((s) => !claimedSlots.has(s));\n const freeTargets = targets.filter((t) => !t.item || !claimedIndices.has(t.index));\n\n freeTargets.forEach((target, i) => {\n const slot = freeSlots[i]!;\n slot.offset = target.offset;\n const { index, item } = target;\n\n if (!item) {\n slot.assignedIndex = null;\n slot.ready = false;\n releaseVideo(slot.media);\n slot.media.replaceChildren();\n return;\n }\n if (slot.assignedIndex === index) return; // unchanged — a decode already in flight here\n\n slot.assignedIndex = index;\n slot.ready = false;\n releaseVideo(slot.media);\n slot.media.replaceChildren(createSpinner());\n // Early, not just from swapIn/moveIn/onReady below — a provider\n // video's poster (shown synchronously, well before onReady) needs\n // --shoji-provider-video-aspect (shoji.css) set before it ever\n // paints, or it always letterboxes to the 16:9 fallback regardless of\n // the item's own declared shape.\n this.applyAspect(slot, item);\n if (index === centerIndex && openPlaceholderSrc) {\n const naturalSize =\n item.width && item.height ? { width: item.width, height: item.height } : undefined;\n this.revealOpenPlaceholder(openPlaceholderSrc, slot, index, naturalSize);\n }\n\n if (item.video?.provider === 'html5') {\n this.renderVideo(item, slot, index, onLoad);\n } else if (item.video) {\n this.renderProviderVideo(item, slot, index, onLoad);\n } else {\n this.ensureImageDecoding(item, index, onLoad); // no-ops if index is already being decoded elsewhere\n }\n });\n\n // Reflect any relabeling above immediately, never animated — masked\n // either by SlideTransition's own ghost-clone crossfade, or by landing\n // exactly on a completed drag's already-settled position (§2.4);\n // animating this too would fight both.\n this.applyTransforms(null);\n }\n\n private applyAspect(slot: Slot, item: GalleryItem): void {\n if (item.width && item.height) {\n slot.media.style.aspectRatio = `${item.width} / ${item.height}`;\n } else {\n slot.media.style.removeProperty('aspect-ratio');\n }\n // A provider video's real shape (YouTube/Vimeo iframe, via\n // shoji.css's cqw/cqh sizing) — kept separate from the aspect-ratio\n // above since that alone can't shape a non-replaced element like an\n // iframe/div (see shoji.css's own comment on this). Only set for a\n // video item: harmless either way, but meaningless for a photo, whose\n // own object-fit: contain already uses its real decoded dimensions\n // directly.\n if (item.video && item.width && item.height) {\n slot.media.style.setProperty(\n '--shoji-provider-video-aspect',\n `calc(${item.width} / ${item.height})`,\n );\n } else {\n slot.media.style.removeProperty('--shoji-provider-video-aspect');\n }\n }\n\n /** Releases the slot's old content and swaps the new node in, caching it. `ensureImageDecoding` goes through `moveIn` instead. */\n private swapIn(slot: Slot, node: HTMLElement, item: GalleryItem, index: number): void {\n releaseVideo(slot.media);\n this.applyAspect(slot, item);\n slot.media.replaceChildren(node);\n slot.ready = true;\n this.cache.set(index, { node, item });\n }\n\n /**\n * Inserts a just-decoded node into the slot waiting on it — skips\n * `releaseVideo`, unlike `swapIn`: this is its first insertion anywhere,\n * nothing outgoing to release. `render()`'s claim pass (Phase 1) never\n * calls this — it reuses `ready` content in place, no DOM touch at all.\n */\n private moveIn(slot: Slot, entry: CacheEntry, index: number): void {\n this.applyAspect(slot, entry.item);\n slot.media.replaceChildren(entry.node);\n slot.ready = true;\n this.cache.set(index, entry);\n }\n\n /** Starts decoding `item` for `index`, only if nothing already is (see `pending`). Resolves by looking up whichever slot currently wants this index, not the one active when the decode started. */\n private ensureImageDecoding(\n item: GalleryItem,\n index: number,\n onLoad: (index: number) => void,\n ): void {\n if (this.pending.has(index)) return;\n\n const img = document.createElement('img');\n img.className = 'shoji-slide-img';\n img.alt = item.alt ?? '';\n // A real bug: <img> is natively draggable by default in every browser\n // (drag-to-save-elsewhere). Left on, a real drag gesture — navigate,\n // vertical-close, or the zoom plugin's own pan — can lose the race to\n // the browser's own \"start a native image drag\" recognition, which\n // fires pointercancel and hands the interaction off to dragstart/drag\n // instead: the gesture appears to move once, then stop dead, since\n // every pointer event after the cancel is simply never dispatched here\n // again. Disabling native drag makes Pointer Events own the gesture\n // unconditionally.\n img.draggable = false;\n // The one image the viewer is actually waiting on — hint the browser to\n // schedule it ahead of any lower-priority background fetches contending\n // for the same connection pool.\n if ('fetchPriority' in img) img.fetchPriority = 'high';\n if (item.srcset) img.srcset = item.srcset;\n if (item.sizes) img.sizes = item.sizes;\n img.src = item.src;\n this.pending.set(index, img);\n\n const reveal = (): void => {\n this.pending.delete(index);\n const entry: CacheEntry = { node: img, item };\n this.cache.set(index, entry);\n const slot = this.slots.find((s) => s.assignedIndex === index);\n if (!slot) return; // nobody currently wants it anymore\n this.moveIn(slot, entry, index);\n onLoad(index);\n };\n\n if (typeof img.decode === 'function') {\n img.decode().then(reveal, reveal);\n } else {\n img.addEventListener('load', reveal, { once: true });\n img.addEventListener('error', reveal, { once: true });\n }\n }\n\n /**\n * DESIGN.md §2.3 — swaps the spinner for the placeholder once *it*\n * decodes, not immediately: `item.thumb` is often just `item.src` again,\n * so an undecoded placeholder can leave as long a blank gap as the\n * spinner it replaces.\n *\n * A real bug: `.shoji-slide-open-placeholder`'s CSS unconditionally\n * force-fills the frame (deliberate default: a real photo is usually\n * bigger than the dialog). Wrong for a genuinely small photo with known\n * `item.width`/`item.height` — same \"grows too big, snaps down once real\"\n * symptom the zoom-in transition itself had (§2.3b), but that fix only\n * governs the animated *transform*, not this placeholder's own CSS box\n * once it settles. When `naturalSize` is known, sizes the placeholder\n * explicitly instead, reusing `containedBox` (`zoomTransition.ts`) —\n * unknown-size items keep the original force-fill guess.\n *\n * A second real bug in that same fix: measured `slot.media`'s own rect as\n * \"the container\" — but `slot.media` is the exact element the zoom-in\n * transition (§2.3b) animates a `scale()` transform on. `getBoundingClientRect()`\n * reflects whatever transform is *currently* applied, and the placeholder's\n * own decode typically resolves before that ~300ms animation settles — so\n * this measured a still-small, mid-animation rect (often the origin\n * thumbnail's own tiny size) instead of the real dialog size. `slot.root`\n * (`.shoji-slide`) fixes it: same box, only ever `translateX`'d for\n * pool-offset positioning, never scaled — a stable read regardless of\n * what its child is mid-animation through.\n */\n private revealOpenPlaceholder(\n src: string,\n slot: Slot,\n index: number,\n naturalSize?: { width: number; height: number },\n ): void {\n const img = createOpenPlaceholder(src);\n const reveal = (): void => {\n if (slot.assignedIndex !== index || slot.ready) return; // stale, or the real content already won the race\n // Only loading indicators, not slot.media wholesale — a provider video\n // may have already appended its own (still-hidden) container alongside\n // it. A real bug: this only ever cleared the spinner, not a\n // `.shoji-slide-provider-poster` a provider video's `item.poster`\n // (`showProviderPoster`, below) may have *already* synchronously\n // inserted by the time this async decode resolves — leaving both\n // visible at once, the poster at its own small natural size next to\n // this placeholder's explicitly-computed large box.\n slot.media\n .querySelectorAll('.shoji-slide-spinner, .shoji-slide-provider-poster')\n .forEach((el) => el.remove());\n slot.media.appendChild(img);\n if (naturalSize) {\n const containerRect = slot.root.getBoundingClientRect();\n const box = containedBox(\n { left: 0, top: 0, width: containerRect.width, height: containerRect.height },\n naturalSize.width / naturalSize.height,\n naturalSize,\n );\n img.style.width = `${box.width}px`;\n img.style.height = `${box.height}px`;\n }\n };\n if (typeof img.decode === 'function') {\n img.decode().then(reveal, reveal);\n } else {\n img.addEventListener('load', reveal, { once: true });\n img.addEventListener('error', reveal, { once: true });\n }\n }\n\n /** Native `<video controls>` — real playback, not just a poster. Not autoplayed/muted: a deliberate click-to-play, not a background loop. */\n private renderVideo(\n item: GalleryItem,\n slot: Slot,\n index: number,\n onLoad: (index: number) => void,\n ): void {\n const hasSource = Boolean(item.src) || Boolean(item.sources && item.sources.length > 0);\n if (!hasSource) {\n const placeholder = document.createElement('div');\n placeholder.className = 'shoji-slide-placeholder';\n placeholder.textContent = 'Video';\n this.swapIn(slot, placeholder, item, index);\n onLoad(index);\n return;\n }\n\n const video = document.createElement('video');\n video.className = 'shoji-slide-video';\n video.controls = true;\n video.playsInline = true;\n // Same real bug as the image path's own `img.draggable = false` above\n // (`ensureImageDecoding()`), never fixed here since no e2e test\n // previously drove a real mouse drag over an actual native\n // `<video controls>` element to catch it: a `<video>` is natively\n // draggable by default too (drag-to-save-elsewhere), and left on, a\n // real drag gesture (navigate, vertical-close) can lose the race to the\n // browser's own \"start a native media drag\" recognition — it fires\n // `pointercancel` and hands the interaction to `dragstart`/`drag`\n // instead, so the gesture appears to move once, then stop dead, since\n // every pointer event after the cancel is simply never dispatched here\n // again. Confirmed directly: reproduced consistently (every browser\n // project) for a horizontal drag specifically, though the same\n // mechanism can affect vertical too. Disabling native drag makes\n // Pointer Events own the gesture unconditionally, same as for photos.\n video.draggable = false;\n if (item.poster) video.poster = item.poster;\n // Before `loadedmetadata`, a <video> has no real intrinsic size to\n // contain itself against (object-fit: contain, above) — the UA default\n // (300x150) until then, visibly jumping to the real shape once it\n // loads. item.width/item.height (already known up front, unlike a\n // photo's real dimensions) sidesteps that: same shape from the first\n // frame the poster shows, no jump once metadata actually arrives.\n if (item.width && item.height) {\n video.style.aspectRatio = `${item.width} / ${item.height}`;\n }\n\n if (item.sources && item.sources.length > 0) {\n for (const s of item.sources) {\n const source = document.createElement('source');\n source.src = s.src;\n source.type = s.type;\n video.appendChild(source);\n }\n } else {\n video.src = item.src;\n }\n\n // Inserted immediately (unlike the image path above) — the native\n // `poster` attribute already shows something meaningful without\n // waiting on `loadedmetadata`, so there's no decode-style gap to close\n // here; deferring would only make video slides slower to show anything.\n this.swapIn(slot, video, item, index);\n const reveal = (): void => onLoad(index);\n video.addEventListener('loadedmetadata', reveal, { once: true });\n video.addEventListener('error', reveal, { once: true });\n }\n\n /** DESIGN.md §4-video. Unregistered provider → same placeholder as no-source video. */\n private renderProviderVideo(\n item: GalleryItem,\n slot: Slot,\n index: number,\n onLoad: (index: number) => void,\n ): void {\n const video = item.video!;\n const renderFn =\n video.provider === 'custom' ? video.render : this.videoProviders.get(video.provider);\n\n if (!renderFn) {\n const placeholder = document.createElement('div');\n placeholder.className = 'shoji-slide-placeholder';\n placeholder.textContent = 'Video';\n this.swapIn(slot, placeholder, item, index);\n onLoad(index);\n return;\n }\n\n // DESIGN.md §4.3 — shows item.poster immediately, in place of the\n // generic spinner Phase 2 already put in slot.media, if the host\n // supplied one; same idea as native <video poster>, just applied to a\n // provider embed instead. A renderer without a host-supplied poster can\n // still resolve a fallback of its own via the setPoster callback below.\n if (item.poster) showProviderPoster(slot, item.poster);\n\n const container = document.createElement('div');\n container.className = 'shoji-slide-provider-video';\n container.hidden = true;\n const controller = new AbortController();\n providerAbortControllers.set(container, controller);\n\n // Unlike an <img> decode, an iframe embed only actually loads once\n // attached to the live document — attach now, hidden behind the\n // still-visible spinner (or poster, above), instead of deferring\n // attachment until ready.\n slot.media.appendChild(container);\n\n let revealed = false;\n const onReady = (): void => {\n if (revealed || controller.signal.aborted) return;\n revealed = true;\n container.hidden = false;\n // Whichever loading indicator(s) — Phase 2's spinner, a poster, an\n // open placeholder — are still sitting behind the now-visible embed,\n // never the embed itself. A real bug: a host-supplied `item.poster`\n // (`showProviderPoster`, above) and `Gallery.open()`'s own low-res\n // placeholder (`revealOpenPlaceholder`, below) are added independently\n // of each other and neither replaces the other, so *both* can be\n // sitting in `slot.media` at once by the time this runs.\n // `querySelector(...).remove()` only ever removed the first match in\n // DOM order, permanently leaking whichever indicator lost that\n // race — visible as the leaked one's image bleeding through the\n // transparent letterboxing gaps around a provider embed that isn't\n // exactly the container's own aspect ratio.\n slot.media\n .querySelectorAll(\n '.shoji-slide-spinner, .shoji-slide-open-placeholder, .shoji-slide-provider-poster',\n )\n .forEach((el) => el.remove());\n this.applyAspect(slot, item);\n slot.ready = true;\n this.cache.set(index, { node: container, item });\n onLoad(index);\n };\n const setPoster = (url: string): void => {\n // A host-supplied poster (above) always wins over a renderer's own\n // fallback — and once the real embed is showing, or this slide's\n // moved on, a fallback resolving late (e.g. an async fetch) must\n // never replace or paint over it.\n if (item.poster || revealed || controller.signal.aborted) return;\n showProviderPoster(slot, url);\n };\n renderFn(container, item, onReady, controller.signal, setPoster);\n }\n\n destroy(): void {\n for (const slot of this.slots) {\n releaseVideo(slot.media);\n slot.media.replaceChildren();\n slot.assignedIndex = null; // any decode still in `pending` correctly finds no slot wanting it once it resolves\n slot.ready = false;\n }\n this.cache.clear();\n this.pending.clear();\n this.element.remove();\n }\n}\n\n/** DESIGN.md §4.3 — a provider video's loading placeholder, host-supplied (`item.poster`) or a renderer's own fallback (`setPoster`) — replaces whichever generic indicator (spinner, or an earlier poster call) is currently in `slot.media`, same \"only one loading indicator at a time\" rule `createOpenPlaceholder`'s own callers already follow. Reuses `.shoji-slide-img`'s sizing, same reasoning as that function. */\nfunction showProviderPoster(slot: Slot, url: string): void {\n // A real bug: this only ever cleared the spinner/a previous poster, never\n // an already-shown `.shoji-slide-open-placeholder` (`revealOpenPlaceholder`,\n // above) — that placeholder's async image decode can resolve *before*\n // this (e.g. `setPoster`'s fallback, resolved later than the initial\n // synchronous poster call), leaving both visible at once.\n slot.media\n .querySelectorAll(\n '.shoji-slide-spinner, .shoji-slide-open-placeholder, .shoji-slide-provider-poster',\n )\n .forEach((el) => el.remove());\n const img = document.createElement('img');\n img.className = 'shoji-slide-img shoji-slide-provider-poster';\n img.alt = '';\n img.draggable = false;\n img.src = url;\n slot.media.appendChild(img);\n}\n\n/** DESIGN.md §2.3 — low-res open() placeholder; reuses .shoji-slide-img's sizing so the zoom-in FLIP measures it like real content (zoomTransition.ts's effectiveTargetBox only special-cases the spinner as non-content). */\nfunction createOpenPlaceholder(src: string): HTMLImageElement {\n const img = document.createElement('img');\n img.className = 'shoji-slide-img shoji-slide-open-placeholder';\n img.alt = '';\n img.draggable = false;\n img.src = src;\n return img;\n}\n\n/** DESIGN.md §2.3 — shown in a slot while its content is still decoding/loading, replacing whatever a stale \"keep the old image visible\" approach would have left there (a previous version of this did that; it read as broken, not helpful, since the old image no longer matches the caption/counter/URL that already moved on). Purely CSS-animated (a rotating ring via `border`), no JS-driven layout. */\nfunction createSpinner(): HTMLElement {\n const spinner = document.createElement('div');\n spinner.className = 'shoji-slide-spinner';\n spinner.setAttribute('aria-hidden', 'true');\n return spinner;\n}\n\nconst providerAbortControllers = new WeakMap<HTMLElement, AbortController>();\n\n/** Pause/release a slot's previous <video> or provider content, if any. */\nfunction releaseVideo(media: HTMLElement): void {\n const video = media.querySelector('video');\n if (video) releaseVideoNode(video);\n const provider = media.querySelector<HTMLElement>('.shoji-slide-provider-video');\n if (provider) releaseProviderNode(provider);\n}\n\n/**\n * Pauses whatever's playing in `media` — native `<video>`, or a provider\n * embed wired via `wirePlayableContract` (§4-video) — without\n * releasing/destroying it, unlike `releaseVideo` above. `Gallery` calls\n * this on `close()` and before every navigation, on the outgoing slide.\n */\nexport function pauseMedia(media: HTMLElement | null): void {\n const video = media?.querySelector('video');\n if (video) {\n video.pause();\n return;\n }\n const provider = media?.querySelector<HTMLElement & { pause?: () => void }>(\n '.shoji-slide-provider-video',\n );\n if (provider && typeof provider.pause === 'function') provider.pause();\n}\n\n/** Cache-eviction counterpart — the node itself might *be* the content. */\nfunction releaseVideoNode(node: HTMLElement): void {\n if (node.tagName !== 'VIDEO') {\n releaseProviderNode(node);\n return;\n }\n const video = node as HTMLVideoElement;\n video.pause();\n video.removeAttribute('src');\n video.load();\n}\n\nfunction releaseProviderNode(node: HTMLElement): void {\n const controller = providerAbortControllers.get(node);\n if (!controller) return;\n providerAbortControllers.delete(node);\n controller.abort();\n}\n","/**\n * DESIGN.md §2.5 — built-in presets, generated from a small config→transform\n * DSL rather than ~20 hand-written keyframe functions. `enter(direction)` is\n * where the incoming slide starts (then transitions to natural: `transform:\n * none; opacity: 1`); `leave(direction)` is where the outgoing clone (see\n * `SlideTransition.ts`) ends up (starts at natural, transitions to this).\n * `direction` is `1` for next()-style, `-1` for prev()-style — presets that\n * don't care (fade, zoom, flipX, deck, drop...) never read it. transform/\n * opacity only, per CLAUDE.md's hardware-accelerated-CSS3 constraint.\n */\n\nexport interface TransitionState {\n transform: string;\n opacity: number;\n}\n\nexport interface TransitionPreset {\n name: string;\n enter(direction: 1 | -1): TransitionState;\n leave(direction: 1 | -1): TransitionState;\n}\n\n/** One end of one preset. `directional: true` multiplies translateX/Y/rotateZ/Y by nav direction (mirror-image next vs prev — the \"slide\" family); omitted means the same fixed transform either way (the \"fade\" family — fadeUp always rises). */\ninterface AxisConfig {\n directional?: boolean;\n translateX?: number; // %\n translateY?: number; // %\n scale?: number;\n rotateX?: number; // deg, never direction-multiplied\n rotateY?: number; // deg\n rotateZ?: number; // deg\n perspective?: number; // px, needed alongside rotateX/Y for real 3D depth\n opacity?: number; // default 1\n}\n\nfunction resolveAxis(config: AxisConfig, direction: 1 | -1): TransitionState {\n const sign = config.directional ? direction : 1;\n const parts: string[] = [];\n if (config.perspective) parts.push(`perspective(${config.perspective}px)`);\n const tx = config.translateX !== undefined ? config.translateX * sign : 0;\n const ty = config.translateY ?? 0;\n if (tx !== 0 || ty !== 0) parts.push(`translate(${tx}%, ${ty}%)`);\n if (config.rotateY !== undefined) parts.push(`rotateY(${config.rotateY * sign}deg)`);\n if (config.rotateX !== undefined) parts.push(`rotateX(${config.rotateX}deg)`);\n if (config.rotateZ !== undefined) parts.push(`rotateZ(${config.rotateZ * sign}deg)`);\n if (config.scale !== undefined) parts.push(`scale(${config.scale})`);\n return { transform: parts.length > 0 ? parts.join(' ') : 'none', opacity: config.opacity ?? 1 };\n}\n\ninterface PresetSpec {\n name: string;\n enter: AxisConfig;\n leave: AxisConfig;\n}\n\nfunction preset(spec: PresetSpec): TransitionPreset {\n return {\n name: spec.name,\n enter: (direction) => resolveAxis(spec.enter, direction),\n leave: (direction) => resolveAxis(spec.leave, direction),\n };\n}\n\nconst PRESET_LIST: TransitionPreset[] = [\n // The four DESIGN.md calls out by name: default mode, plus fade/zoom/deck.\n preset({\n name: 'slide',\n enter: { directional: true, translateX: 100 },\n leave: { directional: true, translateX: -100 },\n }),\n preset({\n name: 'fade',\n enter: { opacity: 0 },\n leave: { opacity: 0 },\n }),\n preset({\n name: 'zoom',\n enter: { scale: 0.6, opacity: 0 },\n leave: { scale: 1.4, opacity: 0 },\n }),\n preset({\n // \"lg-style deck\": the incoming slide drops into place from slightly\n // above/small while the outgoing one recedes back and fades, like a\n // card being placed on top of a deck — not direction-dependent, a deck\n // doesn't care whether the card came from the left or right.\n name: 'deck',\n enter: { translateY: 6, scale: 0.85, opacity: 0 },\n leave: { translateY: -4, scale: 0.92, opacity: 0 },\n }),\n\n // Positional siblings of `slide`.\n preset({\n name: 'slideVertical',\n enter: { directional: true, translateY: 100 },\n leave: { directional: true, translateY: -100 },\n }),\n preset({\n name: 'push',\n // Same geometry as `slide` but travels further (120% vs 100%) and with\n // no cross-fade at all — a harder, snappier \"shove it off\" feel.\n enter: { directional: true, translateX: 120 },\n leave: { directional: true, translateX: -120 },\n }),\n\n // Fixed-direction fades (not nav-direction-aware — a named entrance style).\n preset({\n name: 'fadeUp',\n enter: { translateY: 12, opacity: 0 },\n leave: { translateY: -12, opacity: 0 },\n }),\n preset({\n name: 'fadeDown',\n enter: { translateY: -12, opacity: 0 },\n leave: { translateY: 12, opacity: 0 },\n }),\n preset({\n name: 'fadeLeft',\n enter: { translateX: -12, opacity: 0 },\n leave: { translateX: 12, opacity: 0 },\n }),\n preset({\n name: 'fadeRight',\n enter: { translateX: 12, opacity: 0 },\n leave: { translateX: -12, opacity: 0 },\n }),\n\n // Zoom family.\n preset({ name: 'zoomOut', enter: { scale: 1.4, opacity: 0 }, leave: { scale: 0.6, opacity: 0 } }),\n preset({ name: 'scaleUp', enter: { scale: 0.7 }, leave: { scale: 1.3 } }),\n preset({ name: 'scaleDown', enter: { scale: 1.3 }, leave: { scale: 0.7 } }),\n\n // Rotation family — `rotate` is fixed-direction; `rotateLeft`/`rotateRight`\n // tie their spin to nav direction instead.\n preset({\n name: 'rotate',\n enter: { rotateZ: 15, opacity: 0 },\n leave: { rotateZ: -15, opacity: 0 },\n }),\n preset({\n name: 'rotateLeft',\n enter: { directional: true, rotateZ: -25, opacity: 0 },\n leave: { directional: true, rotateZ: 25, opacity: 0 },\n }),\n preset({\n name: 'rotateRight',\n enter: { directional: true, rotateZ: 25, opacity: 0 },\n leave: { directional: true, rotateZ: -25, opacity: 0 },\n }),\n\n // 3D family — needs `perspective()` in the same transform for real depth.\n preset({\n name: 'flipX',\n enter: { rotateX: 90, perspective: 1200, opacity: 0 },\n leave: { rotateX: -90, perspective: 1200, opacity: 0 },\n }),\n preset({\n name: 'flipY',\n enter: { directional: true, rotateY: 90, perspective: 1200, opacity: 0 },\n leave: { directional: true, rotateY: -90, perspective: 1200, opacity: 0 },\n }),\n preset({\n name: 'cube',\n enter: { directional: true, rotateY: 70, translateX: 40, perspective: 1600, opacity: 1 },\n leave: { directional: true, rotateY: -70, translateX: -40, perspective: 1600, opacity: 1 },\n }),\n preset({\n name: 'coverflow',\n enter: {\n directional: true,\n rotateY: 35,\n translateX: 60,\n scale: 0.8,\n perspective: 1400,\n opacity: 0,\n },\n leave: {\n directional: true,\n rotateY: -35,\n translateX: -60,\n scale: 0.8,\n perspective: 1400,\n opacity: 0,\n },\n }),\n\n // Fixed-direction drop.\n preset({\n name: 'drop',\n enter: { translateY: -30, opacity: 0 },\n leave: { translateY: 20, opacity: 0 },\n }),\n];\n\n/** Keyed by name for `Gallery`'s `mode` lookup — an unrecognized string is treated as a custom CSS-class-pair mode instead (see `SlideTransition.animateCustom`), not an error. */\nexport const TRANSITION_PRESETS: Readonly<Record<string, TransitionPreset>> = Object.fromEntries(\n PRESET_LIST.map((p) => [p.name, p]),\n);\n","import type { SlideManager } from '../core/SlideManager';\nimport type { TransitionPreset } from './presets';\n\nfunction prefersReducedMotion(): boolean {\n return (\n typeof window.matchMedia === 'function' &&\n window.matchMedia('(prefers-reduced-motion: reduce)').matches\n );\n}\n\n/** `\"300ms\"` / `\"0.3s\"` → milliseconds — same parser as `zoomTransition.ts`, duplicated rather than shared: two independent transition subsystems that happen to share a timing format. */\nfunction parseCssTime(value: string): number {\n const first = value.split(',')[0]?.trim() ?? '';\n if (first.endsWith('ms')) return parseFloat(first);\n if (first.endsWith('s')) return parseFloat(first) * 1000;\n return 0;\n}\n\n/** Waits for `transitionend` *or* `animationend` (with a timeout fallback) then calls `cb` once — the custom CSS-class-pair path (§2.5) may use either, since \"no JS needed\" for the host means Shoji can't assume which. */\nfunction waitForEnd(el: HTMLElement, cb: () => void): void {\n const style = getComputedStyle(el);\n const durationMs = Math.max(\n parseCssTime(style.transitionDuration),\n parseCssTime(style.animationDuration),\n );\n let done = false;\n const finish = (): void => {\n if (done) return;\n done = true;\n el.removeEventListener('transitionend', onEnd);\n el.removeEventListener('animationend', onEnd);\n cb();\n };\n const onEnd = (event: Event): void => {\n if (event.target === el) finish();\n };\n el.addEventListener('transitionend', onEnd);\n el.addEventListener('animationend', onEnd);\n setTimeout(finish, durationMs + 100);\n}\n\n/** Incoming slide: instant jump to `(transform, opacity)` (same FLIP trick as `zoomTransition.ts`'s `zoomIn`), then transitions to natural. */\nfunction applyEnter(el: HTMLElement, transform: string, opacity: number): void {\n el.style.transition = 'none';\n el.style.transformOrigin = 'center';\n el.style.transform = transform;\n el.style.opacity = String(opacity);\n void el.offsetHeight; // force the instant jump to commit before transitioning away from it\n el.style.transition =\n 'transform var(--shoji-duration) var(--shoji-easing), opacity var(--shoji-duration) var(--shoji-easing)';\n el.style.transform = 'none';\n el.style.opacity = '1';\n}\n\n/** Outgoing ghost: already sits at natural (a fresh clone has no inline transform yet), so it transitions the other way — natural → `(transform, opacity)`, mirroring `zoomOut`'s shape. */\nfunction applyLeave(el: HTMLElement, transform: string, opacity: number): void {\n el.style.transformOrigin = 'center';\n el.style.transition =\n 'transform var(--shoji-duration) var(--shoji-easing), opacity var(--shoji-duration) var(--shoji-easing)';\n void el.offsetHeight;\n el.style.transform = transform;\n el.style.opacity = String(opacity);\n}\n\nfunction clearInlineTransform(el: HTMLElement): void {\n el.style.transition = '';\n el.style.transform = '';\n el.style.opacity = '';\n el.style.transformOrigin = '';\n}\n\n/**\n * DESIGN.md §2.5 — animates a slide-to-slide navigation via a temporary\n * \"ghost\": a clone of the outgoing `.shoji-slide-media`, positioned over\n * the real slot, playing the preset's `leave` keyframe while `swapContent`\n * (the real `SlideManager.render()` reassignment) runs invisibly underneath\n * it. The newly-swapped-in real active media then plays `enter`. Both\n * animate concurrently.\n *\n * Deliberately separate from the gesture engine's own live-drag/settle\n * animation (`GestureController.ts`, §2.4): `mode` is a discrete \"jump\" for\n * programmatic navigation, not layered onto a continuous drag — a completed\n * swipe always uses its own pool-shift regardless of `mode`. See\n * `Gallery.navigate()`'s `animate` flag.\n */\nexport class SlideTransition {\n constructor(\n private readonly slides: SlideManager,\n private readonly decorateLeave: (clonedMedia: HTMLElement) => () => void = () => () => {},\n ) {}\n\n /** A recognized built-in preset (`TRANSITION_PRESETS`). */\n animate(preset: TransitionPreset, direction: 1 | -1, swapContent: () => void): void {\n this.run(swapContent, (ghost, incoming) => {\n const leave = preset.leave(direction);\n applyLeave(ghost, leave.transform, leave.opacity);\n const enter = preset.enter(direction);\n applyEnter(incoming, enter.transform, enter.opacity);\n });\n }\n\n /** An unrecognized `mode` string: a host-supplied CSS class pair (`shoji-transition-<mode>-leave`/`-enter`), not an error — §2.5's \"custom animation = a CSS class pair, no JS needed.\" */\n animateCustom(mode: string, direction: 1 | -1, swapContent: () => void): void {\n const dirAttr = direction === 1 ? 'next' : 'prev';\n this.run(\n swapContent,\n (ghost, incoming) => {\n ghost.dataset.shojiDirection = dirAttr;\n ghost.classList.add(`shoji-transition-${mode}-leave`);\n incoming.dataset.shojiDirection = dirAttr;\n incoming.classList.add(`shoji-transition-${mode}-enter`);\n },\n (incoming) => {\n incoming.classList.remove(`shoji-transition-${mode}-enter`);\n delete incoming.dataset.shojiDirection;\n },\n );\n }\n\n private run(\n swapContent: () => void,\n kickOff: (ghost: HTMLElement, incoming: HTMLElement) => void,\n cleanupIncoming: (incoming: HTMLElement) => void = clearInlineTransform,\n ): void {\n if (prefersReducedMotion()) {\n swapContent();\n return;\n }\n const outgoing = this.slides.getActiveMedia();\n if (!outgoing) {\n swapContent();\n return;\n }\n\n const ghost = document.createElement('div');\n ghost.className = 'shoji-slide-ghost';\n const clonedMedia = ghost.appendChild(outgoing.cloneNode(true) as HTMLElement);\n this.slides.element.appendChild(ghost);\n // DESIGN.md §2.5/§4.5 — plugins with a per-slide visual override (e.g.\n // RotateFlip's rotate/flip, Zoom's scale/pan) freeze it onto the clone\n // here, before the reflow below commits it as the leave animation's\n // start — the real node was already reset by now, so the clone alone\n // wouldn't otherwise carry it. `settleLeave` (called after `kickOff`\n // below) is what starts each one transitioning back to neutral.\n const settleLeave = this.decorateLeave(clonedMedia);\n void ghost.offsetHeight; // commit the ghost's (and clone's) starting appearance before anything changes it\n\n swapContent();\n\n const incoming = this.slides.getActiveMedia();\n if (!incoming) {\n ghost.remove();\n return;\n }\n\n kickOff(ghost, incoming);\n settleLeave();\n waitForEnd(ghost, () => ghost.remove());\n waitForEnd(incoming, () => cleanupIncoming(incoming));\n }\n}\n","import {\n lockBodyScroll,\n unlockBodyScroll,\n markIntentionalScroll as markIntentionalScrollInternal,\n} from './bodyScrollLock';\nimport { EventBus, type Unsubscribe } from './EventBus';\nimport { buildLightboxDom, type LightboxDom } from './dom';\nimport { FocusTrap } from './FocusTrap';\nimport { GestureController, INTERACTIVE_CONTROL_SELECTOR } from './GestureController';\nimport { LiveRegion } from './LiveRegion';\nimport type { ButtonSpec, PluginContext, ShojiPlugin, VideoProviderRenderer } from './plugin';\nimport { DEFAULT_SELECTOR, resolveDynamicVideoItems, scanContainer } from './scan';\nimport { pauseMedia, SlideManager } from './SlideManager';\nimport type {\n DangerousHtmlCaption,\n GalleryEvents,\n GalleryItem,\n GalleryItemInput,\n GalleryOptions,\n} from './types';\nimport { TRANSITION_PRESETS } from '../transitions/presets';\nimport { SlideTransition } from '../transitions/SlideTransition';\nimport { zoomIn, zoomOut, type Box, type FrozenDragTransform } from './zoomTransition';\n\nfunction itemKey(item: GalleryItem | GalleryItemInput): string {\n return item.id ?? item.src;\n}\n\nfunction resolveElement(target: HTMLElement | string): HTMLElement {\n if (typeof target !== 'string') return target;\n const el = document.querySelector<HTMLElement>(target);\n if (!el) throw new Error(`Shoji: no element matches selector \"${target}\"`);\n return el;\n}\n\n/**\n * DESIGN.md §2.7 — lets code that never held a closure reference to a\n * gallery (a callback resolving long after the call site is gone, a\n * separate `<script>`, devtools) reach the live instance anyway, via\n * `Gallery.getInstance()`/`Gallery.instances()`. Two structures kept in\n * lockstep rather than one: the `WeakMap` gives O(1) lookup-by-element\n * without leaking (an entry is only reachable through an element the host\n * still holds a reference to itself), but WeakMaps aren't iterable, so\n * `instances()` needs the parallel `Set` too. The constructor adds to\n * both; `destroy()` removes from both.\n */\nconst instanceRegistry = new WeakMap<HTMLElement, Gallery>();\nconst allInstances = new Set<Gallery>();\n\n/** CLAUDE.md — all user-visible strings go through `locale`; these are the fallback defaults. */\nconst DEFAULT_LOCALE = {\n close: 'Close',\n previous: 'Previous image',\n next: 'Next image',\n showCaption: 'Show caption',\n hideCaption: 'Hide caption',\n fullCaption: 'Full caption', // DESIGN.md §2.3a\n moreControls: 'More controls', // DESIGN.md §3.1a\n};\n\n/**\n * A click that isn't on the image, a button, a video, or an overlay\n * (counter/caption) counts as \"clicked outside the content.\" Walks\n * `event.composedPath()` (captured at dispatch time) rather than\n * `event.target.closest(...)` — a click handler earlier in the same bubble\n * phase can synchronously replace the clicked element's subtree (e.g. a\n * toolbar button swapping its own icon via `innerHTML` in its click\n * handler), which detaches the original target from the document before\n * this listener (attached higher up, on `.shoji-outer`) ever runs;\n * `.closest()` on a detached node finds no ancestors and would wrongly\n * read as \"clicked outside,\" closing the gallery. `composedPath()` isn't\n * affected by DOM mutations that happen after dispatch.\n *\n * Provider video (YouTube/Vimeo): exclude only the actual iframe/mount\n * element, not the entire `.shoji-slide-provider-video` container. This\n * allows clicks on the blank space around the embed (sides, padding-top\n * toolbar inset) to close the gallery, matching image and HTML5 video\n * behaviour. Clicks that land inside the cross-origin iframe never bubble\n * to the parent at all, so no special handling is needed there; `iframe`\n * covers the edge case where the frame hasn't yet captured focus and a\n * click registers on the element itself from the parent-page perspective.\n * `.shoji-video-mount` is the Vimeo SDK wrapper; YouTube uses a bare\n * `<iframe>` directly inside `.shoji-slide-provider-video`.\n *\n * `caption` — a real bug, reported from real usage: `.shoji-caption--video`\n * (DESIGN.md §2.3a) is `pointer-events: none` so a click on it reaches the\n * video's own native controls underneath, which works fine when the video\n * fills enough of the slide to actually BE underneath it. A letterboxed\n * video (narrower or shorter than the dialog, so the caption's own\n * bottom-left position sits over plain `.shoji-slide-media` background\n * instead) has nothing there to click through *to* — the click fell all\n * the way through to a genuine backdrop click, closing the gallery on what\n * was meant as an interaction with the caption/video area, not \"click\n * outside to close.\" `pointer-events: none` is exactly what removes the\n * caption from `composedPath()` in the first place, so this can't be fixed\n * by adding `.shoji-caption` to the selector above (already there — it\n * already protects a normal, non-click-through caption on a photo slide)\n * — checked by coordinates instead, the one place in this function that\n * has to be.\n */\nfunction isBackdropClick(event: MouseEvent, caption: HTMLElement | null): boolean {\n const hitsProtectedElement = event\n .composedPath()\n .some(\n (node) =>\n node instanceof Element &&\n node.matches(\n `.shoji-slide-img, .shoji-video-mount, iframe, .shoji-counter, ${INTERACTIVE_CONTROL_SELECTOR}`,\n ),\n );\n if (hitsProtectedElement) return false;\n\n if (caption && !caption.hidden && caption.classList.contains('shoji-caption--video')) {\n const rect = caption.getBoundingClientRect();\n const withinCaption =\n event.clientX >= rect.left &&\n event.clientX <= rect.right &&\n event.clientY >= rect.top &&\n event.clientY <= rect.bottom;\n if (withinCaption) return false;\n }\n\n return true;\n}\n\nfunction isDangerousHtmlCaption(value: unknown): value is DangerousHtmlCaption {\n return (\n typeof value === 'object' &&\n value !== null &&\n typeof (value as DangerousHtmlCaption).dangerouslySetInnerHTML === 'string'\n );\n}\n\n/**\n * Core lifecycle (DESIGN.md §2.2), item model / DOM scanning (§2.1), and the\n * lightbox itself: pooled slides (§2.3), dialog semantics/focus trap/live\n * region (§2.6, non-optional), gestures (§2.4, via `GestureController`) and\n * slide-to-slide transitions (§2.5, via `SlideTransition`).\n */\nexport class Gallery {\n /** DESIGN.md §2.7 — the live instance mounted on `el`, or `undefined` if none exists there (or it's since been destroyed). */\n static getInstance(el: HTMLElement): Gallery | undefined {\n return instanceRegistry.get(el);\n }\n\n /** DESIGN.md §2.7 — every live instance, e.g. for page-wide teardown or devtools inspection. */\n static instances(): IterableIterator<Gallery> {\n return allInstances.values();\n }\n\n /** Not `readonly` — `reinit()` (§2.7) reassigns this + every option-derived field below; initializers here just satisfy strict-init. */\n options: GalleryOptions = {};\n readonly element: HTMLElement;\n\n private readonly bus = new EventBus<GalleryEvents>();\n private selector: string = DEFAULT_SELECTOR;\n private isDynamicMode = false;\n private preload = 1;\n private locale: typeof DEFAULT_LOCALE = DEFAULT_LOCALE;\n private showCounter = true;\n private captionVisibleOnVideo = false; // DESIGN.md §2.3a\n // DESIGN.md §2.5 — true from the moment transitionCaption()/open()'s own\n // fade-in starts hiding the caption until the midpoint (or, for open(),\n // the end) where content is swapped and it's revealed again. The async\n // image-load callback in renderCurrentSlide() checks this too, not just\n // the synchronous call in the same function — decode() resolves on a\n // microtask, which can beat the caption's own self-timed reveal for an\n // already-preloaded slide, and updating caption content the instant it\n // fires would show the *new* slide's caption fading, not the outgoing\n // one — same content-swap-timing bug either call site could cause alone.\n private captionFadePending = false;\n private loop = true;\n private closable = true;\n private autoHideDelay: number | false = 5000;\n /** DESIGN.md §3.1a — GalleryOptions.maxPinnedToolbarButtons; see measureToolbarOverflow(). */\n private maxPinnedToolbarButtons = 2;\n private readonly focusTrap = new FocusTrap();\n private readonly liveRegion = new LiveRegion();\n private autoHideTimer: ReturnType<typeof setTimeout> | null = null;\n private autoHidden = false;\n /**\n * Every control `wireControlHover()` has ever wired — the full candidate\n * list `reconcileHover()` (below) checks `:hover` against, kept separate\n * from `hoveringElements` (elements *believed* to be hovered) so\n * reconciliation can also *add* one currently missing from that set, not\n * just remove a stale one.\n */\n private readonly hoverableElements = new Set<HTMLElement>();\n /**\n * The subset of `hoverableElements` the pointer is currently considered\n * to be over — a `Set`, not a plain incrementing counter, so\n * `onActivity()` (below) can reconcile it against the browser's own live\n * `:hover` ground truth on every real pointer movement, self-healing from\n * a `pointerenter`/`pointerleave` pair that never fired at all. Two real\n * bugs, both reported from real usage, both variations on the same\n * browser behavior already leveraged elsewhere in this file (`hidden`\n * doesn't fire `pointerleave` under a stationary cursor, §2.3a): (1) a\n * control that synchronously replaces its own children while the pointer\n * sits stationary over it (Autoplay's own play/pause icon swap) can\n * desync the browser's internal hover-chain tracking badly enough that\n * the *next* real pointer movement away from it never fires `pointerleave`\n * — a plain counter has no way to detect a leave that never reports\n * itself. (2) the mirror image: a control that *appears* under an\n * already-stationary cursor (the common case right after `open()` —\n * whatever the viewer clicked to get here is often right where a\n * toolbar/caption/nav control ends up) never gets a `pointerenter`\n * either, since browsers only recompute hover state on actual pointer\n * movement, not on an element materializing under one already sitting\n * still — so idle auto-hide could fire and hide a control the viewer's\n * cursor is, visibly, still resting directly on top of. Re-deriving \"is\n * this actually hovered\" fresh from the browser on every real pointer\n * move fixes both directions at once, since neither depends on any\n * event having fired correctly in the first place.\n */\n private readonly hoveringElements = new Set<HTMLElement>();\n private isClosing = false;\n /** True while a vertical drag has hidden controls past its own distance threshold (`setControlsHiddenForDrag`) — `onActivity()` defers to it, since the drag's own continuous pointermove stream would otherwise immediately re-reveal what it just hid on every single move. */\n private controlsHiddenByDrag = false;\n private itemList: GalleryItem[] = [];\n private scannedElements: HTMLElement[] = [];\n private activeIndex = 0;\n private opened = false;\n private destroyed = false;\n private slides: SlideManager | null = null;\n private dom: LightboxDom | null = null;\n private gesture: GestureController | null = null;\n private transition: SlideTransition | null = null;\n /** DESIGN.md §2.3a — measures `.shoji-toolbar`'s real rendered height (it can wrap to multiple rows on a narrow viewport with many toolbar buttons registered) so the caption's own height cap can reserve exactly that much space, not a fixed single-row guess. */\n private toolbarHeightObserver: ResizeObserver | null = null;\n private toolbarHeightFrame: number | null = null;\n private captionTruncationFrame: number | null = null;\n /** DESIGN.md §2.3a — a truncated caption's own click/Enter/Space target opens this; also gates `GestureController`'s `isZoomed` (alongside the real zoom gate) so a drag over the open modal can't also navigate/close the lightbox underneath it. */\n private captionModalOpen = false;\n private captionModalReturnFocus: HTMLElement | null = null;\n /**\n * DESIGN.md §2.3a — every real path into `openCaptionModal()` starts from\n * a click *or* a keydown on the caption, so `captionModalReturnFocus`\n * above is always the caption itself either way; this instead\n * distinguishes *how* it got there, so `closeCaptionModal()` only\n * actually calls `.focus()` for the keyboard path (a real Tab+Enter user,\n * where restoring focus continues their tab sequence correctly) and\n * leaves it alone for a mouse click (where the resulting focus was\n * purely incidental — nothing about clicking to read a caption means the\n * *next* keypress, e.g. a plugin's own Space shortcut, should still\n * silently target it).\n */\n private captionModalOpenedViaKeyboard = false;\n /**\n * DESIGN.md §3.1a — every `ctx.ui.toolbar()`-registered button, in\n * registration order, alongside the slot it was registered into (so a\n * collapsed one can be restored to the right place, not just anywhere).\n * Registration order is what decides overflow priority: up to\n * `maxPinnedToolbarButtons` stay pinned, the rest collapse into the\n * popover before them, latest-registered first — see\n * `measureToolbarOverflow()`.\n */\n private readonly pluginToolbarButtons: Array<{\n el: HTMLElement;\n slot: 'left' | 'center' | 'right';\n }> = [];\n private toolbarOverflowOpen = false;\n private toolbarOverflowReturnFocus: HTMLElement | null = null;\n private readonly shortcuts = new Map<string, (e: KeyboardEvent) => void>();\n private readonly pluginStorage = new Map<string, unknown>();\n /** Backs `getActivePlugins()`. */\n private readonly activePluginNames = new Set<string>();\n private readonly videoProviders = new Map<string, VideoProviderRenderer>();\n private zoomGate: (() => boolean) | null = null;\n /** DESIGN.md §2.6a/§4.6 — the zoomed `<img>`'s own real on-screen rect, read by `beginClose()` before `beforeClose` fires (see `registerZoomStartProvider()`), so a button-close continues the zoom-out from wherever the viewer was actually zoomed/panned to instead of snapping back to neutral first. */\n private zoomStartProvider: (() => Box | null) | null = null;\n /** DESIGN.md §2.5/§4.5 — plugins with a per-slide visual override (RotateFlip's rotate/flip, Zoom's scale/pan) that reset unanimated on `beforeSlide` (must clear before `SlideManager.render()` reparents the outgoing node). Multi-slot: more than one can be active on the same slide, each targeting a different part of the clone, so they don't conflict. See `registerSlideLeaveDecorator()`. */\n private readonly slideLeaveDecorators = new Set<\n (clonedMedia: HTMLElement) => (() => void) | void\n >();\n private pluginCleanups: Array<() => void> = [];\n\n private readonly onContainerClick = (event: MouseEvent): void => {\n let node = event.target as Node | null;\n while (node && node !== this.element) {\n if (node instanceof HTMLElement) {\n const index = this.scannedElements.indexOf(node);\n if (index !== -1) {\n event.preventDefault();\n this.open(index);\n return;\n }\n }\n node = node.parentNode;\n }\n };\n\n private readonly onOuterClick = (event: MouseEvent): void => {\n if (this.closable && isBackdropClick(event, this.dom?.caption ?? null)) this.close();\n };\n\n /**\n * DESIGN.md §2.8 — any interaction re-shows controls, restarts the idle\n * clock. `autoHideDelay: 0` = \"never show controls\" — a no-op here.\n * `autoHideDelay: false` = \"always visible\" — also a no-op: nothing to\n * reveal (already shown) or reschedule (no timer ever runs). Also a no-op\n * once `isClosing` — a real bug, reported from real usage: moving the\n * mouse during close()'s own controls-fade-then-zoom-out sequence (§2.6a)\n * re-showed the just-hidden controls mid-animation, since these listeners\n * stay wired for the whole close sequence. Same reasoning for\n * `controlsHiddenByDrag` (§2.4/§2.8): a vertical drag's own `pointermove`\n * stream would otherwise re-reveal what it just hid, every single frame.\n */\n private readonly onActivity = (): void => {\n this.reconcileHover();\n if (\n this.autoHideDelay === 0 ||\n this.autoHideDelay === false ||\n this.isClosing ||\n this.controlsHiddenByDrag ||\n // DESIGN.md §2.3a — requested directly: mouse movement/interaction\n // over the open caption modal shouldn't re-reveal controls already\n // hidden behind it — `dom.outer`'s own pointermove/pointerdown/etc.\n // listeners (below) still see it bubble through, same as any other\n // activity, so without this an idle-hidden toolbar would pop back\n // the instant the viewer so much as moved the mouse to read.\n this.captionModalOpen\n )\n return;\n this.showControls();\n this.scheduleAutoHide();\n };\n\n /** x */\n private controlsHiddenAtGestureStart = false;\n private readonly captureGestureStartState = (): void => {\n this.controlsHiddenAtGestureStart = this.autoHidden;\n };\n\n private readonly onKeydown = (event: KeyboardEvent): void => {\n // A focused <video>'s native controls own arrow/space/home/end for\n // seeking, volume, and play/pause — don't let slide navigation hijack\n // those while the viewer is actually interacting with the video. Escape\n // still closes the gallery regardless.\n if (document.activeElement instanceof HTMLVideoElement && event.key !== 'Escape') return;\n\n this.onActivity();\n switch (event.key) {\n case 'Escape':\n if (this.closable) this.close();\n break;\n case 'ArrowLeft':\n case 'a':\n case 'A':\n this.prev();\n break;\n case 'ArrowRight':\n case 'd':\n case 'D':\n this.next();\n break;\n case 'Home':\n this.goTo(0);\n break;\n case 'End':\n this.goTo(this.itemList.length - 1);\n break;\n default: {\n // DESIGN.md §3 — ctx.ui.registerShortcut(); unhandled keys still\n // fall through with no preventDefault, same as before plugins existed.\n const shortcut = this.shortcuts.get(event.key);\n if (!shortcut) return;\n shortcut(event);\n }\n }\n event.preventDefault();\n };\n\n /**\n * DESIGN.md §2.3a — capture phase, added only while the caption modal is\n * open (not from `open()` onward like `onKeydown`, which is bubble\n * phase): capture always finishes before bubble starts, so\n * `stopPropagation()` here reliably beats `onKeydown`'s own bubble-phase\n * handling regardless of where in the dialog focus happens to be. A real\n * bug, reported from real usage: an earlier version of this only special-\n * cased `Escape`, leaving every other key (`Space` — Autoplay's own\n * play/pause shortcut if that plugin's loaded, arrow keys, `w`/`s` for\n * Zoom, any plugin-registered shortcut) to fall straight through to\n * `onKeydown` while the modal sat open on screen. A modal dialog should\n * make the background fully inert to keyboard input while it's open, not\n * just for one key — so this now stops propagation for *every* key\n * unconditionally, closing the modal as the one piece of extra behavior\n * layered on top for `Escape` specifically. Deliberately no\n * `preventDefault()` here — the modal's own contents (the close button,\n * a scrollable panel) keep their normal native key behavior (Space\n * activating a focused button, arrow/Space scrolling), only the\n * *background* gallery is what this isolates it from.\n */\n private readonly onCaptionModalKeydown = (event: KeyboardEvent): void => {\n event.stopPropagation();\n if (event.key === 'Escape') this.closeCaptionModal();\n };\n\n constructor(target: HTMLElement | string, options: GalleryOptions = {}) {\n this.element = resolveElement(target);\n instanceRegistry.set(this.element, this);\n allInstances.add(this);\n this.applyOptions(options);\n }\n\n /** Everything the constructor does after `this.element` is resolved — shared with `reinit()` (§2.7). */\n private applyOptions(options: GalleryOptions): void {\n this.options = options;\n this.selector = options.selector ?? DEFAULT_SELECTOR;\n this.isDynamicMode = options.items !== undefined;\n this.preload = options.preload ?? 1;\n this.locale = { ...DEFAULT_LOCALE, ...options.locale };\n this.showCounter = options.counter ?? true;\n this.loop = options.loop ?? true;\n this.closable = options.closable ?? true;\n this.autoHideDelay = options.autoHideDelay ?? 5000;\n this.maxPinnedToolbarButtons = options.maxPinnedToolbarButtons ?? 2;\n // Reset explicitly — a reinit() call must not inherit these from before.\n this.activeIndex = 0;\n this.scannedElements = [];\n\n if (this.isDynamicMode) {\n this.itemList = resolveDynamicVideoItems(options.items ?? []);\n } else {\n const scanned = scanContainer(this.element, this.selector);\n this.itemList = scanned.map((s) => s.item);\n this.scannedElements = scanned.map((s) => s.element);\n this.element.addEventListener('click', this.onContainerClick);\n }\n\n // DESIGN.md §3.1 — plugins init here, at construction, not lazily on\n // first open(): a layout plugin needs to render the *inline* container\n // (this.element, always available) before open() is ever called — it's\n // what the viewer clicks to open in the first place. ensureLightbox()'s\n // `if (this.dom) return;` guard makes the later, harmless no-op call\n // from open() safe; building the (hidden, display:none until opened)\n // lightbox DOM this early costs one inert subtree per instance, which is\n // negligible next to the thumbnail images already loading regardless.\n this.ensureLightbox();\n // Set after ensureLightbox() (not inside it) so a reinit() with a\n // different `closable` value updates the already-built button too —\n // ensureLightbox() only builds the DOM once (`if (this.dom) return;`).\n this.dom!.closeButton.hidden = !this.closable;\n\n // Default is closed — the host clicks a thumbnail or calls open()\n // itself. openOnInit is the explicit, opt-in exception (deep-link-style\n // integrations); guarded on having at least one item so an empty\n // gallery can't \"open\" onto nothing.\n if (options.openOnInit && this.itemList.length > 0) {\n this.open(options.index ?? 0);\n }\n }\n\n get items(): readonly GalleryItem[] {\n return this.itemList;\n }\n\n get currentIndex(): number {\n return this.activeIndex;\n }\n\n /** True from the first `open()` until `close()`/`destroy()`. */\n get isOpen(): boolean {\n return this.opened;\n }\n\n /** True once `destroy()` has run. */\n get isDestroyed(): boolean {\n return this.destroyed;\n }\n\n /** Whether auto-hide (§2.8) currently has controls faded. */\n get controlsHidden(): boolean {\n return this.autoHidden;\n }\n\n /** Names of plugins that actually initialized. */\n getActivePlugins(): string[] {\n return [...this.activePluginNames];\n }\n\n /** The active slide's `.shoji-slide-media` container. Empty before the first `open()`; `null` only after `destroy()`. */\n getActiveMedia(): HTMLElement | null {\n return this.slides?.getActiveMedia() ?? null;\n }\n\n /** DESIGN.md §4-zoom — suspends drag-to-navigate/close while zoomed. Single slot, not a multi-subscriber event. */\n registerZoomGate(isZoomed: () => boolean): () => void {\n this.zoomGate = isZoomed;\n return () => {\n if (this.zoomGate === isZoomed) this.zoomGate = null;\n };\n }\n\n /** DESIGN.md §2.6a/§4.6 — returns the zoomed image's current on-screen rect (`getBoundingClientRect()`), or `null` when not zoomed. Single slot, same pattern as `registerZoomGate` above. */\n registerZoomStartProvider(provider: () => Box | null): () => void {\n this.zoomStartProvider = provider;\n return () => {\n if (this.zoomStartProvider === provider) this.zoomStartProvider = null;\n };\n }\n\n /**\n * DESIGN.md §2.5/§4.5 — called once per navigation with the leave-\n * ghost's clone, right after `SlideTransition` creates it. Freeze\n * whatever per-slide visual state is about to be reset onto the clone\n * (or a descendant, e.g. Zoom's own `<img>`) and return a `() => void`\n * to trigger the transition back to neutral once committed — or return\n * nothing if there's nothing to animate away this time.\n */\n registerSlideLeaveDecorator(\n decorator: (clonedMedia: HTMLElement) => (() => void) | void,\n ): () => void {\n this.slideLeaveDecorators.add(decorator);\n return () => {\n this.slideLeaveDecorators.delete(decorator);\n };\n }\n\n on<K extends keyof GalleryEvents>(event: K, fn: (detail: GalleryEvents[K]) => void): Unsubscribe {\n return this.bus.on(event, fn);\n }\n\n private clampToRange(index: number): number {\n return Math.min(Math.max(index, 0), Math.max(this.itemList.length - 1, 0));\n }\n\n private ensureLightbox(): void {\n if (this.dom) return;\n this.slides = new SlideManager({\n preload: this.preload,\n videoProviders: this.videoProviders,\n });\n this.transition = new SlideTransition(this.slides, (clonedMedia) => {\n const settlers: Array<() => void> = [];\n for (const decorate of this.slideLeaveDecorators) {\n const settle = decorate(clonedMedia);\n if (settle) settlers.push(settle);\n }\n return () => {\n for (const settle of settlers) settle();\n };\n });\n const dom = buildLightboxDom(this.slides.element, this.locale);\n this.dom = dom;\n if (this.options.backdropOpacity != null) {\n const clamped = Math.min(Math.max(this.options.backdropOpacity, 0), 1);\n dom.outer.style.setProperty('--shoji-backdrop-opacity', String(clamped));\n }\n if (this.options.transitionDuration != null) {\n const clamped = Math.max(this.options.transitionDuration, 0);\n dom.outer.style.setProperty('--shoji-duration', `${clamped}ms`);\n }\n // DESIGN.md §2.8 — autoHideDelay: 0 means Shoji's own controls stay\n // permanently invisible (a host building fully custom chrome), not that\n // the whole gallery should behave like nothing is there — the mouse\n // cursor itself should still behave normally, not force-hidden along\n // with controls that were never meant to be seen in the first place.\n // Requested directly; `.shoji-cursor-visible` overrides the\n // `.shoji-controls-hidden` cursor:none rule via higher specificity.\n if (this.autoHideDelay === 0) dom.dialog.classList.add('shoji-cursor-visible');\n dom.outer.appendChild(this.liveRegion.element);\n dom.closeButton.addEventListener('click', () => this.close());\n dom.prevButton.addEventListener('click', () => this.prev());\n dom.nextButton.addEventListener('click', () => this.next());\n dom.captionToggleButton.addEventListener('click', () => {\n this.captionVisibleOnVideo = !this.captionVisibleOnVideo;\n this.updateCaptionVisibility();\n });\n dom.caption.addEventListener('click', this.onCaptionActivate);\n dom.caption.addEventListener('keydown', this.onCaptionActivate);\n dom.captionModalCloseButton.addEventListener('click', () => this.closeCaptionModal());\n // Closes on a genuine backdrop click (the modal element itself, not a\n // descendant) — always stops propagation regardless, since without it\n // *any* click anywhere inside the modal (the panel, its content, even\n // the close button) would keep bubbling up to onOuterClick below and\n // get misread as \"clicked outside the real content,\" closing the whole\n // lightbox out from under it.\n dom.captionModal.addEventListener('click', (event) => {\n event.stopPropagation();\n if (event.target === dom.captionModal) this.closeCaptionModal();\n });\n dom.toolbarOverflowButton.addEventListener('click', (event) => {\n event.stopPropagation();\n this.toggleToolbarOverflow();\n });\n // Closes on any click outside the panel and off the caret itself —\n // registered ahead of onOuterClick below (same-node listeners run in\n // registration order) so it can stop the click there too, the same\n // reasoning the caption modal's own backdrop-click listener above uses:\n // without it, a click that closes the popover would also keep bubbling\n // and get misread by onOuterClick as \"clicked outside the real\n // content,\" closing the whole lightbox in the same click.\n dom.outer.addEventListener('click', (event) => {\n if (!this.toolbarOverflowOpen) return;\n // event.composedPath(), not event.target — a real bug, found testing\n // this against Autoplay's own play/pause button (icon-swap-on-click,\n // same class of button CLAUDE.md/DESIGN.md §2.8 already documents):\n // if the click's own handler replaces the button's innerHTML\n // synchronously (as that pattern does), `event.target` can already be\n // a now-detached node by the time this bubble-phase listener runs —\n // `.contains(target)` then reads false even though the click\n // genuinely originated inside the panel, and the popover (wrongly)\n // closes out from under the very button that was just clicked.\n // composedPath() is captured at dispatch time, before any such\n // mutation, so it stays accurate regardless.\n const insidePanel = event\n .composedPath()\n .some((node) => node === dom.toolbarOverflowPanel || node === dom.toolbarOverflowButton);\n if (insidePanel) return;\n event.stopPropagation();\n this.closeToolbarOverflow();\n });\n dom.outer.addEventListener('click', this.onOuterClick);\n dom.outer.addEventListener('pointermove', this.onActivity, { passive: true });\n // Registered before onActivity's own pointerdown listener — order matters, see captureGestureStartState's doc comment.\n dom.outer.addEventListener('pointerdown', this.captureGestureStartState, { passive: true });\n dom.outer.addEventListener('pointerdown', this.onActivity, { passive: true });\n dom.outer.addEventListener('touchstart', this.onActivity, { passive: true });\n dom.outer.addEventListener('wheel', this.onActivity, { passive: true });\n dom.outer.addEventListener('focusin', this.onActivity);\n dom.outer.addEventListener('focusout', () => this.scheduleAutoHide());\n\n // Hovering pauses auto-hide (pointermove alone wouldn't catch a still\n // mouse) — every part of the overlay, not just individual buttons\n // (toolbarLeft/Center/Right cover the bar's own padding/gaps too, not\n // just the buttons already nested inside them). Plugin toolbar\n // buttons/overlays get this wiring at their own call sites.\n for (const el of [\n dom.closeButton,\n dom.prevButton,\n dom.nextButton,\n dom.captionToggleButton,\n dom.caption,\n dom.captionModalPanel,\n dom.counter,\n dom.toolbarLeft,\n dom.toolbarCenter,\n dom.toolbarRight,\n ]) {\n this.wireControlHover(el);\n }\n\n document.body.appendChild(dom.outer);\n\n // DESIGN.md §2.3a — a fixed single-row assumption undercounted a busy\n // toolbar (many plugins registering buttons) wrapping to multiple rows\n // on a narrow viewport, letting the caption's own height cap grow up\n // over the real controls instead of stopping below them. Measuring the\n // real rendered height instead of guessing is correct regardless of\n // plugin count or viewport width. rAF-batched, same pattern the Layout\n // plugin's own resize handling already uses (CLAUDE.md: batch DOM\n // writes that can thrash layout).\n this.toolbarHeightObserver = new ResizeObserver(() => this.scheduleToolbarOverflowMeasure());\n this.toolbarHeightObserver.observe(dom.toolbar);\n\n // DESIGN.md §2.4 — attached to the whole dialog (not just .shoji-slides)\n // so vertical swipe-to-close works from anywhere in it, not only over\n // the image; shouldIgnoreGesture() excludes real controls so their\n // native click/touch behavior is unaffected.\n this.gesture = new GestureController(\n {\n dialog: dom.dialog,\n caption: dom.caption,\n slides: this.slides,\n canGoNext: () => this.loop || this.activeIndex < this.itemList.length - 1,\n canGoPrev: () => this.loop || this.activeIndex > 0,\n // animate:false — a completed swipe already played its own\n // live-drag/settle animation (§2.4); running the §2.5 `mode`\n // transition on top would double-animate. See navigate()'s doc.\n next: () => this.navigate(this.nextIndex(), 1, false),\n prev: () => this.navigate(this.prevIndex(), -1, false),\n close: () => this.close(),\n closeFromSwipe: (frozenDrag) => this.closeFromSwipe(frozenDrag),\n setControlsHiddenForDrag: (hidden) => this.setControlsHiddenForDrag(hidden),\n canClose: () => this.closable,\n onActivity: () => this.onActivity(),\n // DESIGN.md §2.3a — also suspends drag-to-navigate/close while the\n // caption modal is open, same mechanism the Zoom plugin's own gate\n // already uses, so a drag starting over the modal can't also\n // navigate/close the lightbox underneath it.\n isZoomed: () =>\n (this.zoomGate?.() ?? false) || this.captionModalOpen || this.toolbarOverflowOpen,\n },\n {\n onTap: (x, y) =>\n this.bus.emit('tap', {\n x,\n y,\n controlsWereHidden: this.controlsHiddenAtGestureStart,\n }),\n onDoubleTap: (x, y) => this.bus.emit('doubleTap', { x, y }),\n onPinchStart: (centerX, centerY) => this.bus.emit('pinchStart', { centerX, centerY }),\n onPinchMove: (scale, centerX, centerY) =>\n this.bus.emit('pinchMove', { scale, centerX, centerY }),\n onPinchEnd: () => this.bus.emit('pinchEnd', {}),\n onWheelZoom: (deltaScale, x, y) => this.bus.emit('wheelZoom', { deltaScale, x, y }),\n },\n this.options.gestures,\n );\n\n this.initPlugins();\n }\n\n /**\n * DESIGN.md §3 — which of `declared`'s plugins will actually load,\n * resolved as a fixed point rather than a single pass: start with every\n * *structurally* valid entry (a real object with an `init` function),\n * then repeatedly drop any whose `requires` points at a name no longer\n * in the set, until a full pass drops nothing. That repetition is what\n * makes a chain cascade correctly — if a plugin gets dropped because\n * *its* own requirement failed, anything requiring that plugin drops on\n * the very next pass, the same as if the original problem were its own.\n * A genuine mutual requirement (A needs B, B needs A, both otherwise\n * fine) never gets caught in this — each still finds the other present\n * whenever it's checked, so both stay valid. That's correct, not a\n * missed case: this only ever decides *whether* something loads, never\n * *when* — nothing here reorders execution, so two plugins depending on\n * each other creates no actual ordering conflict to detect in the first\n * place.\n */\n private resolveValidPluginNames(declared: readonly ShojiPlugin[]): Set<string> {\n const validNames = new Set<string>();\n for (const plugin of declared) {\n if (plugin && typeof plugin.init === 'function') validNames.add(plugin.name);\n }\n let changed = true;\n while (changed) {\n changed = false;\n for (const plugin of declared) {\n if (!plugin || typeof plugin.init !== 'function' || !validNames.has(plugin.name)) continue;\n const unmet = (plugin.requires ?? []).some((name) => !validNames.has(name));\n if (unmet) {\n validNames.delete(plugin.name);\n changed = true;\n }\n }\n }\n return validNames;\n }\n\n /**\n * DESIGN.md §3 — plugins init here, not the constructor. `requires` is\n * resolved against the *whole* declared `plugins` list up front, not\n * registration order — a real friction point, reported directly: a host\n * with a `requires` chain had to carefully order the array by hand, with\n * no actual reason to (nothing about *execution* order needs to match\n * declaration order for a name-presence check). `resolveValidPluginNames()`\n * decides who's actually going to load, independent of position; this\n * loop still runs — and every `ctx.ui.toolbar()` registration still\n * lands — in exactly the array's own order regardless, so toolbar/\n * collapse-priority order (also array-order-driven, DESIGN.md §3.1a)\n * is completely unaffected by this. Cleared up front so a `reinit()`\n * doesn't inherit names from before.\n */\n private initPlugins(): void {\n this.activePluginNames.clear();\n this.videoProviders.clear();\n const declared = this.options.plugins ?? [];\n const validNames = this.resolveValidPluginNames(declared);\n\n for (const plugin of declared) {\n // Guards a real, easy-to-hit host mistake: `plugins: [Shoji.SomePlugin]`\n // silently becomes `plugins: [undefined]` if the referenced static\n // (e.g. Shoji.Autoplay) doesn't actually exist yet — a stale dist\n // build predating that plugin's addition is the most common cause.\n // One bad entry shouldn't crash plugin init for every entry after it.\n if (!plugin || typeof plugin.init !== 'function') {\n console.error(\n 'Shoji: skipping an invalid plugins[] entry (not a ShojiPlugin object):',\n plugin,\n );\n continue;\n }\n if (!validNames.has(plugin.name)) {\n const missing = (plugin.requires ?? []).filter((name) => !validNames.has(name));\n console.error(\n `Shoji: plugin \"${plugin.name}\" requires [${missing.join(', ')}], which failed to load (missing, invalid, or itself had an unmet requires) — skipping.`,\n );\n continue;\n }\n\n const pluginOptions: Record<string, unknown> = {\n ...plugin.defaults,\n ...(this.options[plugin.name] as Record<string, unknown> | undefined),\n };\n const ctx: PluginContext = {\n gallery: this,\n options: pluginOptions,\n on: this.on.bind(this),\n emit: (event, detail) => this.bus.emit(event, detail),\n ui: {\n toolbar: (slot, el) => this.pluginToolbar(slot, el),\n overlay: (el, layer) => this.pluginOverlay(el, layer),\n outer: () => this.dom!.outer,\n registerShortcut: (key, fn) => {\n this.shortcuts.set(key, fn);\n return () => this.shortcuts.delete(key);\n },\n registerVideoProvider: (name, render) => {\n this.videoProviders.set(name, render);\n return () => {\n if (this.videoProviders.get(name) === render) this.videoProviders.delete(name);\n };\n },\n },\n storage: {\n get: (key) => this.pluginStorage.get(key),\n set: (key, value) => {\n this.pluginStorage.set(key, value);\n },\n },\n };\n\n const cleanup = plugin.init(ctx);\n if (typeof cleanup === 'function') this.pluginCleanups.push(cleanup);\n this.activePluginNames.add(plugin.name);\n }\n }\n\n /**\n * DESIGN.md §3.1 — 'right' inserts immediately before the overflow caret\n * (DESIGN.md §3.1a — never after, and never after close either), so\n * close stays fixed absolute-rightmost, the caret sits just to its left,\n * and plugins cluster further left still, in registration order:\n * `plugins: [A, B, C]` reads A, B, C, caret, close. 'left'/'center' are\n * independent zones for a plugin that doesn't want to sit next to close.\n */\n private pluginToolbar(\n slot: 'left' | 'center' | 'right',\n el: HTMLElement | ButtonSpec,\n ): Unsubscribe {\n const dom = this.dom!;\n // buildToolbarButton() below already wires hover for ButtonSpec; a raw\n // element needs it here instead, once, not double-wired either way.\n const isRawElement = el instanceof HTMLElement;\n const node = isRawElement ? el : this.buildToolbarButton(el);\n const unhover = isRawElement ? this.wireControlHover(node) : null;\n if (slot === 'right') {\n dom.toolbarRight.insertBefore(node, dom.toolbarOverflowButton);\n } else {\n (slot === 'left' ? dom.toolbarLeft : dom.toolbarCenter).appendChild(node);\n }\n this.pluginToolbarButtons.push({ el: node, slot });\n this.scheduleToolbarOverflowMeasure();\n return () => {\n unhover?.();\n node.remove();\n const index = this.pluginToolbarButtons.findIndex((b) => b.el === node);\n if (index !== -1) this.pluginToolbarButtons.splice(index, 1);\n this.scheduleToolbarOverflowMeasure();\n };\n }\n\n private buildToolbarButton(spec: ButtonSpec): HTMLButtonElement {\n const button = document.createElement('button');\n button.type = 'button';\n button.className = 'shoji-toolbar-button';\n if (spec.icon) {\n button.ariaLabel = button.title = spec.label;\n button.innerHTML = spec.icon;\n } else {\n button.textContent = spec.label;\n }\n button.addEventListener('click', spec.onClick);\n this.wireControlHover(button);\n return button;\n }\n\n /**\n * DESIGN.md §3.1a — coalesces both toolbar-overflow measurement and the\n * pre-existing `--shoji-toolbar-height` update into one rAF-batched pass\n * (CLAUDE.md: batch DOM writes that can thrash layout), triggered by the\n * toolbar's own `ResizeObserver` (width *or* height changes — a busy\n * toolbar wrapping counts as both) and by `pluginToolbar()` registering\n * or unregistering a button. Order matters: overflow collapse has to run\n * *before* the height read, so the height custom property reflects the\n * settled (ideally single-row, post-collapse) toolbar, not a momentarily\n * wrapped one the caption's own height cap would otherwise over-reserve\n * space for.\n */\n private scheduleToolbarOverflowMeasure(): void {\n if (this.toolbarHeightFrame !== null) return;\n this.toolbarHeightFrame = requestAnimationFrame(() => {\n this.toolbarHeightFrame = null;\n if (!this.dom) return;\n this.measureToolbarOverflow();\n this.dom.dialog.style.setProperty(\n '--shoji-toolbar-height',\n `${this.dom.toolbar.getBoundingClientRect().height}px`,\n );\n });\n }\n\n /**\n * DESIGN.md §3.1a — collapses plugin toolbar buttons into\n * `toolbarOverflowPanel` once they don't fit in one row, instead of\n * letting the toolbar wrap to a second/third one. Always restores every\n * plugin button to its own original slot first and recomputes from that\n * clean slate, rather than incrementally adjusting whatever the previous\n * pass left behind — the *set* of buttons and the viewport width can\n * both have changed since then, and a fresh, deterministic pass is both\n * simpler and self-correcting than trying to reason about a delta.\n *\n * \"Fits in one row\" is measured, not assumed: each slot's own rendered\n * height is compared against `closeButton`'s (always exactly one row\n * tall, always present) — a slot has wrapped if it's taller than that,\n * regardless of *which* slot a plugin happened to register into (the\n * three slots wrap independently, DESIGN.md §3.1a's own CSS notes).\n *\n * Collapses latest-registered first (DESIGN.md §3's own registration-\n * order-is-priority convention — `plugins: [A, B, C]` keeps A pinned\n * before B/C). `maxPinnedToolbarButtons` (default 2, see\n * `GalleryOptions.maxPinnedToolbarButtons`) is a ceiling, not a\n * guarantee: collapsing always goes down to at most that many pinned,\n * but keeps going *below* it — down to zero if it must — whenever even\n * that many still leaves a slot wrapped. `closeButton` and the counter\n * (`toolbarLeft`) must never wrap onto their own line, and `fitsOneRow()`\n * already checks every slot's height, not just `toolbarRight`'s, so a\n * wide counter or other left-slot content competing for the same row\n * pushes the pinned count down too, not just the right slot's own\n * button count. `closeButton`/`toolbarOverflowButton` themselves are\n * never candidates for collapse — everything else relocates into the\n * popover, which renders directly below that row.\n */\n private measureToolbarOverflow(): void {\n if (!this.dom) return;\n const dom = this.dom;\n for (const { el, slot } of this.pluginToolbarButtons) {\n if (slot === 'right') dom.toolbarRight.insertBefore(el, dom.toolbarOverflowButton);\n else (slot === 'left' ? dom.toolbarLeft : dom.toolbarCenter).appendChild(el);\n }\n dom.toolbarOverflowPanel.replaceChildren();\n dom.toolbarOverflowButton.hidden = true;\n if (this.toolbarOverflowOpen) this.closeToolbarOverflow();\n\n const rowHeight = dom.closeButton.getBoundingClientRect().height;\n const fitsOneRow = (): boolean =>\n [dom.toolbarLeft, dom.toolbarCenter, dom.toolbarRight].every(\n (slotEl) => slotEl.getBoundingClientRect().height <= rowHeight + 1,\n );\n\n if (fitsOneRow()) return;\n\n const pinnedCeiling = Math.min(this.maxPinnedToolbarButtons, this.pluginToolbarButtons.length);\n dom.toolbarOverflowButton.hidden = false;\n for (let i = this.pluginToolbarButtons.length - 1; i >= 0; i--) {\n // Below the ceiling, stop as soon as it fits; at/above it, keep\n // collapsing regardless of fit — the ceiling is enforced first, then\n // fit is what decides whether to go further still.\n if (i < pinnedCeiling && fitsOneRow()) break;\n // Collapsed latest-registered first (the decision above), but\n // inserted at the *front* of the panel each time — the panel should\n // read in the same left-to-right registration order the toolbar row\n // itself would have shown, not reversed just because that's the order\n // they were evaluated in.\n dom.toolbarOverflowPanel.insertBefore(\n this.pluginToolbarButtons[i]!.el,\n dom.toolbarOverflowPanel.firstChild,\n );\n }\n }\n\n private readonly onToolbarOverflowKeydown = (event: KeyboardEvent): void => {\n event.stopPropagation();\n if (event.key === 'Escape') this.closeToolbarOverflow();\n };\n\n private toggleToolbarOverflow(): void {\n if (this.toolbarOverflowOpen) this.closeToolbarOverflow();\n else this.openToolbarOverflow();\n }\n\n private openToolbarOverflow(): void {\n if (!this.dom || this.toolbarOverflowOpen) return;\n this.toolbarOverflowReturnFocus = document.activeElement as HTMLElement | null;\n this.dom.toolbarOverflowPanel.hidden = false;\n this.positionToolbarOverflowPanel();\n this.dom.toolbarOverflowButton.setAttribute('aria-expanded', 'true');\n this.toolbarOverflowOpen = true;\n this.focusTrap.retarget(this.dom.toolbarOverflowPanel);\n document.addEventListener('keydown', this.onToolbarOverflowKeydown, true);\n }\n\n /**\n * DESIGN.md §3.1a — the panel's `right` offset is set here, per open,\n * rather than as a fixed CSS value: the caret's own x-position isn't\n * fixed, it shifts with how many buttons are pinned ahead of it\n * (`maxPinnedToolbarButtons` is host-configurable, and can itself be\n * reduced further at measure time, DESIGN.md §3.1a), so a static\n * `right: var(--shoji-spacing-md)` only happened to line up with the\n * caret at one particular pinned-button count and viewport width. It\n * otherwise anchored to the toolbar/dialog's own right edge — under\n * `closeButton`, which sits to the right of the caret, not under the\n * caret that actually opens it.\n *\n * Aligns the panel's own *content* edge (inside its padding), not just\n * its border box, with the caret's right edge — the panel's grid pitch\n * (44px columns, `--shoji-spacing-sm` gaps, `.shoji-toolbar-overflow-\n * panel` in shoji.css) already matches the toolbar row's own button\n * size/gap, so subtracting the panel's own right padding here is what\n * makes the popover's icon columns actually line up with the toolbar\n * row's icons above them, not just the panel block sitting roughly\n * nearby.\n *\n * Also sets the grid's own column count to match the toolbar row's own\n * column count right now: however many buttons are *actually* still\n * pinned on `toolbarRight`, *plus the caret itself* — requested directly:\n * the popover should read as the same row of columns continuing\n * downward, caret included, not a fixed 3 columns regardless of how many\n * ended up pinned. At the default `maxPinnedToolbarButtons` (2), that's 2\n * pinned + the caret = 3 columns. Collapsed buttons beyond that count\n * still wrap onto further rows within it, same as before.\n *\n * **A real bug: \"pinned\" is counted from `this.pluginToolbarButtons`\n * (registered via `ctx.ui.toolbar()`), never by querying `toolbarRight`'s\n * DOM children.** The old filter (`toolbarRight.children`, excluding\n * `closeButton`/`toolbarOverflowButton` by identity, `!el.hidden`) broke\n * once a host appended an unrelated element straight into `toolbarRight`\n * — not a documented extension point, but nothing stops it. Reported from\n * real usage: a plugin's own loading spinner, toggled via `style.display`\n * rather than the `hidden` attribute, passed `!el.hidden` and got\n * miscounted as a pinned button, turning a 3-column popover into 4.\n * Counting from the registry instead is immune to this regardless of how\n * such an element manages its own visibility. `closeButton`/\n * `toolbarOverflowButton` were never in `pluginToolbarButtons`, so\n * excluding them by identity is no longer needed either.\n */\n private positionToolbarOverflowPanel(): void {\n if (!this.dom) return;\n const {\n dialog,\n toolbarOverflowButton,\n toolbarOverflowPanel,\n toolbarRight,\n captionToggleButton,\n } = this.dom;\n const dialogRight = dialog.getBoundingClientRect().right;\n const caretRight = toolbarOverflowButton.getBoundingClientRect().right;\n const paddingRight = parseFloat(getComputedStyle(toolbarOverflowPanel).paddingRight) || 0;\n const right = dialogRight - caretRight - paddingRight;\n toolbarOverflowPanel.style.right = `${Math.max(0, right)}px`;\n\n // A second real bug, reported from real usage on a video slide: still\n // over/undercounts, even immune to the DOM-parentage issue above.\n // Undercounts by not knowing about `captionToggleButton` at all — a\n // real, space-consuming, never-collapsible button that only exists on\n // video slides, added directly in dom.ts rather than through\n // `ctx.ui.toolbar()`, so it was never in `pluginToolbarButtons` to\n // begin with. Overcounts separately, in the other direction: a\n // registered button hidden for its own content reasons (e.g. Zoom's\n // zoomIn/zoomOut/actualSize buttons hiding themselves on video slides,\n // §4.6) still passes the parentage check — hidden means zero layout\n // size, so `measureToolbarOverflow()`'s collapse loop never needed to\n // move it into the panel, leaving it sitting in `toolbarRight`,\n // invisible but still counted as if it occupied a column. Both fixed\n // by also checking `!b.el.hidden`, and adding one more for\n // `captionToggleButton` whenever it's currently shown.\n const pinnedCount =\n this.pluginToolbarButtons.filter(\n (b) => b.slot === 'right' && b.el.parentElement === toolbarRight && !b.el.hidden,\n ).length + (captionToggleButton.hidden ? 0 : 1);\n const columnCount = pinnedCount + 1; // + the caret itself\n toolbarOverflowPanel.style.gridTemplateColumns = `repeat(${columnCount}, 44px)`;\n }\n\n private closeToolbarOverflow(): void {\n if (!this.dom || !this.toolbarOverflowOpen) return;\n document.removeEventListener('keydown', this.onToolbarOverflowKeydown, true);\n this.dom.toolbarOverflowPanel.hidden = true;\n this.dom.toolbarOverflowButton.setAttribute('aria-expanded', 'false');\n this.toolbarOverflowOpen = false;\n this.focusTrap.retarget(this.dom.dialog);\n this.toolbarOverflowReturnFocus?.focus({ preventScroll: true });\n this.toolbarOverflowReturnFocus = null;\n }\n\n private pluginOverlay(el: HTMLElement, layer?: number): Unsubscribe {\n if (layer !== undefined) el.style.zIndex = String(layer);\n this.dom!.dialog.appendChild(el);\n const unhover = this.wireControlHover(el);\n return () => {\n unhover();\n el.remove();\n };\n }\n\n /** DESIGN.md §2.8/§3 — pauses auto-hide while genuinely hovered: controls, caption, and any plugin overlay (`ctx.ui.overlay()`). Unsubscribe also corrects the set if removed mid-hover — a real risk for overlay content a plugin can toggle while the gallery stays open, unlike static buttons. */\n private wireControlHover(el: HTMLElement): Unsubscribe {\n this.hoverableElements.add(el);\n const onEnter = (): void => {\n this.hoveringElements.add(el);\n this.onActivity();\n };\n const onLeave = (): void => {\n if (!this.hoveringElements.delete(el)) return;\n this.scheduleAutoHide();\n };\n el.addEventListener('pointerenter', onEnter);\n el.addEventListener('pointerleave', onLeave);\n return () => {\n el.removeEventListener('pointerenter', onEnter);\n el.removeEventListener('pointerleave', onLeave);\n this.hoverableElements.delete(el);\n if (this.hoveringElements.delete(el)) this.scheduleAutoHide();\n };\n }\n\n /**\n * DESIGN.md §2.8 — paused only by a real *hover*. Used to also treat a\n * *focused* control as active via `document.activeElement`, but a click\n * leaves a `<button>` focused indefinitely, permanently blocking\n * `hideControls()`. Focus still counts as activity (`focusin` →\n * `onActivity()`), it just no longer blocks the eventual hide.\n */\n private isControlActive(): boolean {\n return this.hoveringElements.size > 0;\n }\n\n /**\n * Syncs `hoveringElements` to the browser's own live `:hover` truth for\n * every registered control — both directions, not just dropping a stale\n * entry: also picks up one that's genuinely hovered but never got a\n * `pointerenter` of its own (see `hoveringElements`'s own doc comment for\n * why either direction can happen). Whichever ends up true here is what\n * `isControlActive()` reads immediately after, in the same `onActivity()`\n * call — no separate reveal step needed for a newly-added element; that\n * call already does it for any activity, hover included. Cheap in\n * practice: `hoverableElements` is normally single digits, and `:hover`\n * matching is a native, already-computed browser check, not a\n * layout-triggering one.\n */\n private reconcileHover(): void {\n for (const el of this.hoverableElements) {\n if (el.matches(':hover')) this.hoveringElements.add(el);\n else this.hoveringElements.delete(el);\n }\n }\n\n /**\n * The thumbnail for `index` — what the zoom transition animates to/from,\n * and what `activeThumbnail` marks/scrolls-to. `data-shoji-id` is an\n * explicit opt-in that works in any mode (needed for dynamic mode, which\n * has no scanned DOM); checked first so it can override selector mode's\n * default `scannedElements[index]` too, e.g. to target an inner element.\n */\n getOriginElement(index: number): HTMLElement | null {\n const item = this.itemList[index];\n if (item?.id && typeof CSS !== 'undefined' && typeof CSS.escape === 'function') {\n const marked = document.querySelector<HTMLElement>(\n `[data-shoji-id=\"${CSS.escape(item.id)}\"]`,\n );\n if (marked) return marked;\n }\n return this.scannedElements[index] ?? null;\n }\n\n /** Lets a plugin's background-page `scrollIntoView()` survive `close()`'s scroll restore, routed through this shared instance rather than importing `bodyScrollLock` directly — required for the standalone core+plugins distribution, where a direct import would get its own disconnected module copy (DESIGN.md §4.2/§10). Same reasoning `getOriginElement`/`getActiveMedia` were made public for. */\n markIntentionalScroll(): void {\n markIntentionalScrollInternal();\n }\n\n private scheduleAutoHide(): void {\n if (this.autoHideTimer !== null) {\n clearTimeout(this.autoHideTimer);\n this.autoHideTimer = null;\n }\n if (!this.opened) return;\n if (this.autoHideDelay === false) return; // always visible — no timer to arm\n if (this.autoHideDelay === 0) {\n this.hideControls(); // 0 = hidden immediately, no reveal — see onActivity\n return;\n }\n this.autoHideTimer = setTimeout(() => this.hideControls(), this.autoHideDelay);\n }\n\n /**\n * Forces the same fade §2.8's idle timer would eventually trigger —\n * public so a plugin can hide controls on its own trigger. Same\n * `isControlActive()` guard as the timer. Also a no-op under\n * `autoHideDelay: false` — a real bug, reported from real usage: Autoplay's\n * tap-to-toggle-chrome behavior called this directly and ignored `false`\n * entirely, since only the idle timer checked it. `false` has to hold for\n * every caller, not just the timer. `forceHideControls()`/\n * `setControlsHiddenForDrag()` (close, drag-to-close) deliberately don't\n * check this — direct user actions with their own feedback, not auto-hide.\n */\n hideControls(): void {\n // A real bug, reported from real usage: an idle timer already ticking\n // down before the caption modal opened isn't cancelled by opening it\n // (DESIGN.md §2.3a) — `onActivity()`'s own `captionModalOpen` guard\n // only stops a *new* timer being armed while it's open, it doesn't stop\n // one already in flight from firing. Left unguarded here, that timer\n // could still call this mid-read, and `.shoji-controls-hidden`'s own\n // `cursor: none` (shoji.css) applies to the whole dialog including the\n // modal's own backdrop/panel — the cursor visibly vanishing over text\n // the viewer is actively reading, not idling on.\n //\n // A real bug, found testing this cross-browser: `hoveringElements` is\n // normally kept in sync by `reconcileHover()` running inside\n // `onActivity()`, on every real pointer event — but that only helps if\n // one actually fires *after* the pointer reaches wherever it's really\n // ending up. Confirmed directly: some browser pointer-simulation paths\n // (reproduced consistently under Firefox automation, though nothing\n // rules out a genuine — if rarer — real-hardware equivalent) fire their\n // last bubbling pointer event mid-transition, before the browser's own\n // `:hover` state has caught up to the pointer's actual final position —\n // reconciling *then* just recorded the wrong, stale answer, and with no\n // further pointer event ever arriving, nothing was left to correct it\n // before this timer fired. A plain counter would have the identical gap\n // (whatever event it's trusted to fire is the same one that didn't).\n // Fixed by re-reconciling right here too, immediately before the\n // decision that actually depends on the answer being fresh — this is\n // the one moment `hoveringElements` being even briefly stale actually\n // matters, so it's also the one moment worth an extra, authoritative\n // check instead of trusting whatever the last activity event happened\n // to leave behind.\n this.reconcileHover();\n if (\n !this.dom ||\n this.autoHidden ||\n this.isControlActive() ||\n this.autoHideDelay === false ||\n this.captionModalOpen ||\n // DESIGN.md §3.1a — same reasoning as `captionModalOpen` immediately\n // above: an idle timer already ticking down before the toolbar\n // overflow popover opened isn't cancelled by opening it, and the\n // popover is anchored to (visually part of) the toolbar this would\n // hide out from under it.\n this.toolbarOverflowOpen\n )\n return;\n this.autoHidden = true;\n this.dom.dialog.classList.add('shoji-controls-hidden');\n this.bus.emit('controls:hide', {});\n }\n\n private showControls(): void {\n if (!this.dom || !this.autoHidden) return;\n this.autoHidden = false;\n this.dom.dialog.classList.remove('shoji-controls-hidden', 'shoji-controls-hidden-for-close');\n this.bus.emit('controls:show', {});\n }\n\n /**\n * DESIGN.md §2.1 — `caption` is `string | HTMLElement | DangerousHtmlCaption`:\n * a plain string renders as text (`textContent`, auto-escaped, safe by\n * default); an `HTMLElement` is appended as real DOM (the host built it);\n * `{ dangerouslySetInnerHTML }` renders raw, unescaped HTML via `innerHTML`\n * — Shoji does not sanitize it, the host must, same contract as React's\n * identically-named escape hatch. Returns whether the caption element\n * should be hidden.\n */\n private renderCaption(\n el: HTMLElement,\n caption: string | HTMLElement | DangerousHtmlCaption | undefined,\n ): boolean {\n if (caption instanceof HTMLElement) {\n el.replaceChildren(caption);\n return false;\n }\n if (isDangerousHtmlCaption(caption)) {\n el.innerHTML = caption.dangerouslySetInnerHTML;\n return caption.dangerouslySetInnerHTML === '';\n }\n el.textContent = caption ?? '';\n return !caption;\n }\n\n /**\n * `.shoji-toolbar-button` only — never `.shoji-close`/`.shoji-nav`, which\n * are different classes entirely, so this can't accidentally block\n * closing or navigating away from a slow-loading slide. `tabIndex = -1`\n * (not just the CSS below) so a keyboard user tabbing through the\n * toolbar skips these entirely while disabled, not just visually dims\n * them — `pointer-events: none` alone wouldn't stop Enter/Space\n * activating an already-focused button.\n */\n private setSlideLoading(loading: boolean): void {\n if (!this.dom) return;\n this.dom.outer.classList.toggle('shoji-slide-loading', loading);\n for (const button of this.dom.outer.querySelectorAll<HTMLButtonElement>(\n '.shoji-toolbar-button',\n )) {\n button.ariaDisabled = loading ? 'true' : null;\n if (loading) button.tabIndex = -1;\n else button.removeAttribute('tabindex');\n }\n }\n\n /**\n * DESIGN.md §2.5 — the caption's disappear-then-reappear fits inside the\n * *same* `--shoji-duration` window the slide's own mode animation runs\n * in, not a separate one after it: fades out over the first half, swaps\n * content invisibly at the midpoint, fades back in over the second half\n * — timed to finish exactly when the mode animation does, not still\n * catching up a beat behind it. Self-timed off the resolved\n * `--shoji-duration` value rather than hooked to the mode animation's\n * own completion event: both are driven by the same duration either way,\n * and the fade-in's own completion is what has to line up, which a\n * \"start on completion, then animate\" hook can't give — by definition,\n * a fade started only once something else finishes hasn't finished\n * *with* it.\n */\n private transitionCaption(): void {\n const dom = this.dom;\n if (!dom) return;\n if (\n typeof window.matchMedia === 'function' &&\n window.matchMedia('(prefers-reduced-motion: reduce)').matches\n ) {\n return; // --shoji-duration is already 0ms; nothing to sequence\n }\n\n // .shoji-caption's own `transition: opacity var(--shoji-duration) ...`\n // rule transitions a single property, so the computed shorthand\n // resolves to exactly --shoji-duration's own value — same read as\n // `zoomTransition.ts`/`SlideTransition.ts` already use elsewhere,\n // rather than `getPropertyValue('--shoji-duration')` directly.\n const raw = getComputedStyle(dom.caption).transitionDuration.split(',')[0]?.trim() ?? '';\n const fullMs = raw.endsWith('ms')\n ? parseFloat(raw)\n : raw.endsWith('s')\n ? parseFloat(raw) * 1000\n : 0;\n const half = fullMs / 2;\n\n this.captionFadePending = true;\n dom.caption.style.transitionDuration = `${half}ms`;\n dom.caption.style.opacity = '0';\n\n setTimeout(() => {\n if (!this.dom) return;\n this.captionFadePending = false;\n this.updateCaptionVisibility();\n this.dom.caption.style.opacity = '';\n setTimeout(() => {\n // Only the temporary half-duration override — restores the\n // caption's normal full-duration transition (e.g. for idle\n // auto-hide) once this fade-in has actually finished with it.\n if (this.dom) this.dom.caption.style.transitionDuration = '';\n }, half);\n }, half);\n }\n\n /**\n * Content is always kept current (correct the instant loading finishes,\n * no text flash) — only `hidden` also gates on `isActiveReady()`, so a\n * caption for the *new* slide can't sit there readable while the\n * image/video it describes is still a spinner.\n */\n private updateCaptionVisibility(): void {\n if (!this.dom || !this.slides) return;\n const item = this.itemList[this.activeIndex];\n const noCaption = this.renderCaption(this.dom.caption, item?.caption);\n const isVideo = !!item?.video;\n this.dom.caption.classList.toggle('shoji-caption--video', isVideo);\n const hiddenByToggle = isVideo && !this.captionVisibleOnVideo;\n this.dom.caption.hidden = noCaption || !this.slides.isActiveReady() || hiddenByToggle;\n\n const btn = this.dom.captionToggleButton;\n btn.hidden = !isVideo || noCaption;\n if (!btn.hidden) {\n btn.setAttribute('aria-pressed', String(this.captionVisibleOnVideo));\n btn.ariaLabel = btn.title = this.captionVisibleOnVideo\n ? this.locale.hideCaption\n : this.locale.showCaption;\n }\n this.updateCaptionTruncation();\n // A real bug, reported from real usage: reopening an already-loaded\n // slide (e.g. clicking the same thumbnail twice) skips the async\n // image-decode path entirely — nothing re-runs this measurement once\n // the dialog actually finishes laying out, so the synchronous call\n // above can catch it mid-layout (still effectively 0×0 at that exact\n // point, even though `hidden` itself is already correctly false) and\n // wrongly conclude nothing overflows. A fresh *first* open happens to\n // dodge this because real image decode work is slow enough that by the\n // time it resolves, layout has already settled — not something a\n // reopen (cached, near-instant) can rely on. Re-checking one frame\n // later, after layout has actually been committed, catches the case the\n // synchronous read above can miss without regressing it (the sync call\n // already got it right whenever it can, so this is a rarely-needed\n // correction, not a routine double-measurement).\n if (this.captionTruncationFrame !== null) cancelAnimationFrame(this.captionTruncationFrame);\n this.captionTruncationFrame = requestAnimationFrame(() => {\n this.captionTruncationFrame = null;\n this.updateCaptionTruncation();\n });\n }\n\n /**\n * DESIGN.md §2.3a — the caption's own default height cap (above) already\n * keeps it clear of the toolbar, but says nothing about the vertically-\n * centered prev/next nav arrows sharing its same left edge; a long\n * enough caption could still grow up over one of those. `--shoji-*`\n * collapses it to roughly one line by default (shoji.css) regardless, so\n * this only ever needs to detect whether that collapse actually clipped\n * something — `scrollHeight > clientHeight` after layout, the same\n * technique already used elsewhere in this codebase (and its own tests)\n * to detect caption overflow. Marks it truncated/interactive only when\n * there's genuinely more to read; a caption that already fits shouldn't\n * look or behave clickable.\n *\n * A real bug, reported from real usage: the first version only capped\n * `max-height` + `overflow: hidden` — a plain pixel clip with no regard\n * for where a line of text actually ends, so the cutoff routinely sliced\n * straight through the middle of the last visible line instead of\n * stopping at a clean line boundary, reading as broken rather than\n * intentionally truncated.\n *\n * Two follow-up attempts, both also wrong, both worth recording so they\n * aren't retried: `-webkit-line-clamp` never cleanly stopped at a line\n * boundary here regardless of the line count fed into it. Replacing it\n * with `lines * lineHeight + paddingY` arithmetic (using\n * `getComputedStyle(el).lineHeight`) still left a sliver of the next\n * line visible in a real browser — confirmed by screenshot, not just\n * this sandbox. Root cause: a browser doesn't necessarily lay out N\n * stacked lines at exactly N times the CSS `line-height` value: text\n * layout does its own sub-pixel rounding per line, so arithmetic\n * multiplication drifts from the real rendered geometry by enough to\n * expose part of an extra line, and no fixed safety margin is correct\n * for every font/zoom/line-count combination.\n *\n * This version doesn't compute line boundaries at all — it reads them\n * straight from the browser's own layout via `Range.getClientRects()`,\n * which returns one rect per actual rendered line fragment (rich\n * captions with inline markup can put more than one rect on the same\n * visual row; each is handled independently below rather than grouped,\n * since only the topmost/bottommost edges per row matter here). The cap\n * is set to the bottom edge of the last line whose bottom still fits\n * inside the current (arbitrary, CSS-calc'd) height budget — a real\n * measured boundary, never an assumed one.\n *\n * A third real bug, caught after switching to this measured approach:\n * padding the box out by a full `padding-bottom` past that last line's\n * *measured* bottom still isn't safe, because line boxes butt up much\n * closer together than the padding value — the next (excluded) line's\n * own top can fall well inside that padding band, so its glyph\n * ascenders paint into what was supposed to be empty breathing room.\n * Fixed by also finding that next line's top and never letting the cap\n * reach it, regardless of how much of `padding-bottom` that leaves.\n */\n private updateCaptionTruncation(): void {\n if (!this.dom) return;\n const el = this.dom.caption;\n el.style.removeProperty('max-height');\n const truncated = !el.hidden && el.scrollHeight > el.clientHeight;\n if (truncated) {\n const paddingBottom = parseFloat(getComputedStyle(el).paddingBottom) || 0;\n const budget = el.clientHeight - paddingBottom;\n const elTop = el.getBoundingClientRect().top;\n const range = document.createRange();\n range.selectNodeContents(el);\n let lastFittingBottom = 0;\n let nextLineTop = Infinity;\n for (const rect of range.getClientRects()) {\n if (rect.height === 0) continue;\n const relTop = rect.top - elTop;\n const relBottom = rect.bottom - elTop;\n if (relBottom <= budget) {\n if (relBottom > lastFittingBottom) lastFittingBottom = relBottom;\n } else if (relTop < nextLineTop) {\n nextLineTop = relTop;\n }\n }\n const cutBottom = lastFittingBottom > 0 ? lastFittingBottom : budget;\n const desired = cutBottom + paddingBottom;\n const cap = Number.isFinite(nextLineTop) ? Math.min(desired, nextLineTop) : desired;\n el.style.maxHeight = `${Math.max(0, Math.floor(cap) - 1)}px`;\n }\n el.classList.toggle('shoji-caption--truncated', truncated);\n if (truncated) {\n el.tabIndex = 0;\n el.setAttribute('role', 'button');\n el.setAttribute('aria-haspopup', 'dialog');\n } else {\n el.removeAttribute('tabindex');\n el.removeAttribute('role');\n el.removeAttribute('aria-haspopup');\n if (this.captionModalOpen) this.closeCaptionModal();\n }\n }\n\n private readonly onCaptionActivate = (event: Event): void => {\n if (!this.dom?.caption.classList.contains('shoji-caption--truncated')) return;\n this.captionModalOpenedViaKeyboard = event instanceof KeyboardEvent;\n if (event instanceof KeyboardEvent) {\n if (event.key !== 'Enter' && event.key !== ' ') return;\n event.preventDefault();\n // A real bug, reported from real usage against Autoplay's\n // pauseOnCaptionExpand (§4.1 point 18): a *keyboard* activation here\n // (Tab+Enter/Space, the only path this branch actually runs for)\n // must not also bubble up to onKeydown's shortcut dispatch — this is\n // unambiguously \"activate this control,\" not also a global shortcut\n // key, same reasoning the caption modal's own keydown handler\n // already uses to isolate itself from onKeydown. This alone doesn't\n // cover the click path below reopening on a *later*, unrelated\n // keypress — see `captionModalOpenedViaKeyboard`'s own doc comment\n // for that half of the fix.\n event.stopPropagation();\n } else {\n // A mouse-drag that ended in a real text selection is not a click to\n // open — `.shoji-caption` is deliberately excluded from drag-to-\n // navigate (GestureController's INTERACTIVE_CONTROL_SELECTOR) so the\n // viewer can select/copy caption text; without this check, finishing\n // that selection would also toggle the modal open on every drag.\n const selection = window.getSelection();\n if (selection && !selection.isCollapsed) return;\n // A link/button inside a rich-HTML caption keeps its own click\n // behavior — this only opens the modal for a click on the caption\n // itself, not one that's actually aimed at interactive content\n // inside it.\n if (event.target instanceof Element && event.target.closest('a, button')) return;\n }\n this.openCaptionModal();\n };\n\n /**\n * DESIGN.md §2.3a — shows the full caption (re-rendered via the same\n * `renderCaption()` the truncated one already used, so a rich-HTML\n * caption's own links/formatting are identical in both places) in a\n * small nested dialog, scrollable if it's still taller than the\n * viewport allows. `FocusTrap.retarget()` narrows Tab-cycling to just\n * this modal without touching the outer trap's own focus-restore state\n * (see its own doc comment for why a second `FocusTrap` instance isn't\n * used instead). Requested directly: the modal *replaces* the truncated\n * caption rather than just visually sitting on top of it — hiding\n * `dom.caption` too, not only because the modal's own opaque backdrop\n * already covers it, but so it can't still be reached by a screen\n * reader's browse-mode cursor (unlike Tab, `FocusTrap`/`retarget()`\n * don't affect that) while a completely different dialog is the one\n * actually open.\n */\n private openCaptionModal(): void {\n if (!this.dom || this.captionModalOpen) return;\n const item = this.itemList[this.activeIndex];\n this.renderCaption(this.dom.captionModalContent, item?.caption);\n this.dom.captionModal.hidden = false;\n // Captured before hiding the caption below (a real bug, caught by e2e\n // regression: hiding the currently-focused caption blurs it immediately\n // — a hidden element can't hold focus — so reading `activeElement`\n // *after* that line ended up capturing wherever the browser's own\n // focus-loss fallback landed (the dialog), not the caption itself.\n // Escape/close-button/backdrop then \"restored\" focus to the dialog\n // instead of back to the caption that was actually focused when this\n // opened.\n this.captionModalReturnFocus = document.activeElement as HTMLElement | null;\n this.dom.caption.hidden = true;\n // A real bug, caught testing this: the click that opens the modal\n // typically leaves the pointer sitting right over the caption —\n // hiding it out from under a *stationary* cursor never fires the\n // `pointerleave` `wireControlHover()` needs to drop it from\n // `hoveringElements` (browsers only fire that on actual pointer\n // movement past an element's edge, not on the element disappearing).\n // `reconcileHover()` would eventually self-heal this on the next real\n // pointer move anyway, but nothing meaningful should still read as\n // \"hovered\" once the modal has taken over — clearing it immediately\n // here is more deterministic than waiting for that.\n this.hoveringElements.clear();\n this.captionModalOpen = true;\n this.focusTrap.retarget(this.dom.captionModalPanel);\n document.addEventListener('keydown', this.onCaptionModalKeydown, true);\n this.bus.emit('captionModalChange', { open: true });\n }\n\n /**\n * Requested directly: closing should leave the viewer looking at a fully\n * normal, fully visible gallery — not just the modal gone, but the\n * truncated caption back (via `updateCaptionVisibility()`, which\n * recomputes its real hidden state from scratch rather than blindly\n * flipping a flag back — the more robust \"recompute from source of\n * truth\" this codebase already prefers elsewhere) and any auto-hidden\n * toolbar/nav explicitly re-shown (`onActivity()`, same pairing every\n * other real interaction already uses) rather than left hidden until\n * the viewer happens to move the mouse.\n */\n private closeCaptionModal(): void {\n if (!this.dom || !this.captionModalOpen) return;\n document.removeEventListener('keydown', this.onCaptionModalKeydown, true);\n this.dom.captionModal.hidden = true;\n this.captionModalOpen = false;\n this.updateCaptionVisibility();\n this.focusTrap.retarget(this.dom.dialog);\n if (this.captionModalOpenedViaKeyboard) {\n this.captionModalReturnFocus?.focus({ preventScroll: true });\n }\n this.captionModalReturnFocus = null;\n this.captionModalOpenedViaKeyboard = false;\n this.onActivity();\n this.bus.emit('captionModalChange', { open: false });\n }\n\n private renderCurrentSlide(openPlaceholderSrc?: string): void {\n if (!this.slides || !this.dom) return;\n const dom = this.dom;\n this.slides.render(\n this.itemList,\n this.activeIndex,\n (loadedIndex) => {\n if (loadedIndex === this.activeIndex) {\n this.bus.emit('slideItemLoad', { index: loadedIndex });\n this.setSlideLoading(false);\n // captionFadePending: skip here too, not just the synchronous\n // call below — decode() resolves on a microtask, which can win\n // the race against the mode animation's own onComplete (the one\n // that's actually supposed to update it), especially for an\n // already-preloaded slide.\n if (!this.captionFadePending) this.updateCaptionVisibility();\n }\n },\n openPlaceholderSrc,\n this.loop,\n );\n this.setSlideLoading(!this.slides.isActiveReady());\n\n const item = this.itemList[this.activeIndex];\n const total = this.itemList.length;\n dom.counter.textContent = total > 0 ? `${this.activeIndex + 1} / ${total}` : '';\n dom.counter.hidden = !this.showCounter || total === 0;\n // navigate()'s animated path already faded the outgoing caption out\n // and updates content itself, once that fade (and the slide's own mode\n // transition) actually finishes — updating it here too, immediately,\n // would flash the new caption's text mid-fade instead of the outgoing\n // one.\n if (!this.captionFadePending) this.updateCaptionVisibility();\n const label = `Image ${this.activeIndex + 1} of ${total}${item?.alt ? `: ${item.alt}` : ''}`;\n this.liveRegion.announce(label);\n\n dom.prevButton.hidden = total <= 1;\n dom.nextButton.hidden = total <= 1;\n dom.prevButton.disabled = !this.loop && this.activeIndex <= 0;\n dom.nextButton.disabled = !this.loop && this.activeIndex >= total - 1;\n }\n\n open(index = this.options.index ?? 0): void {\n if (this.destroyed || this.opened) return;\n this.bus.emit('beforeOpen', { index });\n this.ensureLightbox();\n this.opened = true;\n this.activeIndex = index;\n this.captionVisibleOnVideo = !!this.options.showVideoCaption;\n lockBodyScroll();\n const origin = this.getOriginElement(index);\n // Both the open placeholder and the zoom-in animation below need a size\n // to show/grow toward before the real image has loaded — without\n // item.width/height, that size can only ever be guessed (\"probably\n // fills the dialog\"), which is exactly what visibly overshoots for a\n // genuinely small photo (§2.3/§2.3b's own real-bug history). Rather\n // than guess, skip both entirely when naturalSize is unknown: the\n // ordinary spinner shows, and the real image just appears once it's\n // ready — no animation of its own, deliberately (same \"no second,\n // disconnected transition\" reasoning the placeholder-to-real-image\n // handoff already uses elsewhere).\n const naturalSize = this.resolveNaturalSize(index);\n // The first slide's caption fades in alongside zoomIn() the same way a\n // navigated-to one fades in alongside the mode transition (§2.5) —\n // only when there's actually a zoom to sync with, same \"no second,\n // disconnected transition\" reasoning as the comment above: nothing\n // grows in from the thumbnail without known dimensions, so nothing\n // should fade in disconnected from that either.\n const fadeInOnOpen =\n !!(origin && naturalSize) &&\n !(\n typeof window.matchMedia === 'function' &&\n window.matchMedia('(prefers-reduced-motion: reduce)').matches\n );\n if (fadeInOnOpen && this.dom) {\n this.captionFadePending = true;\n this.dom.caption.style.opacity = '0';\n }\n // A real bug/inconsistency: a video item *does* have a known naturalSize\n // (its aspect ratio), but the low-res placeholder this unlocks is a\n // dead end — it's a static photo, immediately replaced by something\n // entirely different (an error state, or the real embed), not the same\n // image sharpening the way a photo's placeholder-to-full-res handoff\n // is. That made a fresh open() inconsistent with navigating back to an\n // already-cached video slide (no placeholder shown there at all, since\n // navigate() never passes one) — same \"no second, disconnected\n // transition\" reasoning as the naturalSize-unknown case above, just\n // triggered by content type instead of missing dimensions.\n this.renderCurrentSlide(\n naturalSize && !this.itemList[index]?.video\n ? this.resolveOpenPlaceholderSrc(this.itemList[index], origin)\n : undefined,\n );\n this.dom!.outer.classList.add('shoji-open');\n document.addEventListener('keydown', this.onKeydown);\n this.focusTrap.activate(this.dom!.dialog);\n this.scheduleAutoHide();\n\n const media = this.slides?.getActiveMedia();\n if (media && origin && naturalSize) {\n zoomIn({\n origin,\n target: media,\n aspectRatio: this.resolveAspectRatio(index, origin),\n naturalSize,\n });\n }\n if (fadeInOnOpen && this.dom) {\n // No half-duration split here, unlike transitionCaption() — there's\n // no outgoing caption to fade out first, just this one fading in\n // from nothing, over the same full --shoji-duration zoomIn() itself\n // runs on.\n this.captionFadePending = false;\n this.updateCaptionVisibility();\n this.dom.caption.style.opacity = '';\n }\n\n this.bus.emit('open', { index });\n this.bus.emit('afterOpen', { index });\n }\n\n /** DESIGN.md §2.3 — low-res open() placeholder source, checked in order: item.thumb, a live data-shoji-thumb on origin, else origin's own rendered <img>. */\n private resolveOpenPlaceholderSrc(\n item: GalleryItem | undefined,\n origin: HTMLElement | null,\n ): string | undefined {\n if (item?.thumb) return item.thumb;\n const dataThumb = origin?.getAttribute('data-shoji-thumb');\n if (dataThumb) return dataThumb;\n const img = origin?.querySelector('img');\n return img?.currentSrc || img?.src || undefined;\n }\n\n /**\n * DESIGN.md §2.2/§2.6 — navigates while open; always clamps an explicit\n * out-of-range index (this is a directed jump, not a step — `loop` only\n * affects `next()`/`prev()`, see below). No-op if not open. `animate`\n * (default `true`) runs the configured §2.5 transition; hosts that want\n * an instant jump — e.g. syncing to an external index without a visual\n * flourish — can pass `{ animate: false }`.\n */\n goTo(index: number, options?: { animate?: boolean }): void {\n if (this.destroyed || !this.opened || this.itemList.length === 0) return;\n const target = this.clampToRange(index);\n if (target === this.activeIndex) return;\n const direction: 1 | -1 = target > this.activeIndex ? 1 : -1;\n this.navigate(target, direction, options?.animate ?? true);\n }\n\n next(): void {\n this.navigate(this.nextIndex(), 1, true);\n }\n\n prev(): void {\n this.navigate(this.prevIndex(), -1, true);\n }\n\n private nextIndex(): number {\n const atEnd = this.activeIndex >= this.itemList.length - 1;\n return atEnd && this.loop ? 0 : this.activeIndex + 1;\n }\n\n private prevIndex(): number {\n const atStart = this.activeIndex <= 0;\n return atStart && this.loop ? this.itemList.length - 1 : this.activeIndex - 1;\n }\n\n /**\n * DESIGN.md §2.5 — the shared path behind `goTo()`/`next()`/`prev()`.\n * `animate` is `false` only for `GestureController`'s own host callbacks\n * (see `ensureLightbox()`): a gesture-completed swipe already played its\n * own live-drag/settle animation, so running a *second*, `mode`-based\n * transition on top of it would visibly double-animate. Every other\n * caller — buttons, keyboard, autoplay, a plugin calling `goTo()` — gets\n * the real, configured transition.\n */\n private navigate(target: number, direction: 1 | -1, animate: boolean): void {\n if (this.destroyed || !this.opened || this.itemList.length === 0) return;\n const clamped = this.clampToRange(target);\n if (clamped === this.activeIndex) return;\n // DESIGN.md §2.3a — the modal shows a specific slide's caption; leaving\n // it open across a navigation (arrow keys reach it even while the\n // gesture gate above suspends drag-navigate) would keep showing the\n // outgoing slide's text over the new slide.\n if (this.captionModalOpen) this.closeCaptionModal();\n // DESIGN.md §3.1a — the popover's own position depends on the current\n // toolbar height, which can change slide-to-slide (e.g. a video slide's\n // caption-toggle button appearing/disappearing); simplest to just close\n // it rather than reposition it mid-navigation.\n if (this.toolbarOverflowOpen) this.closeToolbarOverflow();\n const from = this.activeIndex;\n // beforeSlide fires first so a listener (e.g. Autoplay) can detach its\n // own 'pause' listener from the outgoing video before pauseMedia() below\n // triggers a native 'pause' event on it — otherwise that listener is\n // still attached and misreads this programmatic pause as the viewer\n // manually pausing the video, a real regression this caused once\n // (advancing past a stuck video called next(), which paused it here,\n // which the still-attached listener treated as a manual pause and\n // stopped the whole slideshow before the new slide's afterSlide handler\n // ever got a chance to run). activeIndex hasn't changed yet at this\n // point, so getActiveMedia() still resolves to the outgoing slide either\n // way — only the listener-detach ordering relative to pauseMedia matters.\n this.bus.emit('beforeSlide', { from, to: clamped });\n // A video started by the viewer keeps playing otherwise — this slide\n // stays cached (still within `preload`), and only pausing (not\n // releasing) lets it resume right where it left off if revisited.\n pauseMedia(this.slides?.getActiveMedia() ?? null);\n this.activeIndex = clamped;\n\n if (animate && this.transition) {\n // Not layered onto a gesture-completed swipe (the `else` branch) —\n // same scoping as the mode transition itself (§2.4/§2.5).\n this.transitionCaption();\n const swap = (): void => this.renderCurrentSlide();\n const modeName = this.resolveTransitionMode();\n const builtin = TRANSITION_PRESETS[modeName];\n if (builtin) {\n this.transition.animate(builtin, direction, swap);\n } else {\n this.transition.animateCustom(modeName, direction, swap);\n }\n } else {\n const swap = (): void => this.renderCurrentSlide();\n swap();\n }\n\n this.bus.emit('afterSlide', { from, to: clamped });\n }\n\n private resolveTransitionMode(): string {\n return this.options.mode ?? 'slide';\n }\n\n close(): void {\n this.beginClose();\n }\n\n /**\n * DESIGN.md §2.4/§2.6a — same effect as `close()`, from a completed\n * vertical swipe. `frozenDrag` is threaded through to `zoomOut()`'s own\n * `dragStart`, so the whole close continues as one motion from exactly\n * where the drag left off (see `GestureController.\n * takeFrozenDragTransform`).\n */\n private closeFromSwipe(frozenDrag: FrozenDragTransform): void {\n this.beginClose(frozenDrag);\n }\n\n /**\n * `frozenDrag` — see `closeFromSwipe()`'s doc comment; absent for a\n * button-close, which has no drag to continue from. Controls fade and the\n * zoom-out run concurrently, both starting the instant `forceHideControls()`\n * runs — not sequenced one after the other. (A previous version waited for\n * the controls' own fade to fully finish before starting the zoom-out, for\n * a button-close specifically — requested directly, to avoid stationary\n * chrome hovering over an already-shrinking photo. Reversed on later,\n * explicit feedback: waiting read as two distinct steps rather than one\n * motion; starting both together still avoids stationary chrome, since\n * the chrome is disappearing too, just without the pause.)\n */\n private beginClose(frozenDrag?: FrozenDragTransform): void {\n if (this.destroyed || !this.opened || this.isClosing) return;\n // DESIGN.md §2.3a — every close path (button, swipe, Escape, backdrop\n // click, destroy-while-open) funnels through here; the modal must not\n // linger past the lightbox itself closing, and its own document-level\n // keydown listener needs removing before that.\n if (this.captionModalOpen) this.closeCaptionModal();\n if (this.toolbarOverflowOpen) this.closeToolbarOverflow();\n // Read before beforeClose fires — the Zoom plugin's own beforeClose\n // handler resets its scale/pan back to neutral there (so zoomOut()'s\n // measurement below stays correct, same reasoning dragStart's own doc\n // comment explains), which would otherwise erase the very state this\n // is capturing.\n const zoomStart = this.zoomStartProvider?.() ?? undefined;\n this.bus.emit('beforeClose', {});\n\n // .shoji-outer must stay display:block for the zoom-out to be visible,\n // so the actual state flip (isOpen, focus restore, etc.) is deferred\n // until the animation finishes — see finishClose(). isClosing guards\n // against a second close() (e.g. a repeated Escape) re-triggering the\n // animation mid-flight while isOpen is still (deliberately) true.\n this.isClosing = true;\n\n const media = this.slides?.getActiveMedia();\n // close() never tears down the slide pool (reopening should be\n // instant), so nothing else stops a viewer-started video just because\n // the lightbox is now hidden.\n pauseMedia(media ?? null);\n\n const origin = this.getOriginElement(this.activeIndex);\n const naturalSize = this.resolveNaturalSize(this.activeIndex);\n // Real content already rendered doesn't need naturalSize at all —\n // effectiveTargetBox() (zoomTransition.ts) measures it directly, no\n // guessing involved. Only closing before it ever finished loading (no\n // real content, no naturalSize to fall back on either) would have\n // nothing but a guessed target to shrink toward — same \"don't guess\"\n // reasoning open() above already applies to its own animation.\n if (media && origin && (this.slides?.isActiveReady() || naturalSize)) {\n const aspectRatio = this.resolveAspectRatio(this.activeIndex, origin);\n this.forceHideControls();\n // Starts at the same instant as the zoom-out and the controls fade —\n // see the comment on .shoji-backdrop's own transition (shoji.css) for\n // why this can't just be left to the instant display:none cut at the\n // end of finishClose().\n if (this.dom) this.dom.backdrop.style.opacity = '0';\n zoomOut(\n { origin, target: media, aspectRatio, naturalSize, dragStart: frozenDrag, zoomStart },\n () => this.finishClose(),\n );\n } else {\n this.finishClose();\n }\n }\n\n /**\n * Forces the same fade §2.8's idle timer would eventually trigger, bypassing `hideControls()`'s own `isControlActive()` hover guard — the most common close path (clicking close) is hovering a control at this exact instant, and a deliberate close should hide regardless. No-op if already hidden.\n *\n * Also marks `.shoji-controls-hidden-for-close` alongside the ordinary\n * class — a real bug, reported from real usage: a plugin's own overlay\n * (Autoplay's progress bar, `autoplay.css`) was wired to fade on plain\n * `.shoji-controls-hidden`, the same class *ordinary idle auto-hide* also\n * applies — so it faded out and stayed gone through every idle period\n * too, not just the close animation the original request was actually\n * about. That went unnoticed for a while because until recently, tapping\n * the image toggled play/pause *and* revealed controls on the same tap,\n * papering over how often it was actually gone; once that tap-to-toggle\n * was removed (§4.1 point 15) as a separate, unrelated fix, an idle\n * slideshow now visibly loses its own progress indicator and never gets\n * it back without an unrelated interaction — surfacing this as a real\n * regression in practice, even though no code touching this class had\n * changed. This second class is the hook a plugin's CSS can key off\n * specifically for \"closing,\" without it ever matching ordinary idle-hide.\n */\n private forceHideControls(): void {\n if (!this.dom || this.autoHidden) return;\n this.autoHidden = true;\n this.dom.dialog.classList.add('shoji-controls-hidden', 'shoji-controls-hidden-for-close');\n this.bus.emit('controls:hide', {});\n }\n\n /**\n * DESIGN.md §2.4/§2.8 — `GestureController`'s live vertical-drag cue: hide\n * past the same distance a release would close, reveal again on retreat.\n * `hidden: false` only reveals if visible when *this* gesture started\n * (`controlsHiddenAtGestureStart`) — shouldn't resurrect controls already\n * separately hidden (idle, or `autoHideDelay: 0`) before the drag began.\n * `dragCloseThreshold` (bus event) fires on every crossing regardless of\n * that guard — Autoplay (§4-autoplay) needs to know about the crossing\n * itself, not just whether controls visibly moved.\n */\n private setControlsHiddenForDrag(hidden: boolean): void {\n this.controlsHiddenByDrag = hidden;\n if (hidden) this.forceHideControls();\n else if (!this.controlsHiddenAtGestureStart) this.showControls();\n // A real bug, reported from real usage: `.shoji-controls-hidden` also\n // hides the cursor (shoji.css) — fine for an idle/inactive hide, but\n // this hide happens *during* an active drag, so the cursor vanished\n // right when the viewer most needs to see it. This marker class scopes\n // a cursor override to specifically this case (see shoji.css).\n this.dom?.dialog.classList.toggle('shoji-controls-hidden-for-drag', hidden);\n this.bus.emit('dragCloseThreshold', { hidden });\n }\n\n /** `item.width`/`height`, else origin's `naturalWidth`/`naturalHeight` (accurate when `item.thumb` is unset). Feeds `computeTransform`'s letterbox-aware sizing. */\n private resolveAspectRatio(index: number, origin: HTMLElement | null): number | undefined {\n const item = this.itemList[index];\n if (item?.width && item.height) return item.width / item.height;\n const thumbImg = origin?.querySelector('img');\n if (thumbImg?.naturalWidth && thumbImg.naturalHeight) {\n return thumbImg.naturalWidth / thumbImg.naturalHeight;\n }\n return undefined;\n }\n\n /**\n * `item.width`/`height` only — deliberately never a thumbnail's own\n * `naturalWidth`/`naturalHeight` the way `resolveAspectRatio` above will:\n * that's a fine stand-in for *shape*, but using it as the real photo's\n * true pixel size would under-cap a genuinely large photo down to\n * thumbnail resolution. `undefined` here just means \"genuinely unknown,\"\n * not \"assume small\" — `zoomTransition.ts`'s `containedBox` already\n * treats it that way (no cap applied at all).\n */\n private resolveNaturalSize(index: number): { width: number; height: number } | undefined {\n const item = this.itemList[index];\n return item?.width && item.height ? { width: item.width, height: item.height } : undefined;\n }\n\n /**\n * Idempotent on purpose: a pending zoom-out's `transitionend`/fallback\n * timeout can still fire after `destroy()` has already force-finished the\n * close (§ destroy() below) — the `!this.opened` guard makes that a no-op\n * instead of a second `close`/`afterClose` emission on a torn-down gallery.\n */\n private finishClose(): void {\n if (!this.opened) return;\n this.isClosing = false;\n this.opened = false;\n unlockBodyScroll();\n document.removeEventListener('keydown', this.onKeydown);\n this.focusTrap.deactivate();\n this.dom?.outer.classList.remove('shoji-open');\n // Cleared, not left at '0' — a fresh open() must start fully opaque\n // again, not still faded out from the last close.\n if (this.dom) this.dom.backdrop.style.opacity = '';\n if (this.autoHideTimer !== null) {\n clearTimeout(this.autoHideTimer);\n this.autoHideTimer = null;\n }\n this.autoHidden = false;\n this.hoveringElements.clear();\n this.controlsHiddenByDrag = false;\n this.dom?.dialog.classList.remove('shoji-controls-hidden', 'shoji-controls-hidden-for-close');\n // A completed drag-close never calls setControlsHiddenForDrag(false)\n // (nothing left to un-hide for) — clean up the marker here instead, so\n // it can't linger into the next open().\n this.dom?.dialog.classList.remove('shoji-controls-hidden-for-drag');\n this.bus.emit('close', {});\n this.bus.emit('afterClose', {});\n }\n\n /** DESIGN.md §2.1 — diffs by id (fallback src), preserving the active slide. */\n updateSlides(items: GalleryItemInput[], currentIndex?: number): void {\n if (this.destroyed) return;\n\n const activeItem = this.itemList[this.activeIndex];\n const activeKey = activeItem ? itemKey(activeItem) : undefined;\n this.itemList = resolveDynamicVideoItems(items);\n\n let nextIndex = currentIndex;\n if (nextIndex === undefined && activeKey !== undefined) {\n const preserved = items.findIndex((item) => itemKey(item) === activeKey);\n if (preserved !== -1) nextIndex = preserved;\n }\n this.activeIndex = Math.min(\n Math.max(nextIndex ?? this.activeIndex, 0),\n Math.max(items.length - 1, 0),\n );\n\n if (this.opened) this.renderCurrentSlide();\n this.bus.emit('itemsUpdated', { items: this.itemList });\n }\n\n /**\n * Sugar over `updateSlides()` for the common \"insert some items\" case —\n * splices `items` into a copy of the current list at `atIndex` (default:\n * append at the end) and hands the result to `updateSlides()`, which\n * still does all the real work (active-item preservation, live re-render\n * if open, `itemsUpdated`). `atIndex` follows `Array.prototype.splice`'s\n * own semantics (negative counts from the end, out-of-range clamps) —\n * no extra validation on top of that.\n */\n addSlides(items: GalleryItemInput[], atIndex?: number): void {\n if (this.destroyed) return;\n const next: GalleryItemInput[] = [...this.itemList];\n next.splice(atIndex ?? next.length, 0, ...items);\n this.updateSlides(next);\n }\n\n /**\n * Sugar over `updateSlides()` for the common \"remove some items\" case.\n * Accepts a single id/index or an array mixing both: a `string` matches\n * the same `id ?? src` key `updateSlides()` already uses for active-item\n * preservation, a `number` matches that position in the *current*\n * `items` list (resolved against the list as it is now, before any\n * removal — passing `[0, 1]` removes the first two items, not \"index 0,\n * then whatever became index 1 after that\").\n */\n removeSlides(match: string | number | Array<string | number>): void {\n if (this.destroyed) return;\n const matches = Array.isArray(match) ? match : [match];\n const indices = new Set(matches.filter((m): m is number => typeof m === 'number'));\n const keys = new Set(matches.filter((m): m is string => typeof m === 'string'));\n const next = this.itemList.filter((item, i) => !indices.has(i) && !keys.has(itemKey(item)));\n this.updateSlides(next);\n }\n\n /** DESIGN.md §2.7 — selector-mode only; sugar over updateSlides() sourced from a DOM rescan. */\n refresh(): void {\n if (this.destroyed) return;\n if (this.isDynamicMode) {\n console.warn('Shoji: refresh() is a no-op in dynamic mode — call updateSlides() instead.');\n return;\n }\n const scanned = scanContainer(this.element, this.selector);\n this.scannedElements = scanned.map((s) => s.element);\n this.updateSlides(scanned.map((s) => s.item));\n }\n\n /** Shared by `destroy()`/`reinit()` (§2.7): force-close, run plugin cleanups, drop gesture/transition/slide/dialog. */\n private teardown(): void {\n if (this.opened) {\n // A real bug, caught by cross-test interference in the unit suite —\n // never reproduced as a simple direct assertion because the existing\n // \"destroy() while the modal is open\" test's `removeEventListener`\n // spy assertion was too loose to actually catch it (FocusTrap's own\n // unrelated document keydown listener removal satisfied the same\n // `expect.any(Function)` matcher). `beginClose()` closes the caption\n // modal first (DESIGN.md §2.3a) — `teardown()` calls `finishClose()`\n // directly instead, skipping `beginClose()` and its own close-path\n // entirely, so destroying a gallery while its caption modal was open\n // never removed that modal's document-level keydown listener. Left\n // unfixed, it leaks forever: a capture-phase listener bound to the\n // destroyed instance, `stopPropagation()`-ing every future keydown on\n // the page regardless of what it's for.\n if (this.captionModalOpen) this.closeCaptionModal();\n // Same gap, same fix, for the toolbar overflow popover (§3.1a) —\n // teardown() skips beginClose() entirely, so a destroy()/reinit()\n // while it happened to be open would leak its own document-level\n // keydown listener the same way.\n if (this.toolbarOverflowOpen) this.closeToolbarOverflow();\n if (!this.isClosing) this.bus.emit('beforeClose', {});\n this.finishClose();\n }\n for (const cleanup of this.pluginCleanups) cleanup();\n this.pluginCleanups = [];\n this.shortcuts.clear();\n this.pluginStorage.clear();\n this.activePluginNames.clear();\n this.videoProviders.clear();\n if (!this.isDynamicMode) {\n this.element.removeEventListener('click', this.onContainerClick);\n }\n this.gesture?.destroy();\n this.gesture = null;\n this.transition = null;\n this.toolbarHeightObserver?.disconnect();\n this.toolbarHeightObserver = null;\n if (this.toolbarHeightFrame !== null) cancelAnimationFrame(this.toolbarHeightFrame);\n this.toolbarHeightFrame = null;\n if (this.captionTruncationFrame !== null) cancelAnimationFrame(this.captionTruncationFrame);\n this.captionTruncationFrame = null;\n this.captionFadePending = false;\n this.slides?.destroy();\n this.dom?.outer.remove();\n this.slides = null;\n this.dom = null;\n // A real leak, caught by inspection: core's own `wireControlHover()`\n // calls in `ensureLightbox()` (closeButton, toolbar slots, caption,\n // counter, ...) never store/call the unsubscribe they return, since\n // there was never anywhere else that needed it before — those elements\n // were expected to just live and die with the rest of `dom` above.\n // `reinit()` routes through here too, though, and rebuilds a whole new\n // `dom` afterward (`ensureLightbox()` runs again) — without this,\n // `hoverableElements` would keep every previous reinit cycle's now-\n // detached elements forever, growing unbounded across repeated calls.\n this.hoverableElements.clear();\n this.hoveringElements.clear();\n // Same reasoning: a plugin's own `ctx.ui.toolbar()` unsubscribe not\n // being called for some reason must not leave stale, now-detached\n // elements in here across a `reinit()` cycle either.\n this.pluginToolbarButtons.length = 0;\n }\n\n destroy(): void {\n if (this.destroyed) return;\n // Guarded, not a plain delete(): if a second Gallery was constructed on\n // this same element without the first ever being destroyed, the\n // registry entry already points at that newer instance — an old,\n // stale `this` being destroyed later must not evict it.\n if (instanceRegistry.get(this.element) === this) instanceRegistry.delete(this.element);\n allInstances.delete(this);\n this.teardown();\n this.bus.emit('destroy', {});\n this.bus.clear();\n this.destroyed = true;\n }\n\n /**\n * DESIGN.md §2.7 — full teardown + reconstruction on `this`, not a new\n * object: `Shoji.getInstance(el)`/any held reference keeps working across\n * the call, unlike `destroy()` + `new Shoji(...)`. Omit `options` to\n * rebuild unchanged (a hard reset); pass options to reconfigure\n * structurally. `gallery.on(...)` listeners do NOT survive — the event\n * bus is fully cleared, same as `destroy()` — re-`on()` anything needed.\n */\n reinit(options: GalleryOptions = this.options): void {\n if (this.destroyed) return;\n this.teardown();\n this.bus.clear();\n this.applyOptions(options);\n }\n}\n"],"names":["prefersReducedMotion","parseCssTime","clearInlineTransform","preset","markIntentionalScrollInternal"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"shoji-core.js","sources":["../../src/core/bodyScrollLock.ts","../../src/core/EventBus.ts","../../src/core/icons.ts","../../src/core/dom.ts","../../src/core/FocusTrap.ts","../../src/gestures/GestureEngine.ts","../../src/core/zoomTransition.ts","../../src/core/GestureController.ts","../../src/core/LiveRegion.ts","../../src/core/scan.ts","../../src/core/SlideManager.ts","../../src/transitions/presets.ts","../../src/transitions/SlideTransition.ts","../../src/core/Gallery.ts"],"sourcesContent":["/** DESIGN.md §2.6a — reference-counted page scroll lock. */\nlet lockCount = 0;\nlet savedHtmlOverflow = '';\nlet savedHtmlPaddingRight = '';\nlet savedHtmlPaddingLeft = '';\nlet savedScrollX = 0;\nlet savedScrollY = 0;\nlet styleObserver: MutationObserver | null = null;\n// Set by markIntentionalScroll() (ActiveThumbnail's own scrollIntoView while\n// locked, DESIGN.md §2.6a/§4.2) — a real bug/regression: unlockBodyScroll()\n// unconditionally restoring savedScrollX/Y (below) was meant to undo\n// *unrelated* code hijacking scroll during the lock (a router, a stray\n// \"scroll to top\" button), but it just as unconditionally undid Shoji's own\n// legitimate background-page scrolling too, making ActiveThumbnail's\n// scrollIntoView a complete no-op the instant the lightbox that triggers it\n// is also what's holding the lock — every navigation while open, always.\n// Once anything marks an intentional scroll during this lock session, skip\n// the restore entirely rather than try to track a smooth-scroll animation's\n// eventual resting position (no reliable completion signal across browsers)\n// — same \"leave it where legitimate code put it\" reasoning, just without\n// pinpointing the exact pixel.\nlet intentionalScrollOccurred = false;\n\nconst LIGHTBOX_SELECTOR = '.shoji-outer';\n\n/** Call after intentionally scrolling something on the host page while the lock is active (e.g. ActiveThumbnail's own `scrollIntoView`) — tells `unlockBodyScroll()` not to undo it. */\nexport function markIntentionalScroll(): void {\n intentionalScrollOccurred = true;\n}\n\nfunction isRtl(): boolean {\n return getComputedStyle(document.documentElement).direction === 'rtl';\n}\n\n/**\n * Walks up from `node` looking for a real scrollable container — lets\n * `onTouchMove` below allow touch-scrolling through to something like a\n * host-app modal/sidebar opened on top of the lightbox, instead of\n * blocking it just because it's outside `.shoji-outer`. Stops at\n * `document.documentElement` deliberately — that's the exact background\n * scroll this lock exists to block, not a container to exempt from it.\n */\nfunction hasScrollableAncestor(node: Node | null): boolean {\n let el = node instanceof Element ? node : (node?.parentElement ?? null);\n while (el && el !== document.documentElement) {\n const style = getComputedStyle(el);\n if (\n (style.overflowY === 'auto' || style.overflowY === 'scroll') &&\n el.scrollHeight > el.clientHeight\n ) {\n return true;\n }\n el = el.parentElement;\n }\n return false;\n}\n\n// Computed once per gesture, in onTouchStart below, not recomputed on every\n// onTouchMove — a touch's `event.target` stays fixed to the original\n// touchstart target for the whole gesture regardless of where the finger\n// moves (Touch Events spec), so re-walking the ancestor chain and calling\n// getComputedStyle again on every move would just repeat the same, not-free\n// answer for no benefit (CLAUDE.md: no forced synchronous layout in hot\n// paths like gestures/scroll).\nlet touchAllowsScrollThrough = false;\n\nfunction onTouchStart(event: TouchEvent): void {\n const target = event.target;\n const insideLightbox = target instanceof Element && target.closest(LIGHTBOX_SELECTOR) !== null;\n touchAllowsScrollThrough = !insideLightbox && hasScrollableAncestor(target as Node | null);\n}\n\n/**\n * A real gap: `overflow: hidden` on `<html>` doesn't reliably block iOS\n * Safari's own touch-driven rubber-band/bounce scroll in every case — a\n * well-known limitation of this technique across the ecosystem. The common\n * workaround (flipping `body` to `position: fixed` while locked) has its\n * own real tradeoffs: a full reflow, plus manual scroll-position capture/\n * restore that's a frequent source of bugs in other libraries (the exact\n * class of bug this whole lock has already had twice — see the other real\n * bugs in this file's history). Lighter alternative: block the browser's\n * default only for a touchmove that didn't start inside a lightbox,\n * leaving Shoji's own in-dialog gesture handling (which already manages\n * its own `preventDefault`, see `GestureEngine.ts`) completely untouched.\n * Not a passive listener — `preventDefault` is genuinely required here to\n * suppress the browser's native scroll, not just observe it.\n *\n * Also untouched: a touch that started on a genuinely scrollable ancestor\n * outside the lightbox (`touchAllowsScrollThrough`, set in `onTouchStart`\n * above) — a real bug, reported from real usage: this used to block\n * touch-scrolling in *any* host-app UI outside `.shoji-outer`, including\n * something like a Bootstrap modal or sidebar opened on top of the\n * lightbox, not just the page body behind it.\n */\nfunction onTouchMove(event: TouchEvent): void {\n const target = event.target;\n const insideLightbox = target instanceof Element && target.closest(LIGHTBOX_SELECTOR) !== null;\n if (!insideLightbox && !touchAllowsScrollThrough) event.preventDefault();\n}\n\n/**\n * A real gap: this lock is reference-counted against other `Gallery`\n * instances, but has no way to know about unrelated code on the host page\n * — another modal/dropdown library that also sets\n * `document.documentElement.style.overflow`, then clears it back to `''`\n * on its own close, would silently undo this lock mid-lightbox with\n * neither library aware of the other. Watching the one attribute this\n * lock itself owns is the only way to detect and correct that reactively.\n * `takeRecords()` discards the mutation record our own corrective write\n * below just queued — otherwise the observer would fire again for a\n * change it caused itself, forever.\n */\nfunction onStyleMutation(): void {\n if (lockCount === 0) return;\n const html = document.documentElement;\n if (getComputedStyle(html).overflow !== 'hidden') {\n html.style.overflow = 'hidden';\n styleObserver?.takeRecords();\n }\n}\n\nexport function lockBodyScroll(): void {\n if (lockCount === 0) {\n // A real gap: `overflow: hidden` blocks wheel/touch/keyboard-driven\n // scrolling, but not a programmatic `window.scrollTo()`/`scrollTop`\n // write — confirmed directly. If the host's own code scrolls the\n // window while the lightbox is open (a router restoring scroll\n // position, an unrelated \"scroll to top\" button), the page ends up\n // somewhere different than where the viewer left it once the lock\n // releases. Restored unconditionally on unlock below — a scroll\n // *lock* implies the background stays frozen in place for the whole\n // time the lightbox is open, not just protected from direct input.\n savedScrollX = window.scrollX;\n savedScrollY = window.scrollY;\n intentionalScrollOccurred = false;\n\n // A real bug: hiding overflow below reclaims the scrollbar's own\n // gutter, widening <html>'s content box by however many px the\n // scrollbar was — on a host page whose content spans the full viewport\n // width, that reflow is visible as a shift right when the lightbox\n // opens/closes (and reverses on close). Measured *before* overflow is\n // hidden (clientWidth only grows once the gutter is actually reclaimed,\n // so measuring after would always read 0) and only compensated for when\n // a real scrollbar was actually there — a page that never had one gets\n // no padding at all, nothing to compensate for.\n //\n // Two rejected approaches, both real bugs of their own: padding-right\n // on `document.body` compensates the wrong element — a page whose body\n // is narrower than the viewport (a centered, `max-width`-capped layout,\n // this docs site included) never touches the scrollbar's gutter in the\n // first place, so padding body just shrinks its own content box by an\n // *extra* scrollbar-width's worth, a new, self-inflicted reflow.\n // `scrollbar-gutter: stable` compensates the right element (`<html>`,\n // the same one the width is measured on) but paints its own visible,\n // if non-interactive, scrollbar-track styling for as long as the lock\n // is active — trading a brief shift for a permanent scrollbar-shaped\n // strip sitting over the page the whole time the lightbox is open,\n // which reads as more wrong, not less. Padding is genuinely invisible\n // — the same blank space a wider margin would be — and, applied to\n // `<html>` (not `body`), compensates the actual element the scrollbar's\n // gutter belongs to either way.\n const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;\n const rtl = isRtl();\n savedHtmlPaddingRight = document.documentElement.style.paddingRight;\n savedHtmlPaddingLeft = document.documentElement.style.paddingLeft;\n if (scrollbarWidth > 0) {\n // A real gap: a classic scrollbar renders on the *left* on a\n // `direction: rtl` page, not the right — compensating the wrong\n // side would reintroduce the exact reflow this exists to prevent,\n // just mirrored. Checked at lock time, not baked in as a fixed\n // assumption.\n if (rtl) {\n const currentPaddingLeft =\n parseFloat(getComputedStyle(document.documentElement).paddingLeft) || 0;\n document.documentElement.style.paddingLeft = `${currentPaddingLeft + scrollbarWidth}px`;\n } else {\n const currentPaddingRight =\n parseFloat(getComputedStyle(document.documentElement).paddingRight) || 0;\n document.documentElement.style.paddingRight = `${currentPaddingRight + scrollbarWidth}px`;\n }\n }\n\n // A real bug: `document.body.style.overflow = 'hidden'` (this function's\n // original, and until now only, scroll-blocking mechanism) was still\n // set here alongside <html>'s own below — redundant, and not harmless:\n // any value of `overflow` other than `visible` makes an element\n // establish a new block-formatting context, which blocks top-margin\n // collapsing between it and its first child. Confirmed directly via\n // real-browser instrumentation: with body's overflow locked, an h1\n // immediately inside it rendered `bodyMarginTop + h1's own default\n // margin-top` below the viewport top (its margin no longer collapsing\n // into body's own); the instant `unlockBodyScroll()` restored body's\n // overflow, collapsing resumed and the h1 snapped back up by its own\n // margin's worth — reading as the page shifting upward right as the\n // lightbox closes, on *any* page where the first child's margin would\n // otherwise collapse with body's (essentially any page without padding/\n // border on body itself — not a corner case). <html>'s own overflow:\n // hidden below is sufficient on its own to block user-driven scrolling\n // (wheel/touch/keyboard) — confirmed directly — since it's the real\n // scrolling element in standards mode; body's lock was never adding\n // independent protection, only this side effect.\n //\n // <html>'s own overflow, not just body's — mobile viewport-widening\n // bug, see DESIGN.md §2.6a. Both axes, not just overflow-x: setting\n // only one non-'visible' axis forces the browser to silently promote\n // the other from 'visible' to 'auto' (mixing hidden+visible isn't\n // allowed per spec), which revealed a real scrollbar that wasn't\n // there before.\n savedHtmlOverflow = document.documentElement.style.overflow;\n document.documentElement.style.overflow = 'hidden';\n\n // passive: true — onTouchStart only ever reads/caches, never calls\n // preventDefault(), unlike onTouchMove below.\n document.addEventListener('touchstart', onTouchStart, { passive: true });\n document.addEventListener('touchmove', onTouchMove, { passive: false });\n\n // Starts observing only after the writes above have already landed, so\n // it never reacts to its own initial setup — only to a later, external\n // change.\n styleObserver = new MutationObserver(onStyleMutation);\n styleObserver.observe(document.documentElement, {\n attributes: true,\n attributeFilter: ['style'],\n });\n }\n lockCount++;\n}\n\nexport function unlockBodyScroll(): void {\n lockCount = Math.max(0, lockCount - 1);\n if (lockCount === 0) {\n styleObserver?.disconnect();\n styleObserver = null;\n document.removeEventListener('touchstart', onTouchStart);\n document.removeEventListener('touchmove', onTouchMove);\n\n document.documentElement.style.overflow = savedHtmlOverflow;\n document.documentElement.style.paddingRight = savedHtmlPaddingRight;\n document.documentElement.style.paddingLeft = savedHtmlPaddingLeft;\n\n if (!intentionalScrollOccurred) {\n window.scrollTo({ left: savedScrollX, top: savedScrollY, behavior: 'instant' });\n }\n }\n}\n","export type EventMap = Record<string, unknown>;\n\nexport type Unsubscribe = () => void;\n\ntype Listener<T> = (detail: T) => void;\n\n/**\n * Typed pub/sub used by Gallery and every plugin (`ctx.on`). Plugin\n * subscriptions are unsubscribed automatically on destroy by the caller\n * that owns the PluginContext, not by this class.\n */\nexport class EventBus<Events extends EventMap> {\n private listeners = new Map<keyof Events, Set<Listener<unknown>>>();\n\n on<K extends keyof Events>(event: K, fn: Listener<Events[K]>): Unsubscribe {\n let set = this.listeners.get(event);\n if (!set) {\n set = new Set();\n this.listeners.set(event, set);\n }\n set.add(fn as Listener<unknown>);\n return () => this.off(event, fn);\n }\n\n off<K extends keyof Events>(event: K, fn: Listener<Events[K]>): void {\n this.listeners.get(event)?.delete(fn as Listener<unknown>);\n }\n\n emit<K extends keyof Events>(event: K, detail: Events[K]): void {\n const set = this.listeners.get(event);\n if (!set) return;\n for (const fn of set) (fn as Listener<Events[K]>)(detail);\n }\n\n clear(): void {\n this.listeners.clear();\n }\n}\n","/** DESIGN.md §9 — inline SVG, stroke = currentColor, overridable per-button. */\n\nexport const CLOSE_ICON =\n '<svg viewBox=\"0 0 24 24\" width=\"24\" height=\"24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><path d=\"M6 6l12 12M18 6L6 18\"/></svg>';\n\nexport const PREV_ICON =\n '<svg viewBox=\"0 0 24 24\" width=\"24\" height=\"24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M15 6l-6 6 6 6\"/></svg>';\n\nexport const NEXT_ICON =\n '<svg viewBox=\"0 0 24 24\" width=\"24\" height=\"24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M9 6l6 6-6 6\"/></svg>';\n\nexport const CAPTION_ICON =\n '<svg viewBox=\"0 0 24 24\" width=\"24\" height=\"24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"5\" width=\"18\" height=\"14\" rx=\"2\"/><path d=\"M7 10h10M7 14h6\"/></svg>';\n\nexport const CARET_ICON =\n '<svg viewBox=\"0 0 24 24\" width=\"20\" height=\"20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M6 9l6 6 6-6\"/></svg>';\n","import { CAPTION_ICON, CARET_ICON, CLOSE_ICON, NEXT_ICON, PREV_ICON } from './icons';\n\nlet idCounter = 0;\n\nexport interface LightboxLabels {\n close: string;\n previous: string;\n next: string;\n showCaption: string;\n hideCaption: string;\n fullCaption: string; // DESIGN.md §2.3a — aria-label for the caption modal's own dialog role\n moreControls: string; // DESIGN.md §3.1a — the toolbar overflow popover's own toggle/dialog label\n}\n\nexport interface LightboxDom {\n outer: HTMLElement;\n backdrop: HTMLElement;\n dialog: HTMLElement;\n counter: HTMLElement;\n caption: HTMLElement;\n closeButton: HTMLButtonElement;\n prevButton: HTMLButtonElement;\n nextButton: HTMLButtonElement;\n captionToggleButton: HTMLButtonElement; // DESIGN.md §2.3a\n /** DESIGN.md §2.3a — a truncated caption's click/Enter/Space target opens this, showing the full text scrollable, so a long caption never has to cover the nav arrows to be readable. Hidden (`[hidden]`) whenever not open. */\n captionModal: HTMLElement;\n captionModalPanel: HTMLElement;\n captionModalContent: HTMLElement;\n captionModalCloseButton: HTMLButtonElement;\n /** The whole toolbar bar — DESIGN.md §2.3a's toolbar-height measurement observes this, not any one slot, since it's what actually wraps to multiple rows. */\n toolbar: HTMLElement;\n /** DESIGN.md §3 — `ctx.ui.toolbar(slot, ...)` inserts into these; close lives in `right`. */\n toolbarLeft: HTMLElement;\n toolbarCenter: HTMLElement;\n toolbarRight: HTMLElement;\n /** DESIGN.md §3.1a — hidden unless the toolbar actually overflows; toggles `toolbarOverflowPanel`. Lives in `toolbarRight`, just before `closeButton`. */\n toolbarOverflowButton: HTMLButtonElement;\n /** DESIGN.md §3.1a — the floating popover collapsed plugin buttons relocate into once they don't fit. */\n toolbarOverflowPanel: HTMLElement;\n}\n\nfunction iconButton(className: string, icon: string, label: string): HTMLButtonElement {\n const button = document.createElement('button');\n button.type = 'button';\n button.className = className;\n button.ariaLabel = button.title = label;\n button.innerHTML = icon;\n return button;\n}\n\n/**\n * DESIGN.md §2.6 — `.shoji-outer` is the exact element `ctx.ui.outer()` (§3)\n * will return to plugins once the real plugin loader exists. Close/prev/next\n * are core-owned baseline controls, not routed through any plugin API.\n */\nexport function buildLightboxDom(slides: HTMLElement, labels: LightboxLabels): LightboxDom {\n const labelId = `shoji-counter-${++idCounter}`;\n\n const outer = document.createElement('div');\n outer.className = 'shoji-outer';\n\n const backdrop = document.createElement('div');\n backdrop.className = 'shoji-backdrop';\n outer.appendChild(backdrop);\n\n const dialog = document.createElement('div');\n dialog.className = 'shoji-dialog';\n dialog.setAttribute('role', 'dialog');\n dialog.setAttribute('aria-modal', 'true');\n dialog.setAttribute('aria-labelledby', labelId);\n dialog.tabIndex = -1;\n\n const toolbar = document.createElement('div');\n toolbar.className = 'shoji-toolbar';\n const toolbarLeft = document.createElement('div');\n toolbarLeft.className = 'shoji-toolbar-slot shoji-toolbar-left';\n const toolbarCenter = document.createElement('div');\n toolbarCenter.className = 'shoji-toolbar-slot shoji-toolbar-center';\n const toolbarRight = document.createElement('div');\n toolbarRight.className = 'shoji-toolbar-slot shoji-toolbar-right';\n const closeButton = iconButton('shoji-close', CLOSE_ICON, labels.close);\n // Ahead of close so plugin buttons (always inserted right before close) land between the two.\n const captionToggleButton = iconButton(\n 'shoji-toolbar-button shoji-caption-toggle',\n CAPTION_ICON,\n labels.showCaption,\n );\n captionToggleButton.setAttribute('aria-pressed', 'false');\n captionToggleButton.hidden = true;\n // DESIGN.md §3.1a — requested directly: close stays the absolute\n // rightmost control, with the (usually hidden) overflow caret just to its\n // left — `pluginToolbar()` inserts new buttons immediately before the\n // caret now, not before close, so they still land to the left of both\n // fixed controls.\n const toolbarOverflowButton = iconButton(\n 'shoji-toolbar-button shoji-toolbar-overflow',\n CARET_ICON,\n labels.moreControls,\n );\n toolbarOverflowButton.setAttribute('aria-haspopup', 'true');\n toolbarOverflowButton.setAttribute('aria-expanded', 'false');\n toolbarOverflowButton.hidden = true;\n toolbarRight.append(captionToggleButton, toolbarOverflowButton, closeButton);\n\n const toolbarOverflowPanel = document.createElement('div');\n toolbarOverflowPanel.className = 'shoji-toolbar-overflow-panel';\n toolbarOverflowPanel.setAttribute('role', 'dialog');\n toolbarOverflowPanel.setAttribute('aria-label', labels.moreControls);\n toolbarOverflowPanel.tabIndex = -1;\n toolbarOverflowPanel.hidden = true;\n\n // The counter lives in the toolbar's own left slot (flex flow, not\n // independent absolute positioning) specifically so a plugin button\n // sharing that slot (e.g. autoplay's play/pause, ctx.ui.toolbar('left', ...))\n // sits next to it instead of the two overlapping and fighting over clicks.\n const counter = document.createElement('div');\n counter.className = 'shoji-counter';\n counter.id = labelId;\n toolbarLeft.appendChild(counter);\n\n toolbar.append(toolbarLeft, toolbarCenter, toolbarRight);\n dialog.appendChild(toolbar);\n // Appended to `dialog` directly, not nested in `.shoji-toolbar` — the\n // toolbar's own `pointer-events: none` base rule (re-enabled per-slot,\n // not per-descendant) would otherwise need remembering to re-enable here\n // too; sidestepped entirely by living outside it, same placement pattern\n // `.shoji-caption-modal` already uses below.\n dialog.appendChild(toolbarOverflowPanel);\n\n const prevButton = iconButton('shoji-nav shoji-nav-prev', PREV_ICON, labels.previous);\n const nextButton = iconButton('shoji-nav shoji-nav-next', NEXT_ICON, labels.next);\n dialog.appendChild(prevButton);\n dialog.appendChild(nextButton);\n\n dialog.appendChild(slides);\n\n const caption = document.createElement('div');\n caption.className = 'shoji-caption';\n dialog.appendChild(caption);\n\n const captionModal = document.createElement('div');\n captionModal.className = 'shoji-caption-modal';\n captionModal.hidden = true;\n const captionModalPanel = document.createElement('div');\n captionModalPanel.className = 'shoji-caption-modal-panel';\n captionModalPanel.setAttribute('role', 'dialog');\n captionModalPanel.setAttribute('aria-modal', 'true');\n captionModalPanel.setAttribute('aria-label', labels.fullCaption);\n captionModalPanel.tabIndex = -1;\n const captionModalCloseButton = iconButton('shoji-caption-modal-close', CLOSE_ICON, labels.close);\n const captionModalContent = document.createElement('div');\n captionModalContent.className = 'shoji-caption-modal-content';\n captionModalPanel.append(captionModalCloseButton, captionModalContent);\n captionModal.appendChild(captionModalPanel);\n dialog.appendChild(captionModal);\n\n outer.appendChild(dialog);\n\n return {\n outer,\n backdrop,\n dialog,\n counter,\n caption,\n captionModal,\n captionModalPanel,\n captionModalContent,\n captionModalCloseButton,\n closeButton,\n prevButton,\n nextButton,\n captionToggleButton,\n toolbar,\n toolbarLeft,\n toolbarCenter,\n toolbarRight,\n toolbarOverflowButton,\n toolbarOverflowPanel,\n };\n}\n","const FOCUSABLE_SELECTOR =\n 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), video[controls], [tabindex]:not([tabindex=\"-1\"])';\n\n/** DESIGN.md §2.6 — focus trap on open, focus restore on close, Tab cycles within the dialog. */\nexport class FocusTrap {\n private container: HTMLElement | null = null;\n private previouslyFocused: HTMLElement | null = null;\n\n private readonly onKeydown = (event: KeyboardEvent): void => {\n if (event.key !== 'Tab' || !this.container) return;\n\n const focusable = this.getFocusable();\n if (focusable.length === 0) {\n event.preventDefault();\n this.container.focus();\n return;\n }\n\n const first = focusable[0]!;\n const last = focusable[focusable.length - 1]!;\n const active = document.activeElement;\n const activeIsInside = active instanceof HTMLElement && focusable.includes(active);\n\n if (event.shiftKey) {\n if (!activeIsInside || active === first) {\n event.preventDefault();\n last.focus();\n }\n } else if (!activeIsInside || active === last) {\n event.preventDefault();\n first.focus();\n }\n };\n\n private getFocusable(): HTMLElement[] {\n if (!this.container) return [];\n return Array.from(this.container.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR));\n }\n\n activate(container: HTMLElement): void {\n this.previouslyFocused = document.activeElement as HTMLElement | null;\n this.container = container;\n document.addEventListener('keydown', this.onKeydown, true);\n container.focus();\n }\n\n /**\n * DESIGN.md §2.3a — narrows (or widens back) which subtree Tab cycles\n * within, without touching `previouslyFocused`/the listener registration\n * — a caption modal nested inside the already-trapped dialog needs Tab\n * confined to just itself while open, then back to the whole dialog on\n * close, but must NOT trigger `activate()`'s own focus-capture (that's\n * reserved for the real open/close boundary; re-running it mid-session\n * would capture the wrong \"previously focused\" element and restore focus\n * to it too early). `getFocusable()` reads `this.container` fresh on\n * every keydown, so simply reassigning it is enough — no need for a\n * second, independent `FocusTrap` instance, which would double-handle\n * every Tab keypress (two capture-phase `document` listeners, each\n * computing its own overlapping focusable list).\n */\n retarget(container: HTMLElement): void {\n this.container = container;\n container.focus();\n }\n\n deactivate(): void {\n document.removeEventListener('keydown', this.onKeydown, true);\n this.container = null;\n // preventScroll: true — a bare focus() call auto-scrolls the target\n // into view within any scrollable ancestor by default. A real bug:\n // previouslyFocused is captured once at activate() time and never\n // updated afterward (nothing re-focuses the current thumbnail as the\n // viewer navigates), so restoring it on close can drag a host's own\n // scrollable thumbnail strip all the way back to wherever the gallery\n // was originally opened from, discarding whatever it had scrolled to\n // since — e.g. ActiveThumbnail's auto-scroll, entirely undone by a\n // focus restoration that has nothing to do with it.\n this.previouslyFocused?.focus({ preventScroll: true });\n this.previouslyFocused = null;\n }\n}\n","/**\n * DESIGN.md §2.4 — one Pointer-Events-based engine feeding all consumers:\n * core drag-to-navigate, core vertical-swipe-to-close, and (via `onPinch`/\n * `onDoubleTap`/`onWheelZoom`) a future zoom plugin that doesn't exist yet.\n * Pointer Events (not separate mouse/touch listeners) give mouse-drag and\n * touch-swipe parity for free — one code path, one set of thresholds.\n *\n * State machine: `idle → pending → dragging(h|v) → idle`. `pending` is the\n * window before `lockThreshold` is crossed, where it's still ambiguous\n * whether this is a tap, a horizontal drag, or a vertical drag; `dragging`\n * is entered once one axis's delta exceeds the threshold first, and the\n * *other* axis is ignored for the rest of that gesture (an axis lock, not\n * a snapped direction — diagonal drags don't fight the engine). There's no\n * literal blocking `settling` state after release: `onDragEnd` reports the\n * outcome synchronously and the caller owns however long its own\n * snap-back/completion animation takes; a new `pointerdown` before that\n * finishes is allowed to interrupt it (matches real touch-carousel UX —\n * grabbing a still-settling slide feels responsive, not broken).\n *\n * Pinch tracking is a separate concern layered on top of the same pointer\n * map: a second simultaneous pointer suspends whatever single-pointer drag\n * state was in progress (real pinches start from a single touch already\n * down) and switches to distance-ratio tracking until back down to one\n * pointer or zero.\n */\n\nexport type GestureDirection = 'horizontal' | 'vertical';\n\nexport interface GestureEngineOptions {\n /** px of movement before a direction locks in. DESIGN.md §2.4 default: 10. */\n lockThreshold: number;\n /** px of movement past which a released drag counts as \"swiped\" (completes), independent of velocity. DESIGN.md §2.4 default: 50. */\n swipeThreshold: number;\n /** px/ms release velocity past which a drag completes even under swipeThreshold — a fast flick counts. DESIGN.md §2.4 default: 0.3. */\n swipeVelocity: number;\n}\n\nexport interface GestureEngineCallbacks {\n /** Direction has just locked in for this gesture — the first move past lockThreshold. */\n onDragStart?(direction: GestureDirection, event: PointerEvent): void;\n /** delta: signed px moved along the locked axis so far (this move's cumulative total, not a per-frame diff). */\n onDragMove?(direction: GestureDirection, delta: number, event: PointerEvent): void;\n /** Pointer released (or cancelled) after a drag was in progress. velocity is signed px/ms over the whole gesture; completed reflects swipeThreshold/swipeVelocity. cancelled is true for pointercancel (e.g. an OS gesture took over) — always treated as not-completed regardless of delta/velocity. */\n onDragEnd?(\n direction: GestureDirection,\n delta: number,\n velocity: number,\n completed: boolean,\n cancelled: boolean,\n ): void;\n /** A pointerdown that never crossed lockThreshold before release — not a drag. Used to detect double-tap (the engine tracks tap timing/position itself and calls onDoubleTap instead when two land close enough together) as well as being a simple tap signal on its own. */\n onTap?(x: number, y: number, event: PointerEvent): void;\n onDoubleTap?(x: number, y: number, event: PointerEvent): void;\n /** scale is relative to the pinch's own start distance (1 = unchanged), not a cumulative multiplier across multiple pinch gestures. */\n onPinchStart?(centerX: number, centerY: number): void;\n onPinchMove?(scale: number, centerX: number, centerY: number): void;\n onPinchEnd?(): void;\n /** Ctrl+wheel (trackpad pinch reports this way in every major browser) or a genuine ctrl+scroll-wheel. deltaScale is a small increment (positive = zoom in), not an absolute scale — the caller accumulates it. */\n onWheelZoom?(deltaScale: number, x: number, y: number, event: WheelEvent): void;\n /** Only consulted on pointerdown — returning true means this pointer is ignored for the rest of its lifetime (a click on a real button/control shouldn't also start a drag). */\n ignore?(event: PointerEvent): boolean;\n /** Consulted right before capturing a locked drag's pointer — `false` skips capture for this gesture entirely (not just its drag effect). Default `true` if omitted. See onPointerMove's capture branch for why this needs to be a real skip, not just a suppressed effect. */\n shouldCapture?(): boolean;\n}\n\nconst DOUBLE_TAP_MAX_DELAY_MS = 300;\nconst DOUBLE_TAP_MAX_DISTANCE = 30;\nconst TAP_MAX_DELAY_MS = 500; // a pointerdown→up this slow isn't a \"tap\" even if it never crossed lockThreshold\n\ninterface PointerState {\n startX: number;\n startY: number;\n startTime: number;\n lastX: number;\n lastY: number;\n lastTime: number;\n}\n\nexport class GestureEngine {\n private readonly target: HTMLElement;\n private readonly options: GestureEngineOptions;\n private readonly callbacks: GestureEngineCallbacks;\n\n private readonly pointers = new Map<number, PointerState>();\n private primaryPointerId: number | null = null;\n private direction: GestureDirection | null = null;\n private dragStartDistance = 0; // along the locked axis, at the moment direction locked — subtracted so onDragStart's delta is 0\n\n private pinching = false;\n private pinchStartDistance = 0;\n\n /** True once `setPointerCapture` has actually been called for the gesture currently in progress — see `suppressRetargetedClick`'s own doc comment for why this needs tracking separately from `direction`. */\n private capturedThisGesture = false;\n\n // -Infinity, not 0: `event.timeStamp` is time-since-navigation-start, so a\n // real first tap is always some large positive number and 0 would seem\n // like a safe \"no previous tap\" sentinel in practice — but not always: a\n // tap in the first 300ms of the page's life, near the viewport's top-left\n // corner, would satisfy registerTap's isDouble check against this\n // sentinel and get wrongly reported as a double-tap on nothing. -Infinity\n // can never be within DOUBLE_TAP_MAX_DELAY_MS of any real timestamp.\n private lastTapTime = -Infinity;\n private lastTapX = 0;\n private lastTapY = 0;\n\n constructor(\n target: HTMLElement,\n callbacks: GestureEngineCallbacks,\n options?: Partial<GestureEngineOptions>,\n ) {\n this.target = target;\n this.callbacks = callbacks;\n this.options = {\n lockThreshold: options?.lockThreshold ?? 10,\n swipeThreshold: options?.swipeThreshold ?? 50,\n swipeVelocity: options?.swipeVelocity ?? 0.3,\n };\n\n this.target.addEventListener('pointerdown', this.onPointerDown);\n // Not passive: once a horizontal/vertical drag direction has locked in,\n // preventDefault() on move is required so the browser's own\n // scroll/back-navigation gesture doesn't fight the drag — see\n // onPointerMove. Before that lock, moves don't preventDefault, so\n // ordinary page scroll/tap behavior is unaffected until a real drag is\n // actually recognized.\n this.target.addEventListener('pointermove', this.onPointerMove);\n this.target.addEventListener('pointerup', this.onPointerUp);\n this.target.addEventListener('pointercancel', this.onPointerCancel);\n // ctrl+wheel must preventDefault (documented, not passive) — otherwise\n // the browser zooms the whole page instead of this becoming a gesture\n // the caller can hand to a zoom plugin.\n this.target.addEventListener('wheel', this.onWheel, { passive: false });\n }\n\n destroy(): void {\n this.target.removeEventListener('pointerdown', this.onPointerDown);\n this.target.removeEventListener('pointermove', this.onPointerMove);\n this.target.removeEventListener('pointerup', this.onPointerUp);\n this.target.removeEventListener('pointercancel', this.onPointerCancel);\n this.target.removeEventListener('wheel', this.onWheel);\n this.pointers.clear();\n }\n\n private readonly onPointerDown = (event: PointerEvent): void => {\n if (event.isPrimary === false && this.pointers.size === 0) return; // stray non-primary with no primary tracked yet — ignore\n if (this.pointers.size === 0 && this.callbacks.ignore?.(event)) return;\n\n this.pointers.set(event.pointerId, {\n startX: event.clientX,\n startY: event.clientY,\n startTime: event.timeStamp,\n lastX: event.clientX,\n lastY: event.clientY,\n lastTime: event.timeStamp,\n });\n\n if (this.pointers.size === 1) {\n this.primaryPointerId = event.pointerId;\n this.direction = null;\n this.capturedThisGesture = false;\n // Deliberately NOT captured here — see onPointerMove's direction-lock\n // branch for why.\n } else if (this.pointers.size === 2) {\n // A second pointer arriving mid-drag suspends the single-pointer\n // gesture (real pinches start from an already-down single touch) and\n // starts pinch tracking instead.\n this.direction = null;\n this.pinching = true;\n const [a, b] = [...this.pointers.values()];\n this.pinchStartDistance = distanceBetween(a!, b!);\n const center = centerOf(a!, b!);\n this.callbacks.onPinchStart?.(center.x, center.y);\n }\n };\n\n private readonly onPointerMove = (event: PointerEvent): void => {\n const state = this.pointers.get(event.pointerId);\n if (!state) return;\n state.lastX = event.clientX;\n state.lastY = event.clientY;\n state.lastTime = event.timeStamp;\n\n if (this.pinching) {\n if (this.pointers.size < 2) return;\n const [a, b] = [...this.pointers.values()];\n const distance = distanceBetween(a!, b!);\n const scale = this.pinchStartDistance > 0 ? distance / this.pinchStartDistance : 1;\n const center = centerOf(a!, b!);\n this.callbacks.onPinchMove?.(scale, center.x, center.y);\n return;\n }\n\n if (event.pointerId !== this.primaryPointerId) return;\n\n const dx = event.clientX - state.startX;\n const dy = event.clientY - state.startY;\n\n if (this.direction === null) {\n const absX = Math.abs(dx);\n const absY = Math.abs(dy);\n if (Math.max(absX, absY) < this.options.lockThreshold) return; // still pending — too small to call yet\n this.direction = absX > absY ? 'horizontal' : 'vertical';\n this.dragStartDistance = this.direction === 'horizontal' ? dx : dy;\n // Captured only once a real drag is confirmed (direction locked), not\n // on every pointerdown — capturing eagerly retargets the browser's\n // synthesized mousedown/click to the capturing element, breaking any\n // click-target-based logic downstream on an ordinary tap. Deferring\n // capture to here fixes that — but a *confirmed* drag still captures,\n // and a captured pointer's release still fires a real click\n // afterward, still retargeted to `target`. See\n // `suppressRetargetedClick` below for that other half of the fix.\n if (this.callbacks.shouldCapture?.() ?? true) {\n this.target.setPointerCapture(event.pointerId);\n this.capturedThisGesture = true;\n }\n this.callbacks.onDragStart?.(this.direction, event);\n }\n\n if (this.direction === 'horizontal') event.preventDefault(); // stop page/back-gesture scroll from fighting the drag\n\n const delta = (this.direction === 'horizontal' ? dx : dy) - this.dragStartDistance;\n this.callbacks.onDragMove?.(this.direction, delta, event);\n };\n\n private readonly onPointerUp = (event: PointerEvent): void => {\n this.finishPointer(event, false);\n };\n\n private readonly onPointerCancel = (event: PointerEvent): void => {\n this.finishPointer(event, true);\n };\n\n private finishPointer(event: PointerEvent, cancelled: boolean): void {\n const state = this.pointers.get(event.pointerId);\n if (!state) return;\n // pointerup/pointercancel carry their own release coordinates — these\n // don't necessarily match the last pointermove (coalesced/throttled\n // move events, or a release with no intervening move at all), so total\n // delta/velocity/tap-position must be computed from *this* event's\n // position, not whatever onPointerMove last recorded.\n state.lastX = event.clientX;\n state.lastY = event.clientY;\n state.lastTime = event.timeStamp;\n this.pointers.delete(event.pointerId);\n\n if (this.pinching) {\n if (this.pointers.size < 2) {\n this.pinching = false;\n this.callbacks.onPinchEnd?.();\n }\n // Still >= 2 pointers (a third was down) — stay in pinch mode; not\n // handled further here, an unusual case not worth extra state for.\n return;\n }\n\n if (event.pointerId !== this.primaryPointerId) return;\n\n if (this.direction === null) {\n // Never crossed lockThreshold — a tap, not a drag (unless it was\n // cancelled, or took too long to plausibly be an intentional tap).\n if (!cancelled && event.timeStamp - state.startTime <= TAP_MAX_DELAY_MS) {\n this.registerTap(state.lastX, state.lastY, event);\n }\n } else {\n const elapsed = Math.max(1, state.lastTime - state.startTime); // avoid divide-by-zero on a same-tick move\n const totalDelta =\n (this.direction === 'horizontal'\n ? state.lastX - state.startX\n : state.lastY - state.startY) - this.dragStartDistance;\n const velocity = totalDelta / elapsed;\n const completed =\n !cancelled &&\n (Math.abs(totalDelta) >= this.options.swipeThreshold ||\n Math.abs(velocity) >= this.options.swipeVelocity);\n this.callbacks.onDragEnd?.(this.direction, totalDelta, velocity, completed, cancelled);\n if (this.capturedThisGesture) this.suppressRetargetedClick();\n }\n\n this.primaryPointerId = null;\n this.direction = null;\n this.capturedThisGesture = false;\n }\n\n /**\n * A captured pointer's release still fires a real `click`, retargeted to\n * `target` regardless of where the pointer visually ends up — misread by\n * Gallery's click-outside-to-close as landing nowhere recognizable.\n * Consumes exactly one `click` on `target` in the capture phase, then\n * removes itself; a fallback timeout also removes it in case no `click`\n * ever comes, so it can't swallow a later, unrelated one.\n */\n private suppressRetargetedClick(): void {\n let done = false;\n const cleanup = (event?: MouseEvent): void => {\n if (done) return;\n done = true;\n event?.stopPropagation();\n this.target.removeEventListener('click', cleanup, { capture: true });\n };\n this.target.addEventListener('click', cleanup, { capture: true });\n setTimeout(cleanup, 500);\n }\n\n private registerTap(x: number, y: number, event: PointerEvent): void {\n const now = event.timeStamp;\n const isDouble =\n now - this.lastTapTime <= DOUBLE_TAP_MAX_DELAY_MS &&\n Math.hypot(x - this.lastTapX, y - this.lastTapY) <= DOUBLE_TAP_MAX_DISTANCE;\n if (isDouble) {\n this.callbacks.onDoubleTap?.(x, y, event);\n this.lastTapTime = 0; // consumed — a third tap starts fresh, isn't a triple-double\n } else {\n this.callbacks.onTap?.(x, y, event);\n this.lastTapTime = now;\n this.lastTapX = x;\n this.lastTapY = y;\n }\n }\n\n private readonly onWheel = (event: WheelEvent): void => {\n if (!event.ctrlKey) return;\n event.preventDefault();\n // Browsers report trackpad pinch as ctrl+wheel with deltaY roughly\n // proportional to pinch speed; negate so pinching out (deltaY < 0,\n // conventionally) reads as zooming in (positive deltaScale).\n const deltaScale = -event.deltaY * 0.01;\n this.callbacks.onWheelZoom?.(deltaScale, event.clientX, event.clientY, event);\n };\n}\n\nfunction distanceBetween(\n a: { lastX: number; lastY: number },\n b: { lastX: number; lastY: number },\n): number {\n return Math.hypot(a.lastX - b.lastX, a.lastY - b.lastY);\n}\n\nfunction centerOf(\n a: { lastX: number; lastY: number },\n b: { lastX: number; lastY: number },\n): { x: number; y: number } {\n return { x: (a.lastX + b.lastX) / 2, y: (a.lastY + b.lastY) / 2 };\n}\n","/** The drag's exact on-screen appearance at the moment it completed a close — `translateY(px)`/`scale`/`opacity`, all as last shown live (translateY clamped, see `GestureController.takeFrozenDragTransform`). `zoomOut()` jumps `target` here instantly (after measuring its natural box — see `ZoomTransitionTarget.dragStart`) before transitioning away from it, so the close continues in one motion from exactly where the drag left off. */\nexport interface FrozenDragTransform {\n translateY: number;\n scale: number;\n opacity: number;\n}\n\nexport interface ZoomTransitionTarget {\n /** The thumbnail element to animate to/from. */\n origin: HTMLElement;\n /** The `.shoji-slide-media` element being animated — not `.shoji-slide` itself, whose transform is already owned by pool-offset positioning. */\n target: HTMLElement;\n /** `item.width / item.height`, when known, else the origin thumbnail's own `naturalWidth`/`naturalHeight` ratio — just a *shape* guess; see `effectiveTargetBox`. */\n aspectRatio?: number;\n /** The real photo's true pixel size — `item.width`/`item.height` only, never the thumbnail's own natural size (would under-cap a large photo). Caps `containedBox` below at native resolution, mirroring `.shoji-slide-img`'s own `max-width/max-height: 100%`, instead of always growing toward filling the dialog. */\n naturalSize?: { width: number; height: number };\n /**\n * A completed drag-close's own last appearance (`GestureController.\n * takeFrozenDragTransform`) — `zoomOut()` (the only consumer) jumps\n * `target` here instantly, *after* measuring its natural box for the\n * landing math, then transitions away from it. Order matters: applying\n * this before measuring would make `effectiveTargetBox()` read the\n * child `<img>`'s rect *already* shrunk by this scale, double-counting\n * it once `zoomOut()`'s own transform (computed from that shrunk\n * measurement) then wholesale replaces — not composes with — it.\n */\n dragStart?: FrozenDragTransform;\n /**\n * The zoomed `<img>`'s own real on-screen rect (`getBoundingClientRect()`)\n * at the moment a button-close (or any non-drag close) starts, if it's\n * currently zoomed in — captured by `Gallery.beginClose()` from\n * `registerZoomStartProvider()` *before* the Zoom plugin's own\n * `beforeClose` handler resets it back to neutral (so `zoomOut()`'s own\n * measurement below stays correct — same ordering requirement `dragStart`\n * has, and the same reason).\n *\n * A rect, not the Zoom plugin's raw scale/pan numbers — an earlier version\n * passed those through directly and re-applied them to `target`\n * (`.shoji-slide-media`) the same way `dragStart` reapplies its own\n * translateY/scale. That doesn't work here: Zoom's own pan math is\n * relative to the `<img>` itself with `transform-origin: 0 0`\n * (`zoom/index.ts`), a *different* element with a *different* origin than\n * `target`'s `transform-origin: center` below — reapplying the same\n * numbers onto the wrong element/origin pair visibly shifted the image\n * toward its top-left corner the instant close started, worse the more\n * zoomed in it was. A rect sidesteps this entirely: `zoomOut()` computes\n * the jump the same center-to-center way it computes everything else\n * (`transformBetween`), from `target`'s own freshly-measured natural box\n * to this rect — origin-and-coordinate-system-agnostic, so it doesn't\n * matter what internal convention produced the rect.\n *\n * `zoomOut()` jumps `target` here instantly, then transitions away from\n * it, so closing while zoomed continues in one motion from the zoomed-in\n * view instead of snapping back to neutral first and only then shrinking\n * to the thumbnail. Mutually exclusive with `dragStart` in practice — you\n * can't complete a vertical drag-close while the zoom plugin is also\n * mid-pan, since `GestureController` suspends its own drag handling\n * entirely while zoomed (`isZoomed()`, DESIGN.md §4.6).\n */\n zoomStart?: Box;\n}\n\nfunction prefersReducedMotion(): boolean {\n return (\n typeof window.matchMedia === 'function' &&\n window.matchMedia('(prefers-reduced-motion: reduce)').matches\n );\n}\n\n/** `\"300ms\"` / `\"0.3s\"` → milliseconds. Reads the *actual* computed value, not an assumed default, so a host overriding `--shoji-duration` still gets a correct fallback timeout below. */\nfunction parseCssTime(value: string): number {\n const first = value.split(',')[0]?.trim() ?? '';\n if (first.endsWith('ms')) return parseFloat(first);\n if (first.endsWith('s')) return parseFloat(first) * 1000;\n return 0;\n}\n\nexport interface Box {\n left: number;\n top: number;\n width: number;\n height: number;\n}\n\n/** The `object-fit: contain` box for `aspectRatio` within `container`, computed analytically since the real image may not be loaded yet. `naturalSize` caps the result at the photo's own true pixel size, never stretching past native resolution. Exported — `SlideManager.ts`'s open-transition placeholder (§2.3) reuses this same math. */\nexport function containedBox(\n container: Box,\n aspectRatio: number,\n naturalSize?: { width: number; height: number },\n): Box {\n const containerRatio = container.width / container.height;\n let width = aspectRatio > containerRatio ? container.width : container.height * aspectRatio;\n let height = aspectRatio > containerRatio ? container.width / aspectRatio : container.height;\n if (naturalSize && naturalSize.width > 0 && naturalSize.height > 0) {\n const cap = Math.min(1, naturalSize.width / width, naturalSize.height / height);\n width *= cap;\n height *= cap;\n }\n return {\n left: container.left + (container.width - width) / 2,\n top: container.top + (container.height - height) / 2,\n width,\n height,\n };\n}\n\n/**\n * `target`'s effective visual box, not necessarily its own\n * `getBoundingClientRect()`. `target` is always its full flex-box size; the\n * photo inside is usually *smaller*, letterboxed to its own aspect ratio —\n * a real bug used the container's full rect, making the animation scale\n * from/to a box far bigger than the photo ever renders at, visibly\n * shrinking it to something much smaller than the thumbnail whenever the\n * aspect ratios didn't match (the common case). Preferred sources: the real\n * rendered media child, if attached; else an analytical contain-box from\n * `aspectRatio`; else the container's own rect as a last resort.\n *\n * A real bug this guards against: on a fresh open with nothing decoded yet,\n * `target`'s only child at this point is the loading spinner (§2.3) — a\n * fixed ~40px circle, not the photo. Trusting its rect as \"the real\n * rendered media child\" made the FLIP transition scale toward/from a box\n * far smaller than the actual photo, so the scale factor came out inverted\n * (>1 instead of <1) and the spinner rendered wildly oversized for the\n * transition's first frames before shrinking back down — never the actual\n * photo, which hadn't swapped in yet either way.\n *\n * Same reasoning excludes `.shoji-slide-open-placeholder` (§2.3's low-res\n * open() stand-in): it's a real, non-spinner `<img>`, but its own rendered\n * rect reflects *its* crop/aspect ratio, not the real photo's — trusting it\n * would scale toward the placeholder's shape instead of the real photo's,\n * only to visibly jump again once the real photo swaps in and this\n * transition (already run) no longer applies. Excluding it falls through to\n * the analytical `aspectRatio`-based box below, sized for the real photo\n * regardless of what shape the placeholder happens to be.\n */\nfunction effectiveTargetBox(\n target: HTMLElement,\n aspectRatio?: number,\n naturalSize?: { width: number; height: number },\n): Box {\n const child = target.firstElementChild;\n const isPlaceholder =\n child instanceof HTMLElement &&\n (child.classList.contains('shoji-slide-spinner') ||\n child.classList.contains('shoji-slide-open-placeholder'));\n if (child instanceof HTMLElement && !isPlaceholder) {\n const rect = child.getBoundingClientRect();\n if (rect.width > 0 && rect.height > 0) return rect;\n }\n const containerRect = target.getBoundingClientRect();\n if (aspectRatio && containerRect.width > 0 && containerRect.height > 0) {\n return containedBox(containerRect, aspectRatio, naturalSize);\n }\n return containerRect;\n}\n\n/**\n * Center-to-center translate + a single uniform scale that lands `target`'s\n * effective box (`effectiveTargetBox` above) *within* `origin`'s box,\n * without distorting it. `null` if either has no real size — nothing sane\n * to animate. One scale factor, not independent scaleX/scaleY: origin and\n * the photo essentially never share an exact aspect ratio, and scaling each\n * axis independently to force a rect match visibly squeezes/stretches the\n * image. `Math.min` keeps the box fully contained within origin's rect\n * (same tradeoff `object-fit: contain` makes); center always lands exactly\n * on origin's center, only the unconstrained axis's edges fall short.\n */\n/**\n * The actual translate3d/scale3d math `computeTransform` below needs —\n * pulled out on its own so `zoomOut`'s `zoomStart` jump (a *second* use of\n * the exact same center-to-center box-fitting math, landing on a captured\n * live rect instead of `origin`'s) can reuse it directly, rather than\n * re-deriving Zoom's own transform-origin/coordinate conventions on a\n * different element — see `ZoomTransitionTarget.zoomStart`'s own doc\n * comment for why that direct-replication approach doesn't work.\n */\nfunction transformBetween(from: Box, to: Box): string {\n const scale = Math.min(to.width / from.width, to.height / from.height);\n const translateX = to.left + to.width / 2 - (from.left + from.width / 2);\n const translateY = to.top + to.height / 2 - (from.top + from.height / 2);\n // translate3d/scale3d, not translate()/scale() — forces the GPU\n // compositing path instead of a main-thread-painted 2D transform, the\n // same fix already validated for the Zoom plugin's own scale animation\n // (DESIGN.md §4.6): this is the identical technique (a large photo\n // scaled via `transform`) on the same element, just driven by open/close\n // instead of pinch/toolbar zoom.\n return `translate3d(${translateX}px, ${translateY}px, 0) scale3d(${scale}, ${scale}, 1)`;\n}\n\n/**\n * `origin`'s effective visual box, not necessarily its own\n * `getBoundingClientRect()`. In selector/DOM-markup mode without Layout,\n * `origin` is typically a plain `<a>` wrapping a `<img>` — an inline element\n * by default, whose own box is sized to a text-line-height sliver (its\n * `font-size`/`line-height`), not the image it visually contains, even\n * though the image itself paints at full size. A real bug: the zoom\n * open/close transform used that sliver directly as the FLIP target,\n * landing the animation at a tiny, essentially-random offset instead of the\n * real thumbnail's visible position and size. Layout tiles aren't affected\n * (`display: block`, sized to match their child image exactly) — preferring\n * the child's rect there returns the same box either way.\n *\n * `querySelector('img')`, not `firstElementChild` — matches `scan.ts`'s own\n * `scanImage()` convention for locating a thumbnail inside arbitrary host\n * markup, where the `<img>` isn't necessarily the first child (a badge or\n * caption element placed before it in the DOM would otherwise get measured\n * instead, no more correct than the wrapper's own sliver).\n */\nfunction effectiveOriginBox(origin: HTMLElement): Box {\n const img = origin.querySelector('img');\n if (img) {\n const rect = img.getBoundingClientRect();\n if (rect.width > 0 && rect.height > 0) return rect;\n }\n return origin.getBoundingClientRect();\n}\n\nfunction computeTransform(\n origin: HTMLElement,\n target: HTMLElement,\n aspectRatio?: number,\n naturalSize?: { width: number; height: number },\n): string | null {\n const originRect = effectiveOriginBox(origin);\n const targetRect = effectiveTargetBox(target, aspectRatio, naturalSize);\n if (\n targetRect.width === 0 ||\n targetRect.height === 0 ||\n originRect.width === 0 ||\n originRect.height === 0\n ) {\n return null;\n }\n return transformBetween(targetRect, originRect);\n}\n\n/**\n * Waits for `target`'s own transition (on `property`, default `'transform'`)\n * to end (with a safety-net timeout in case transitionend never fires — an\n * interrupted/removed element, a browser quirk, or `--shoji-duration: 0ms`\n * under `prefers-reduced-motion`, which some browsers never fire a real\n * transitionend for at all) then calls `cb` exactly once. Exported: the\n * gesture-driven drag-to-navigate/drag-to-close settle animations (§2.4,\n * `Gallery.ts`) reuse this same wait-with-fallback logic rather than\n * duplicating it — same instant-jump-then-transition FLIP family of moves\n * as the zoom transition, just on a different element/property pairing.\n * `close()`'s own controls-fade-before-zoom-out sequencing (§2.6a) reuses it\n * a second way, waiting on `opacity` instead.\n */\nexport function waitForTransitionEnd(\n target: HTMLElement,\n cb: () => void,\n property = 'transform',\n): void {\n const durationMs = parseCssTime(getComputedStyle(target).transitionDuration);\n let done = false;\n const finish = (): void => {\n if (done) return;\n done = true;\n target.removeEventListener('transitionend', onEnd);\n cb();\n };\n const onEnd = (event: Event): void => {\n if (event.target === target && (event as TransitionEvent).propertyName === property) finish();\n };\n target.addEventListener('transitionend', onEnd);\n setTimeout(finish, durationMs + 100);\n}\n\n/**\n * A real bug: unconditionally clearing `transform` here could wipe out a\n * value another plugin (rotateFlip) legitimately set on the *same* element\n * in the time between this animation starting and its cleanup callback\n * firing (transitionend, or the fallback timeout — either can land well\n * after a quick click elsewhere). Only clears `transform` if it still\n * matches what this animation itself last wrote — nothing else has touched\n * it since — otherwise leaves it alone. `transition`/`transformOrigin`\n * are always safe to clear; nothing else in this codebase sets them.\n *\n * A second real bug, found via reopening the lightbox: `expectedTransform`\n * must be the value *read back* from `target.style.transform` right after\n * assigning it, not the raw string `computeTransform()` produced. Setting\n * `element.style.transform` to a string containing an arbitrary JS float\n * (e.g. `scale(0.10416666666666667)`, `computeTransform`'s un-rounded\n * `Math.min(...)` result) and reading it back gives a *differently\n * formatted* string — the browser's CSSOM serializer rounds/reformats\n * numeric values on its own (observed in Chromium: `scale(0.104167)`).\n * Comparing the raw JS string against that reformatted one here always\n * failed for any scale factor without a short, clean decimal, silently\n * skipping the clear — permanently leaving `zoomOut`'s shrink transform\n * applied to `.shoji-slide-media` after the lightbox closed. The next\n * `open()`'s `computeTransform` then measured that *already-shrunk*\n * element's `getBoundingClientRect()` as if it were the natural size,\n * computing a near-1 (barely visible) scale instead of a real zoom-in —\n * reads as \"doesn't zoom, just appears,\" and compounds on every further\n * open/close cycle since the stuck transform is never cleared either way.\n * `zoomIn`'s own `'none'` literal never hit this — a fixed string round-trips\n * through the CSSOM unchanged, unlike an arbitrary computed float.\n *\n * `opacity` is cleared unconditionally, same as `transition`/\n * `transformOrigin` — a completed drag-close (`Gallery.beginClose`) bakes\n * the drag's own dim onto `target` before this animation starts; nothing\n * else in this codebase sets `.shoji-slide-media`'s opacity, so there's\n * nothing else's value this could ever clobber.\n */\nfunction clearInlineTransform(target: HTMLElement, expectedTransform: string): void {\n target.style.transition = '';\n target.style.transformOrigin = '';\n target.style.willChange = '';\n target.style.opacity = '';\n if (target.style.transform === expectedTransform) target.style.transform = '';\n}\n\n/**\n * FLIP-style open: `target` starts visually at `origin`'s position/size (an\n * instant, untransitioned jump), then transitions to its natural layout —\n * reads as \"growing out of the thumbnail.\" Fire-and-forget: cleans up its\n * own inline styles once the transition ends, nothing to await.\n */\nexport function zoomIn({ origin, target, aspectRatio, naturalSize }: ZoomTransitionTarget): void {\n if (prefersReducedMotion()) return;\n const transform = computeTransform(origin, target, aspectRatio, naturalSize);\n if (!transform) return;\n\n target.style.transition = 'none';\n // computeTransform's translateX/Y is center-to-center math — must pair with\n // a center transform-origin (the CSS default), not 'top left', or the\n // scaled box ends up offset from origin by however far origin's center\n // sits from its own top-left corner.\n target.style.transformOrigin = 'center';\n // Promotes target onto its own compositing layer before the animated\n // transform starts, not mid-animation — same GPU-seam mitigation as the\n // Zoom plugin (DESIGN.md §4.6). Cleared in clearInlineTransform once the\n // transition ends, not left on permanently: this is the offset-0 pool\n // slot, alive for the gallery's whole lifetime, not a class scoped to\n // only-while-zoomed.\n target.style.willChange = 'transform';\n target.style.transform = transform;\n void target.offsetHeight; // force the instant jump to commit before transitioning away from it\n target.style.transition = 'transform var(--shoji-duration) var(--shoji-easing)';\n target.style.transform = 'none';\n\n waitForTransitionEnd(target, () => clearInlineTransform(target, 'none'));\n}\n\n/**\n * Reverse of `zoomIn`: `target` transitions from its natural position down\n * to `origin`'s rect — \"shrinking back into the thumbnail.\" `onComplete`\n * always fires exactly once, synchronously if there's nothing to animate\n * (reduced motion, or no valid rect), otherwise once the transition ends —\n * callers use this to know when it's safe to actually hide/finalize.\n */\nexport function zoomOut(\n { origin, target, aspectRatio, naturalSize, dragStart, zoomStart }: ZoomTransitionTarget,\n onComplete: () => void,\n): void {\n if (prefersReducedMotion()) {\n onComplete();\n return;\n }\n // Measured before dragStart/zoomStart is ever applied to `target` — see\n // ZoomTransitionTarget.dragStart's doc comment for why the order matters.\n const transform = computeTransform(origin, target, aspectRatio, naturalSize);\n if (!transform) {\n onComplete();\n return;\n }\n\n // computeTransform's translateX/Y is center-to-center math — must pair with\n // a center transform-origin (the CSS default), not 'top left', or the\n // scaled box ends up offset from origin by however far origin's center\n // sits from its own top-left corner.\n target.style.transformOrigin = 'center';\n target.style.willChange = 'transform'; // see zoomIn's doc comment on this line\n if (dragStart) {\n // Instant jump to exactly where the drag left off — same FLIP\n // technique `zoomIn()` uses to jump onto origin's box before\n // transitioning away from it, just jumping to wherever the drag left\n // off instead of a fixed point. translate3d/scale3d (not the 2D forms),\n // matching the landing transform's own function list below — mismatched\n // function types between the transition's start/end values force the\n // browser into matrix-decomposition interpolation instead of simple\n // per-parameter interpolation, and lose the GPU-compositing path this\n // codebase otherwise always uses for this element (DESIGN.md §2.3b).\n target.style.transition = 'none';\n target.style.transform = `translate3d(0px, ${dragStart.translateY}px, 0px) scale3d(${dragStart.scale}, ${dragStart.scale}, 1)`;\n target.style.opacity = String(dragStart.opacity);\n void target.offsetHeight; // commit the jump before transitioning away from it\n } else if (zoomStart) {\n // Same FLIP jump as dragStart above, computed the same center-to-center\n // way as `transform` above (not dragStart's raw translate/scale reuse\n // — see ZoomTransitionTarget.zoomStart's doc comment for why that\n // doesn't work here). `target`'s own effective box, re-measured fresh:\n // cheap, and guaranteed unchanged since the read inside\n // computeTransform() above — nothing's touched `target` in between.\n // Still correct even if `target` is currently rotated/flipped (the\n // RotateFlip plugin, applied directly to this same element, unlike\n // Zoom's own scale/pan — see below): both this measurement and\n // `zoomStart` itself were captured at the same rotation, so the\n // scale/translate between their two (equally rotated) bounding boxes\n // still isolates the zoom-only delta correctly.\n const targetRect = effectiveTargetBox(target, aspectRatio, naturalSize);\n if (targetRect.width > 0 && targetRect.height > 0) {\n // Composed onto whatever's already there, never replacing it outright\n // — a real bug, reported from real usage: closing while both rotated/\n // flipped *and* zoomed wholesale-overwrote RotateFlip's own\n // scaleX/scaleY/rotate() with this jump's plain translate/scale,\n // un-rotating *instantly* right here, before the real transition\n // below even starts — a snap to neutral rotation, then a separate\n // zoom-out, instead of the smooth combined un-rotate-while-shrinking\n // motion closing while rotated (without zoomStart at all) already\n // gets for free: transitioning target's transform from *whatever it\n // currently is* to the final plain shrink value is what makes that\n // work, and it needs target's rotation to still be sitting there\n // when the transition starts, not already erased by this jump.\n const existing = target.style.transform;\n const jump = transformBetween(targetRect, zoomStart);\n target.style.transition = 'none';\n target.style.transform = existing && existing !== 'none' ? `${existing} ${jump}` : jump;\n void target.offsetHeight;\n }\n }\n target.style.transition = 'transform var(--shoji-duration) var(--shoji-easing)';\n void target.offsetHeight;\n target.style.transform = transform;\n // Read back what the browser actually stored, not the raw string just\n // assigned — see clearInlineTransform's own doc comment for why the two\n // can differ (CSSOM float reformatting) and why that difference matters.\n const appliedTransform = target.style.transform;\n\n waitForTransitionEnd(target, () => {\n clearInlineTransform(target, appliedTransform);\n onComplete();\n });\n}\n","import {\n GestureEngine,\n type GestureDirection,\n type GestureEngineOptions,\n} from '../gestures/GestureEngine';\nimport type { SlideManager } from './SlideManager';\nimport { waitForTransitionEnd, type FrozenDragTransform } from './zoomTransition';\n\n/** DESIGN.md §2.4 — the drag-released settle easing, distinct from `--shoji-easing`'s open/close/zoom curve. */\nconst DRAG_SETTLE_TRANSITION = 'transform var(--shoji-duration) var(--shoji-momentum-easing)';\nconst DRAG_FEEDBACK_TRANSITION =\n 'transform var(--shoji-duration) var(--shoji-momentum-easing), opacity var(--shoji-duration) var(--shoji-momentum-easing)';\n/** px of vertical drag at which live close-feedback maxes out — independent of `swipeThreshold`, which decides actual completion. */\nconst VERTICAL_FEEDBACK_DISTANCE = 160;\n\n/**\n * A real interactive control, plus `.shoji-caption` — a click/drag starting\n * there should select/scroll its text natively, not get captured as a\n * gesture. Also used by `Gallery.ts`'s `isBackdropClick`, which used to have\n * its own narrower list (missing select/input/textarea/a[href]) — a\n * plugin-mounted non-button control got misread as a backdrop click and\n * closed the gallery. One shared selector so that can't recur.\n *\n * `.shoji-caption-modal` (DESIGN.md §2.3a) — a real bug, reported from real\n * usage: `GestureEngine`'s own tap recognition (`ignore` gates it before\n * any pointer tracking starts, `GestureEngine.ts`) runs entirely on\n * pointer events, independent of whatever a click listener's own\n * `stopPropagation()` does — a tap anywhere inside the caption modal\n * (its content area specifically; the close button was already covered as\n * a real `<button>`) still read as a legitimate tap on the dialog and fired\n * the `tap` bus event, which the Autoplay plugin listens to for its own\n * tap-to-toggle-play/pause. Excluding the whole modal here is what\n * actually stops that, not anything at the click-event layer.\n */\nexport const INTERACTIVE_CONTROL_SELECTOR =\n 'button, video, input, select, textarea, a[href], [data-shoji-no-drag], .shoji-caption, .shoji-caption-modal, .shoji-toolbar-overflow-panel';\n\n/**\n * `INTERACTIVE_CONTROL_SELECTOR` minus `video` — everything else there still\n * needs a real click/drag to behave natively (select/scroll caption text,\n * a plugin's own control, etc.), same reasoning as that selector's own doc\n * comment. `video` is carved out separately, below: unlike those, a native\n * `<video controls>`'s own interactive surface (tap-to-toggle, the\n * scrub-bar drag) isn't the *whole* element — DESIGN.md §2.4/§4.3's\n * swipe-to-navigate-video-slides feature needs gestures to reach the rest\n * of it, not be blocked outright the way the shared selector above still\n * blocks it for backdrop-click/caption-selection purposes.\n */\nconst GESTURE_IGNORE_SELECTOR =\n 'button, input, select, textarea, a[href], [data-shoji-no-drag], .shoji-caption, .shoji-caption-modal, .shoji-toolbar-overflow-panel';\n\n/**\n * A native `<video controls>`'s own scrub-bar/tap-to-toggle chrome is\n * browser-rendered, not real DOM Shoji can measure — there's no element to\n * exclude by selector the way `GESTURE_IGNORE_SELECTOR` excludes a real\n * `<button>`. Approximated instead as a fixed bottom margin\n * (`--shoji-video-gesture-margin`, shoji.css), reserved for the browser's\n * own touch handling; a touch starting above it is fair game for Shoji's\n * own gestures, same as anywhere on a photo slide.\n */\nfunction isInVideoControlsMargin(event: PointerEvent, video: HTMLVideoElement): boolean {\n const rect = video.getBoundingClientRect();\n const margin = parseFloat(\n getComputedStyle(video).getPropertyValue('--shoji-video-gesture-margin'),\n );\n return event.clientY >= rect.bottom - (Number.isFinite(margin) ? margin : 0);\n}\n\n/**\n * A real bug, found auditing this feature end to end with a real browser\n * (not caught by any jsdom unit test — real `click`-after-drag retargeting\n * only happens in a real browser, same gap `GestureEngine`'s own\n * `suppressRetargetedClick` doc comment describes): a drag starting inside\n * a video's reserved bottom margin is correctly never tracked as a Shoji\n * gesture (see `isInVideoControlsMargin` above) — left alone for the\n * browser's own scrub-bar handling. But the pointer is free to move\n * anywhere before release, the same way a real scrub-bar drag routinely\n * does, and an un-captured pointer's trailing `click` lands wherever it\n * actually ends up — often well past the video's own rendered bounds, in\n * `.shoji-slide-media`'s empty letterbox padding around it. That's not a\n * real interactive control (`INTERACTIVE_CONTROL_SELECTOR`), so `Gallery.ts`'s\n * `isBackdropClick` read it as a genuine backdrop click and closed the\n * gallery — defeating \"left alone for the browser's own controls\" the\n * moment a real scrub-bar interaction moved vertically at all. Confirmed\n * directly: a drag starting 5px above a video's bottom edge, moving 200px\n * down, closed the gallery even though `isInVideoControlsMargin` correctly\n * ignored the pointerdown. Same fix as a captured drag's own retargeted\n * click — swallow exactly one trailing click, capture phase, so it never\n * reaches `isBackdropClick` at all. A fallback timeout also removes the\n * listener in case no `click` ever comes, so it can't swallow a later,\n * unrelated one.\n */\nfunction suppressNextClick(dialog: HTMLElement): void {\n let done = false;\n const cleanup = (event?: MouseEvent): void => {\n if (done) return;\n done = true;\n event?.stopPropagation();\n dialog.removeEventListener('click', cleanup, { capture: true });\n };\n dialog.addEventListener('click', cleanup, { capture: true });\n setTimeout(cleanup, 500);\n}\n\n/**\n * `.shoji-caption--video` (shoji.css, DESIGN.md §2.3a/§4-video) sets\n * `pointer-events: none` on a video slide's caption, letting a click reach\n * the video/provider controls underneath it — which also means it never\n * appears in `event.composedPath()` there at all, so\n * `GESTURE_IGNORE_SELECTOR`'s selector-based check can't see it, the same\n * gap `Gallery.ts`'s `isBackdropClick()` already solves the same way:\n * checked by coordinates instead, the one place this function has to be.\n * A real bug this fixes: swipe-to-navigate/drag-to-close over a provider\n * video's own letterboxed margins (below) would otherwise engage right\n * through the caption sitting over them.\n */\nfunction isOverVideoCaption(event: PointerEvent, caption: HTMLElement): boolean {\n if (caption.hidden || !caption.classList.contains('shoji-caption--video')) return false;\n const rect = caption.getBoundingClientRect();\n return (\n event.clientX >= rect.left &&\n event.clientX <= rect.right &&\n event.clientY >= rect.top &&\n event.clientY <= rect.bottom\n );\n}\n\n/** A click/drag starting on a real control (or, on a video slide, its click-through caption — see `isOverVideoCaption`) shouldn't also be captured as a gesture — see `GESTURE_IGNORE_SELECTOR`/`isInVideoControlsMargin`. Ignoring specifically for the video-margin reason also arms `suppressNextClick` — see its own doc comment. */\nfunction shouldIgnoreGesture(\n event: PointerEvent,\n caption: HTMLElement,\n dialog: HTMLElement,\n): boolean {\n if (isOverVideoCaption(event, caption)) return true;\n const path = event.composedPath();\n const video = path.find((node): node is HTMLVideoElement => node instanceof HTMLVideoElement);\n if (video) {\n if (!isInVideoControlsMargin(event, video)) return false;\n suppressNextClick(dialog);\n return true;\n }\n return path.some((node) => node instanceof Element && node.matches(GESTURE_IGNORE_SELECTOR));\n}\n\n/** What `GestureController` needs from `Gallery` — narrow on purpose, so this module never reaches into Gallery internals beyond this contract. */\nexport interface GestureControllerHost {\n dialog: HTMLElement;\n /** Read live, not snapshotted — `shouldIgnoreGesture()` needs its current bounding box/visibility on every gesture start, not whatever it was when `GestureController` was constructed. See `isOverVideoCaption`. */\n caption: HTMLElement;\n slides: SlideManager;\n canGoNext(): boolean;\n canGoPrev(): boolean;\n next(): void;\n prev(): void;\n close(): void;\n /** Same effect as `close()`, but for a completed vertical swipe — skips the separate controls-fade pause in front of the zoom-out (see `finishVerticalDrag`). */\n closeFromSwipe(frozenDrag: FrozenDragTransform): void;\n /** Live, reversible — crossing the close threshold mid-drag hides the toolbar/nav/counter/caption (a \"let go now and this closes\" cue); dragging back under it before release reveals them again. See `applyVerticalDragFeedback`. */\n setControlsHiddenForDrag(hidden: boolean): void;\n /** `closable: false` (GalleryOptions) — false suspends vertical drag-to-close entirely: no live feedback, release never calls `close()`. */\n canClose(): boolean;\n onActivity(): void;\n /** DESIGN.md §4-zoom — true while zoomed; suspends drag-to-navigate/close. */\n isZoomed(): boolean;\n}\n\n/** Relays for gestures core has no built-in behavior for — DESIGN.md §2.4; the zoom plugin (§4-zoom) consumes these via the gallery event bus. */\nexport interface GestureRelayCallbacks {\n onTap(x: number, y: number): void;\n onDoubleTap(x: number, y: number): void;\n onPinchStart(centerX: number, centerY: number): void;\n onPinchMove(scale: number, centerX: number, centerY: number): void;\n onPinchEnd(): void;\n onWheelZoom(deltaScale: number, x: number, y: number): void;\n}\n\n/**\n * DESIGN.md §2.4 — owns the gesture-driven side of the lightbox: horizontal\n * drag-to-navigate and vertical drag-to-close, built on `GestureEngine`\n * (`src/gestures/`) plus the live-feedback/settle-animation mechanics\n * specific to this lightbox's DOM. Split out of `Gallery.ts` (CLAUDE.md:\n * files ≤ ~400 lines) — `Gallery` only constructs this, forwards the host\n * callbacks it needs, and relays no-built-in-effect gestures onto its bus.\n */\nexport class GestureController {\n private readonly engine: GestureEngine;\n /** Same distance `GestureEngine` itself uses to decide a release would complete the close — reused here so the live controls-hide cue in `applyVerticalDragFeedback` actually means what it visually claims. */\n private readonly controlsHideThreshold: number;\n private controlsHiddenForDrag = false;\n private lastDragDelta = 0;\n\n constructor(\n private readonly host: GestureControllerHost,\n relay: GestureRelayCallbacks,\n options?: Partial<GestureEngineOptions>,\n ) {\n this.controlsHideThreshold = options?.swipeThreshold ?? 50;\n this.engine = new GestureEngine(\n host.dialog,\n {\n ignore: (event) => shouldIgnoreGesture(event, host.caption, host.dialog),\n shouldCapture: () => !host.isZoomed(),\n onDragStart: () => host.onActivity(),\n onDragMove: this.onDragMove,\n onDragEnd: this.onDragEnd,\n onTap: (x, y) => relay.onTap(x, y),\n onDoubleTap: (x, y) => relay.onDoubleTap(x, y),\n onPinchStart: (centerX, centerY) => relay.onPinchStart(centerX, centerY),\n onPinchMove: (scale, centerX, centerY) => relay.onPinchMove(scale, centerX, centerY),\n onPinchEnd: () => relay.onPinchEnd(),\n onWheelZoom: (deltaScale, x, y) => relay.onWheelZoom(deltaScale, x, y),\n },\n options,\n );\n }\n\n destroy(): void {\n this.engine.destroy();\n }\n\n /** Horizontal follows the finger 1:1; vertical drives close-feedback instead. Axis-locked, one branch per drag. */\n private readonly onDragMove = (direction: GestureDirection, delta: number): void => {\n if (this.host.isZoomed()) return; // the zoom plugin owns pan while zoomed\n if (direction === 'horizontal') {\n this.host.slides.setDragOffset(delta, null);\n } else if (this.host.canClose()) {\n this.applyVerticalDragFeedback(delta);\n }\n };\n\n private readonly onDragEnd = (\n direction: GestureDirection,\n delta: number,\n _velocity: number,\n completed: boolean,\n ): void => {\n if (this.host.isZoomed()) return;\n if (direction === 'horizontal') {\n this.finishHorizontalDrag(delta, completed);\n } else {\n this.finishVerticalDrag(completed);\n }\n };\n\n /** `completed` decides intent; `canGoNext`/`canGoPrev` guard whether that's actually possible (`loop: false` at an end item). */\n private finishHorizontalDrag(delta: number, completed: boolean): void {\n const goingNext = delta < 0;\n const canMove = goingNext ? this.host.canGoNext() : this.host.canGoPrev();\n\n if (!completed || delta === 0 || !canMove) {\n this.settleDragOffset(0, () => {});\n return;\n }\n\n const width = this.host.dialog.getBoundingClientRect().width || 1;\n const target = goingNext ? -width : width;\n this.settleDragOffset(target, () => {\n // Instant, untransitioned reset in the same synchronous tick as the\n // index change — same FLIP-family trick as zoomTransition.ts's\n // instant-jump: the slot that just animated into the center position\n // is about to receive that exact same \"new current\" item's content\n // (its structural offset already matches, since preload keeps it\n // rendered), so resetting to offset 0 while simultaneously swapping\n // content is visually seamless, not a jump.\n this.host.slides.setDragOffset(0, null);\n if (goingNext) this.host.next();\n else this.host.prev();\n });\n }\n\n private settleDragOffset(targetPx: number, onSettled: () => void): void {\n const settleEl = this.host.slides.getSlotRoot(0);\n this.host.slides.setDragOffset(targetPx, DRAG_SETTLE_TRANSITION);\n if (!settleEl) {\n onSettled();\n return;\n }\n waitForTransitionEnd(settleEl, onSettled);\n }\n\n /**\n * Purely presentational — scales/fades the *image* as it's dragged away,\n * without closing until release decides. Applied to `host.slides.element`,\n * not `host.dialog` — requested directly: toolbar/nav/counter/caption are\n * siblings, not descendants, so they stay anchored, not moving with it.\n */\n private applyVerticalDragFeedback(delta: number): void {\n const progress = Math.min(Math.abs(delta) / VERTICAL_FEEDBACK_DISTANCE, 1);\n const slides = this.host.slides.element;\n this.lastDragDelta = delta;\n slides.style.transition = '';\n slides.style.transform = `translateY(${delta}px) scale(${1 - progress * 0.15})`;\n slides.style.opacity = String(1 - progress * 0.6);\n\n // Requested directly: past the same distance a release would close,\n // controls disappear — a \"let go now\" cue, distinct from the image's own\n // dim above (capped at 0.4, never fully hides). Reversible: retreating\n // under it before release reveals them. Only on a crossing, not every move.\n const pastThreshold = Math.abs(delta) >= this.controlsHideThreshold;\n if (pastThreshold !== this.controlsHiddenForDrag) {\n this.controlsHiddenForDrag = pastThreshold;\n this.host.setControlsHiddenForDrag(pastThreshold);\n }\n }\n\n private clearVerticalDragFeedback(animate: boolean): void {\n const slides = this.host.slides.element;\n slides.style.transition = animate ? DRAG_FEEDBACK_TRANSITION : '';\n slides.style.transform = '';\n slides.style.opacity = '';\n }\n\n private finishVerticalDrag(completed: boolean): void {\n const wasHiddenForDrag = this.controlsHiddenForDrag;\n this.controlsHiddenForDrag = false;\n if (completed && this.host.canClose()) {\n this.host.closeFromSwipe(this.takeFrozenDragTransform());\n } else {\n // Not completed, or closable: false suspended the drag entirely (no\n // feedback was ever applied by onDragMove) — either way, a clean\n // reset (harmless no-op in the suspended case, nothing to clear).\n this.clearVerticalDragFeedback(true);\n // Not closing — controls must come back if the cue above left them\n // hidden, regardless of exactly where release landed (e.g.\n // swipeVelocity deciding \"not completed\" past this threshold).\n if (wasHiddenForDrag) this.host.setControlsHiddenForDrag(false);\n }\n }\n\n /**\n * Reads the drag's last live appearance, instantly resets `.shoji-slides`\n * back to neutral (nothing left on it to visibly snap — `Gallery` bakes\n * these same values onto the photo itself in the same synchronous tick,\n * so the rendered result is unchanged, just re-homed), and returns them\n * for that hand-off. Translate is the raw, unclamped drag distance —\n * requested directly: the close animation must continue from exactly\n * where the drag left off, full stop, not recenter first. (A previous\n * version clamped this to the same 160px the dim/scale feedback ramps\n * over, to bound how far away the close animation could start — but\n * clamping is itself an instant correction: for any drag past that\n * distance, release visibly snapped the photo from wherever it actually\n * was back to the clamped point, before the real shrink-to-thumbnail\n * motion continued from there. That snap — not the shrink itself — is\n * what reads as \"jumps to a small image in the middle of the screen.\"\n * Reported from real usage, confirmed on video: released past the clamp,\n * the photo visibly jumped from off-screen back to near-center in a\n * single frame.)\n */\n private takeFrozenDragTransform(): FrozenDragTransform {\n const progress = Math.min(Math.abs(this.lastDragDelta) / VERTICAL_FEEDBACK_DISTANCE, 1);\n const frozen: FrozenDragTransform = {\n translateY: this.lastDragDelta,\n scale: 1 - progress * 0.15,\n opacity: 1 - progress * 0.6,\n };\n this.clearVerticalDragFeedback(false);\n return frozen;\n }\n}\n","/** DESIGN.md §2.6 — visually-hidden `aria-live` region announcing slide changes. */\nexport class LiveRegion {\n readonly element: HTMLElement;\n\n constructor() {\n this.element = document.createElement('div');\n this.element.className = 'shoji-live-region';\n this.element.setAttribute('role', 'status');\n this.element.setAttribute('aria-live', 'polite');\n }\n\n announce(text: string): void {\n // Clear-then-set (with a forced reflow between) so repeated identical\n // announcements still fire in real assistive tech, which otherwise\n // ignores a text node set to the same value it already held.\n this.element.textContent = '';\n void this.element.offsetWidth;\n this.element.textContent = text;\n }\n}\n","import type { GalleryItem, GalleryItemInput, MediaSource, VideoDescriptor } from './types';\n\n/** DESIGN.md §2.1 zero-config quickstart. */\nexport const DEFAULT_SELECTOR =\n ':scope > a, :scope > [data-shoji-src], :scope > [data-shoji-video]';\n\nexport interface ScannedItem {\n element: HTMLElement;\n item: GalleryItem;\n}\n\nexport function scanContainer(\n container: HTMLElement,\n selector: string = DEFAULT_SELECTOR,\n): ScannedItem[] {\n const elements = Array.from(container.querySelectorAll<HTMLElement>(selector));\n const scanned: ScannedItem[] = [];\n for (const element of elements) {\n const item = scanVideo(element) ?? scanImage(element);\n if (item) scanned.push({ element, item });\n }\n return scanned;\n}\n\nfunction attr(el: Element, name: string): string | undefined {\n return el.getAttribute(name) ?? undefined;\n}\n\nfunction numAttr(el: Element, name: string): number | undefined {\n const raw = el.getAttribute(name);\n if (raw === null) return undefined;\n const n = Number(raw);\n return Number.isFinite(n) ? n : undefined;\n}\n\n/**\n * `caption`/`id` first, then any other `data-shoji-*` attribute with no\n * matching `GalleryItem` field lands in `item.data`, keyed verbatim (not\n * camelCased) — DESIGN.md §2.1. By this point every named field above is\n * already set, so `key in item` alone tells known suffixes (src/thumb/\n * alt/.../poster) apart from custom ones — except attributes that fold into\n * a *nested* field (`video-id`/`video-provider` → `item.video.id`/\n * `.provider`, both already applied in `scanVideo` by the time this runs)\n * or have no field of their own at all (`no-drag`, GestureController's\n * drag-opt-out); `key in item` can't see either case, so both are excluded\n * by name instead.\n */\nconst NO_OWN_DATA_FIELD = new Set(['no-drag', 'video-id', 'video-provider']);\n\nfunction applyCommon(element: HTMLElement, item: GalleryItem, src: string): void {\n const caption = attr(element, 'data-shoji-caption');\n if (caption) item.caption = caption;\n item.id = attr(element, 'data-shoji-id') ?? src;\n\n let data: Record<string, string> | undefined;\n for (const a of element.attributes) {\n if (!a.name.startsWith('data-shoji-')) continue;\n const key = a.name.slice(11);\n if (!NO_OWN_DATA_FIELD.has(key) && !(key in item)) (data ??= {})[key] = a.value;\n }\n if (data) item.data = data;\n}\n\nfunction scanImage(element: HTMLElement): GalleryItem | undefined {\n const src = attr(element, 'href') ?? attr(element, 'data-shoji-src');\n if (!src) return undefined;\n\n const item: GalleryItem = { src };\n const img = element.querySelector('img');\n const thumb = img?.getAttribute('src') ?? attr(element, 'data-shoji-thumb');\n if (thumb) item.thumb = thumb;\n const alt = img?.getAttribute('alt') ?? attr(element, 'data-shoji-alt');\n if (alt) item.alt = alt;\n const width = numAttr(element, 'data-shoji-width');\n if (width !== undefined) item.width = width;\n const height = numAttr(element, 'data-shoji-height');\n if (height !== undefined) item.height = height;\n // On the wrapping element, not the inner <img> (that's the thumb; `src`\n // above is the full-resolution image, so it needs its own attribute).\n const srcset = attr(element, 'data-shoji-srcset');\n if (srcset) item.srcset = srcset;\n const sizes = attr(element, 'data-shoji-sizes');\n if (sizes) item.sizes = sizes;\n applyCommon(element, item, src);\n return item;\n}\n\n/**\n * DESIGN.md §2.1 — a nested `<video>` is always html5; `data-shoji-video`\n * checks known provider hosts first. `data-shoji-video-id`/\n * `data-shoji-video-provider` (both optional) can override what would\n * otherwise be parsed out of that URL — see `resolveExplicitVideo`.\n */\nfunction scanVideo(element: HTMLElement): GalleryItem | undefined {\n const videoIdAttr = attr(element, 'data-shoji-video-id');\n const videoEl = element.querySelector('video');\n if (videoEl) {\n if (videoIdAttr) {\n console.warn(\n `Shoji: data-shoji-video-id=\"${videoIdAttr}\" ignored — no effect on a nested <video> (always html5).`,\n );\n }\n const sources: MediaSource[] = Array.from(videoEl.querySelectorAll('source'))\n .map((s) => ({ src: s.getAttribute('src') ?? '', type: s.getAttribute('type') ?? '' }))\n .filter((s) => s.src);\n const src = videoEl.getAttribute('src') ?? sources[0]?.src;\n if (!src) return undefined;\n\n const item: GalleryItem = { src, video: { provider: 'html5' } };\n if (sources.length) item.sources = sources;\n const poster = videoEl.getAttribute('poster');\n if (poster) item.poster = poster;\n // Same bug as the data-shoji-video branch below: without this, these\n // fall through into item.data.width/height as strings instead.\n const width = numAttr(element, 'data-shoji-width');\n if (width !== undefined) item.width = width;\n const height = numAttr(element, 'data-shoji-height');\n if (height !== undefined) item.height = height;\n applyCommon(element, item, src);\n return item;\n }\n\n const videoUrl = attr(element, 'data-shoji-video');\n if (videoUrl) {\n const providerAttr = attr(element, 'data-shoji-video-provider');\n const resolved = resolveExplicitVideo(videoUrl, providerAttr, videoIdAttr);\n const item: GalleryItem = { src: videoUrl, video: resolved.video };\n if (resolved.sources) item.sources = resolved.sources;\n const poster = attr(element, 'data-shoji-poster');\n if (poster) item.poster = poster;\n // A real bug: unlike `scanImage`, this never read these — they fell\n // through into `item.data.width`/`item.data.height` as strings instead\n // of `item.width`/`item.height`, silently losing `applyAspect()`'s\n // aspect-ratio and the open-placeholder's explicit natural-size box for\n // every DOM-mode (`data-shoji-*`) provider-video item.\n const width = numAttr(element, 'data-shoji-width');\n if (width !== undefined) item.width = width;\n const height = numAttr(element, 'data-shoji-height');\n if (height !== undefined) item.height = height;\n applyCommon(element, item, videoUrl);\n return item;\n }\n\n if (videoIdAttr) {\n console.warn(\n `Shoji: data-shoji-video-id=\"${videoIdAttr}\" ignored — requires a data-shoji-video URL.`,\n );\n }\n return undefined;\n}\n\nconst KNOWN_VIDEO_PROVIDERS = new Set(['youtube', 'vimeo', 'html5']);\n\nfunction html5Fallback(url: string): { video: VideoDescriptor; sources: MediaSource[] } {\n return { video: { provider: 'html5' }, sources: [{ src: url, type: guessVideoType(url) }] };\n}\n\n/**\n * `data-shoji-video-provider` declares the provider outright — trusts an id\n * even off a non-YouTube/non-Vimeo host (e.g. an internal proxy URL).\n * Mirrors dynamic mode's `video: { provider: '...' }`.\n */\nfunction resolveExplicitVideo(\n url: string,\n providerAttr: string | undefined,\n idAttr: string | undefined,\n): { video: VideoDescriptor; sources?: MediaSource[] } {\n if (providerAttr && !KNOWN_VIDEO_PROVIDERS.has(providerAttr)) {\n console.warn(`Shoji: data-shoji-video-provider=\"${providerAttr}\" isn't recognized — ignored.`);\n providerAttr = undefined;\n }\n\n if (providerAttr === 'html5') {\n if (idAttr)\n console.warn(`Shoji: data-shoji-video-id=\"${idAttr}\" ignored — provider html5 has no id.`);\n return html5Fallback(url);\n }\n\n const youtube = resolveDetectedProvider(\n 'youtube',\n url,\n providerAttr,\n idAttr,\n isYouTubeUrl,\n detectYouTubeId,\n );\n if (youtube) return youtube;\n const vimeo = resolveDetectedProvider(\n 'vimeo',\n url,\n providerAttr,\n idAttr,\n isVimeoUrl,\n detectVimeoId,\n );\n if (vimeo) return vimeo;\n\n if (idAttr) {\n console.warn(\n `Shoji: data-shoji-video-id=\"${idAttr}\" ignored — data-shoji-video=\"${url}\" isn't a recognized YouTube/Vimeo URL; falling back to html5.`,\n );\n }\n return html5Fallback(url);\n}\n\n/** Shared logic for youtube/vimeo, the only two providers with URL-based host detection. `undefined` means \"not this provider, try the next one\"; anything else is final, explicit trusting `idAttr` off any host, unset falling back to html5 quietly. */\nfunction resolveDetectedProvider(\n provider: 'youtube' | 'vimeo',\n url: string,\n providerAttr: string | undefined,\n idAttr: string | undefined,\n isHost: (url: string) => boolean,\n detectId: (url: string) => string | undefined,\n): { video: VideoDescriptor; sources?: MediaSource[] } | undefined {\n const explicit = providerAttr === provider;\n if (!explicit && !isHost(url)) return undefined;\n const id = idAttr ?? detectId(url);\n if (!id) {\n if (!explicit) return html5Fallback(url);\n console.warn(\n `Shoji: data-shoji-video-provider=\"${provider}\" but no id could be parsed from \"${url}\".`,\n );\n }\n return { video: { provider, id, url } };\n}\n\nconst YOUTUBE_HOSTS = new Set(['youtu.be', 'youtube.com', 'youtube-nocookie.com']);\nconst VIMEO_HOSTS = new Set(['vimeo.com', 'player.vimeo.com']);\n\n/** Strips the `www.`/`m.`/`music.` subdomain YouTube/Vimeo serve the same content under either way. */\nfunction normalizeVideoHost(hostname: string): string {\n return hostname.replace(/^(www|m|music)\\./, '');\n}\n\n/** Host-only, weaker than `detectYouTubeId` on purpose — tells \"wrong site\" (a real misconfig with an explicit id set) apart from \"right site, unparsed path\" (what an explicit id is for). */\nfunction isYouTubeUrl(url: string): boolean {\n try {\n return YOUTUBE_HOSTS.has(normalizeVideoHost(new URL(url, window.location.href).hostname));\n } catch {\n return false;\n }\n}\n\n/** Same reasoning as `isYouTubeUrl`, for Vimeo's two hosts (`vimeo.com` for an ordinary page link, `player.vimeo.com` for an already-embed-shaped URL). */\nfunction isVimeoUrl(url: string): boolean {\n try {\n return VIMEO_HOSTS.has(normalizeVideoHost(new URL(url, window.location.href).hostname));\n } catch {\n return false;\n }\n}\n\n/**\n * `youtu.be/{id}`, `watch?v=`, `/embed/`, `/shorts/`, `/live/` (streams/\n * premieres), `/v/` (legacy), on `youtube.com` or `youtube-nocookie.com`\n * (the privacy-enhanced embed domain) — `www.`/`m.`/`music.` subdomains all\n * normalize the same way. `undefined` otherwise.\n */\nfunction detectYouTubeId(url: string): string | undefined {\n let parsed: URL;\n try {\n parsed = new URL(url, window.location.href);\n } catch {\n return undefined;\n }\n const host = normalizeVideoHost(parsed.hostname);\n if (host === 'youtu.be') return parsed.pathname.slice(1).split('/')[0] || undefined;\n if (!YOUTUBE_HOSTS.has(host)) return undefined;\n if (parsed.pathname === '/watch') return parsed.searchParams.get('v') ?? undefined;\n const match = parsed.pathname.match(/^\\/(?:embed|shorts|live|v)\\/([^/?]+)/);\n return match?.[1];\n}\n\n/** First numeric path segment on a recognized Vimeo host — covers `vimeo.com/{id}`, `vimeo.com/{id}/{hash}` (unlisted-video privacy hash, kept in `url` not the id), `player.vimeo.com/video/{id}`, and an id buried deeper (`vimeo.com/channels/x/{id}`). `undefined` on an unrecognized host too. */\nfunction detectVimeoId(url: string): string | undefined {\n let parsed: URL;\n try {\n parsed = new URL(url, window.location.href);\n } catch {\n return undefined;\n }\n if (!VIMEO_HOSTS.has(normalizeVideoHost(parsed.hostname))) return undefined;\n return parsed.pathname.match(/\\/(\\d+)(?:\\/|$)/)?.[1];\n}\n\n/** youtube/vimeo, in host-detection preference order — same pair `resolveDetectedProvider` covers for selector mode. */\nconst DETECTABLE_PROVIDERS = [\n { provider: 'youtube' as const, isHost: isYouTubeUrl, detectId: detectYouTubeId },\n { provider: 'vimeo' as const, isHost: isVimeoUrl, detectId: detectVimeoId },\n];\n\n/**\n * Dynamic mode skips `scanContainer()` entirely, so it never gets the same\n * src-based detection `data-shoji-video`/`data-shoji-video-id` have.\n * Mirrors it here: `video: true` infers the whole descriptor from `src`\n * (youtube or vimeo if the host matches, else html5); `{provider:'youtube'|\n * 'vimeo'}` with no `id` fills in just the id. An explicit `id` always\n * wins. Warns (doesn't throw) when a detectable-provider item ends up with\n * no id — the renderer guards this at render time too, showing a\n * placeholder instead of a broken embed.\n */\nexport function resolveDynamicVideoItems(items: GalleryItemInput[]): GalleryItem[] {\n return items.map((item): GalleryItem => {\n if (item.video === true) {\n const detected = DETECTABLE_PROVIDERS.find((p) => p.isHost(item.src));\n const video: VideoDescriptor = detected\n ? { provider: detected.provider, id: detected.detectId(item.src) }\n : { provider: 'html5' };\n if ((video.provider === 'youtube' || video.provider === 'vimeo') && !video.id) {\n console.warn(\n `Shoji: item \"${item.id ?? item.src}\" has video: true, but no id could be parsed from src (\"${item.src}\") — the embed won't load. Use an explicit video.id instead.`,\n );\n }\n return { ...item, video };\n }\n // TS narrows `item.video` past this point, but not `item`'s own type —\n // hence the cast (the map's `: GalleryItem` annotation is what actually\n // catches a mistake here).\n const resolved = item as GalleryItem;\n const provider = resolved.video?.provider;\n if (provider !== 'youtube' && provider !== 'vimeo') return resolved;\n if (resolved.video && 'id' in resolved.video && resolved.video.id) return resolved;\n const id = DETECTABLE_PROVIDERS.find((p) => p.provider === provider)!.detectId(resolved.src);\n if (!id) {\n console.warn(\n `Shoji: item \"${resolved.id ?? resolved.src}\" has video.provider: '${provider}' with no id, and src (\"${resolved.src}\") isn't a recognized ${provider === 'youtube' ? 'YouTube' : 'Vimeo'} URL — the embed won't load. Set video.id explicitly.`,\n );\n return resolved;\n }\n return { ...resolved, video: { provider, id, url: resolved.video?.url } };\n });\n}\n\nfunction guessVideoType(url: string): string {\n const clean = url.split(/[?#]/)[0] ?? url;\n const ext = clean.slice(clean.lastIndexOf('.') + 1).toLowerCase();\n if (ext === 'webm') return 'video/webm';\n if (ext === 'ogg' || ext === 'ogv') return 'video/ogg';\n return 'video/mp4';\n}\n","import type { VideoProviderRenderer } from './plugin';\nimport type { GalleryItem } from './types';\nimport { containedBox } from './zoomTransition';\n\ninterface Slot {\n root: HTMLElement;\n media: HTMLElement;\n /** Which pool position (`-preload…+preload`) this slot represents — reassigned by `render()`, not fixed at construction (see the class comment below). */\n offset: number;\n /** The index this slot currently wants to show, whether or not content has arrived — also how a later-resolving decode (`pending` below) finds the right slot. */\n assignedIndex: number | null;\n /** False from the moment a slot is (re)assigned a new index until that index's content has actually swapped in — see `isActiveReady()`. */\n ready: boolean;\n}\n\n/** A fully-built, ready-to-display node for a given item index — survives slot reassignment (see `cache` below), unlike `Slot.ready`, which resets the moment its slot moves on to a different index. */\ninterface CacheEntry {\n node: HTMLElement;\n item: GalleryItem;\n}\n\nexport interface SlideManagerOptions {\n preload: number;\n videoProviders: Map<string, VideoProviderRenderer>;\n}\n\n/**\n * DESIGN.md §2.3 — only `currentIndex ± preload` exist in the DOM. Content\n * already resident and ready is never moved between slots — the slot that\n * already holds it just gets its own `offset` relabeled to its new pool\n * position; only content resident nowhere yet gets built fresh into\n * whatever's left over. O(preload) re-render cost either way, but never a\n * DOM reparent — which matters for a provider video's `<iframe>` (§4.3):\n * most browsers reload an iframe moved to a new parent, so a live embed can\n * only safely change position by relabeling, never by being moved.\n */\nexport class SlideManager {\n readonly element: HTMLElement;\n private readonly slots: Slot[];\n private readonly preload: number;\n private readonly videoProviders: Map<string, VideoProviderRenderer>;\n private dragOffsetPx = 0;\n\n /** Ready nodes keyed by item index — trimmed to `centerIndex ± preload` each `render()`, same window the slots cover, so an evicted entry's video/iframe resources get released even if no slot ever reclaims it. */\n private readonly cache = new Map<number, CacheEntry>();\n\n /**\n * Image decodes in flight, keyed by item index, not by whichever slot\n * started them — a real bug this fixes: navigating before a preload\n * decode resolved used to reassign that slot, abandoning the decode and\n * starting a duplicate elsewhere, so a slow preload could never finish,\n * only restart. `reveal()` looks up which slot currently wants this\n * index at resolve time, so a reshuffled pool still lands it correctly.\n */\n private readonly pending = new Map<number, HTMLImageElement>();\n\n constructor(options: SlideManagerOptions) {\n this.element = document.createElement('div');\n this.element.className = 'shoji-slides';\n this.preload = options.preload;\n this.videoProviders = options.videoProviders;\n\n const count = options.preload * 2 + 1;\n this.slots = Array.from({ length: count }, (_, i) => {\n const offset = i - options.preload;\n const root = document.createElement('div');\n root.className = 'shoji-slide';\n\n const media = document.createElement('div');\n media.className = 'shoji-slide-media';\n root.appendChild(media);\n\n this.element.appendChild(root);\n return { root, media, offset, assignedIndex: null, ready: false };\n });\n this.applyTransforms(null);\n }\n\n /** The active slot's `.shoji-slide-media` — not `.shoji-slide`, whose transform is owned by pool-offset positioning. */\n getActiveMedia(): HTMLElement | null {\n return this.slots.find((slot) => slot.offset === 0)?.media ?? null;\n }\n\n /** False while the active slot's content is still decoding — Gallery.ts uses this right after `render()` to show the loading state immediately, without waiting for `onLoad`. */\n isActiveReady(): boolean {\n return this.slots.find((slot) => slot.offset === 0)?.ready ?? false;\n }\n\n /**\n * DESIGN.md §2.4 — live drag-to-navigate feedback: every slot's\n * structural `offset * 100%` position gets this same `px` added on top,\n * so dragging left/right visually slides the whole pool together (the\n * adjacent preloaded slot already exists at `±100%`) — no new DOM, no\n * content re-render mid-drag. `transition`: `null` during the live drag\n * (1:1 with the pointer); a real value only for the post-release settle\n * animation, reset back to `null` once that transition ends.\n */\n setDragOffset(px: number, transition: string | null): void {\n this.dragOffsetPx = px;\n this.applyTransforms(transition);\n }\n\n /** The pool slot's `.shoji-slide` root at a structural offset — what the drag settle animation (§2.4) waits for `transitionend` on. */\n getSlotRoot(offset: number): HTMLElement | null {\n return this.slots.find((slot) => slot.offset === offset)?.root ?? null;\n }\n\n private applyTransforms(transition: string | null): void {\n for (const slot of this.slots) {\n slot.root.style.transition = transition ?? '';\n slot.root.style.transform = `translateX(calc(${slot.offset * 100}% + ${this.dragOffsetPx}px))`;\n }\n }\n\n /** Re-renders whichever slots need a different item; `onLoad` fires per index once its media settles. `openPlaceholderSrc` (only from `Gallery.open()`) swaps the centerIndex slot's spinner for a low-res placeholder once it decodes, if not already ready. `loop` matches `Gallery.ts`'s own `nextIndex()`/`prevIndex()` wrap-around — without it, the pool slot just past the last (or before the first) item has no item to preload at all, so a boundary drag reveals nothing even though the completed navigation itself already wraps correctly. */\n render(\n items: readonly GalleryItem[],\n centerIndex: number,\n onLoad: (index: number) => void,\n openPlaceholderSrc?: string,\n loop = false,\n ): void {\n // A real bug found wiring this up, in two parts:\n // 1. Wrapping is only safe from an *index-collision* standpoint when\n // there are enough real items to fill the whole pool without\n // revisiting one twice. `items.length === this.slots.length`\n // (`2*preload + 1`) looks like exactly enough — but that's the\n // degenerate case where the pool *is* the whole gallery: the \"prev\"\n // slot at the first item and the \"next\" slot at the last item both\n // wrap to the *other* end, making the first and last items adjacent\n // in the pool even though nothing actually collides on one index.\n // Confirmed directly, reproduced in CI: a real fixture built\n // specifically to keep two provider-video items apart (never within\n // `preload` of each other, so at most one `.shoji-slide-provider-\n // video` is ever resident) broke exactly this way once its\n // `demo/assets/`-dependent photo tiles were empty (gitignored, so a\n // fresh CI checkout has none) — collapsing it down to precisely\n // `items.length === this.slots.length`, wrapping the two video items\n // into each other's pool despite the fixture's own spacer tile still\n // sitting between them in the array. Requiring *strictly* more items\n // than pool slots (`>`, not `>=`) is what actually guarantees the\n // first and last items are never both in the wrapped pool at once.\n // 2. Below that threshold, an out-of-range offset just shows nothing at\n // that edge, same as `loop: false` — correct for a gallery too small\n // to usefully \"loop\" through anyway.\n const canWrap = loop && items.length > this.slots.length;\n const targets = this.slots.map((_, i) => {\n const offset = i - this.preload;\n const rawIndex = centerIndex + offset;\n const index = canWrap ? ((rawIndex % items.length) + items.length) % items.length : rawIndex;\n const item = index >= 0 && index < items.length ? items[index] : undefined;\n return { offset, index, item };\n });\n\n // Trim the cache to the same window the slots above now cover — the\n // *wrapped* set of indices, not a raw numeric range, matching `loop`;\n // a raw-range check would otherwise immediately evict a just-cached\n // wrapped entry (e.g. index 0, preloaded one slot past the last item)\n // since it falls far outside `centerIndex ± preload` as plain numbers.\n // Releases a dropped video here rather than just letting the reference\n // disappear (which would leave it paused-in-place, never released).\n const windowIndices = new Set(targets.map((t) => t.index));\n for (const [index, entry] of this.cache) {\n if (!windowIndices.has(index)) {\n releaseVideoNode(entry.node);\n this.cache.delete(index);\n }\n }\n\n // Phase 1 — claim in place, before anything gets destructively\n // released. A target already resident and ready in *some* slot (not\n // necessarily the one whose own offset happens to match) stays exactly\n // where it is — only that slot's `offset` moves (see the class comment\n // above). Still mid-decode elsewhere isn't claimed here (no `ready`\n // slot yet) — falls through to phase 2, where `ensureImageDecoding`'s\n // in-flight guard reconciles it once the one real decode resolves.\n const claimedIndices = new Set<number>();\n const claimedSlots = new Set<Slot>();\n for (const target of targets) {\n if (!target.item) continue;\n const owner = this.slots.find((s) => s.assignedIndex === target.index && s.ready);\n if (!owner) continue;\n claimedIndices.add(target.index);\n claimedSlots.add(owner);\n if (owner.offset === target.offset) continue; // already exactly right, nothing to do\n owner.offset = target.offset;\n onLoad(target.index);\n }\n\n // Phase 2 — whatever's left: slots phase 1 didn't claim are free to\n // clear (out-of-range target) or take on fresh content (a genuinely\n // new index, or one already mid-decode elsewhere).\n const freeSlots = this.slots.filter((s) => !claimedSlots.has(s));\n const freeTargets = targets.filter((t) => !t.item || !claimedIndices.has(t.index));\n\n freeTargets.forEach((target, i) => {\n const slot = freeSlots[i]!;\n slot.offset = target.offset;\n const { index, item } = target;\n\n if (!item) {\n slot.assignedIndex = null;\n slot.ready = false;\n releaseVideo(slot.media);\n slot.media.replaceChildren();\n return;\n }\n if (slot.assignedIndex === index) return; // unchanged — a decode already in flight here\n\n slot.assignedIndex = index;\n slot.ready = false;\n releaseVideo(slot.media);\n slot.media.replaceChildren(createSpinner());\n // Early, not just from swapIn/moveIn/onReady below — a provider\n // video's poster (shown synchronously, well before onReady) needs\n // --shoji-provider-video-aspect (shoji.css) set before it ever\n // paints, or it always letterboxes to the 16:9 fallback regardless of\n // the item's own declared shape.\n this.applyAspect(slot, item);\n if (index === centerIndex && openPlaceholderSrc) {\n const naturalSize =\n item.width && item.height ? { width: item.width, height: item.height } : undefined;\n this.revealOpenPlaceholder(openPlaceholderSrc, slot, index, naturalSize);\n }\n\n if (item.video?.provider === 'html5') {\n this.renderVideo(item, slot, index, onLoad);\n } else if (item.video) {\n this.renderProviderVideo(item, slot, index, onLoad);\n } else {\n this.ensureImageDecoding(item, index, onLoad); // no-ops if index is already being decoded elsewhere\n }\n });\n\n // Reflect any relabeling above immediately, never animated — masked\n // either by SlideTransition's own ghost-clone crossfade, or by landing\n // exactly on a completed drag's already-settled position (§2.4);\n // animating this too would fight both.\n this.applyTransforms(null);\n }\n\n private applyAspect(slot: Slot, item: GalleryItem): void {\n if (item.width && item.height) {\n slot.media.style.aspectRatio = `${item.width} / ${item.height}`;\n } else {\n slot.media.style.removeProperty('aspect-ratio');\n }\n // A provider video's real shape (YouTube/Vimeo iframe, via\n // shoji.css's cqw/cqh sizing) — kept separate from the aspect-ratio\n // above since that alone can't shape a non-replaced element like an\n // iframe/div (see shoji.css's own comment on this). Only set for a\n // video item: harmless either way, but meaningless for a photo, whose\n // own object-fit: contain already uses its real decoded dimensions\n // directly.\n if (item.video && item.width && item.height) {\n slot.media.style.setProperty(\n '--shoji-provider-video-aspect',\n `calc(${item.width} / ${item.height})`,\n );\n } else {\n slot.media.style.removeProperty('--shoji-provider-video-aspect');\n }\n }\n\n /** Releases the slot's old content and swaps the new node in, caching it. `ensureImageDecoding` goes through `moveIn` instead. */\n private swapIn(slot: Slot, node: HTMLElement, item: GalleryItem, index: number): void {\n releaseVideo(slot.media);\n this.applyAspect(slot, item);\n slot.media.replaceChildren(node);\n slot.ready = true;\n this.cache.set(index, { node, item });\n }\n\n /**\n * Inserts a just-decoded node into the slot waiting on it — skips\n * `releaseVideo`, unlike `swapIn`: this is its first insertion anywhere,\n * nothing outgoing to release. `render()`'s claim pass (Phase 1) never\n * calls this — it reuses `ready` content in place, no DOM touch at all.\n */\n private moveIn(slot: Slot, entry: CacheEntry, index: number): void {\n this.applyAspect(slot, entry.item);\n slot.media.replaceChildren(entry.node);\n slot.ready = true;\n this.cache.set(index, entry);\n }\n\n /** Starts decoding `item` for `index`, only if nothing already is (see `pending`). Resolves by looking up whichever slot currently wants this index, not the one active when the decode started. */\n private ensureImageDecoding(\n item: GalleryItem,\n index: number,\n onLoad: (index: number) => void,\n ): void {\n if (this.pending.has(index)) return;\n\n const img = document.createElement('img');\n img.className = 'shoji-slide-img';\n img.alt = item.alt ?? '';\n // A real bug: <img> is natively draggable by default in every browser\n // (drag-to-save-elsewhere). Left on, a real drag gesture — navigate,\n // vertical-close, or the zoom plugin's own pan — can lose the race to\n // the browser's own \"start a native image drag\" recognition, which\n // fires pointercancel and hands the interaction off to dragstart/drag\n // instead: the gesture appears to move once, then stop dead, since\n // every pointer event after the cancel is simply never dispatched here\n // again. Disabling native drag makes Pointer Events own the gesture\n // unconditionally.\n img.draggable = false;\n // The one image the viewer is actually waiting on — hint the browser to\n // schedule it ahead of any lower-priority background fetches contending\n // for the same connection pool.\n if ('fetchPriority' in img) img.fetchPriority = 'high';\n if (item.srcset) img.srcset = item.srcset;\n if (item.sizes) img.sizes = item.sizes;\n img.src = item.src;\n this.pending.set(index, img);\n\n const reveal = (): void => {\n this.pending.delete(index);\n const entry: CacheEntry = { node: img, item };\n this.cache.set(index, entry);\n const slot = this.slots.find((s) => s.assignedIndex === index);\n if (!slot) return; // nobody currently wants it anymore\n this.moveIn(slot, entry, index);\n onLoad(index);\n };\n\n if (typeof img.decode === 'function') {\n img.decode().then(reveal, reveal);\n } else {\n img.addEventListener('load', reveal, { once: true });\n img.addEventListener('error', reveal, { once: true });\n }\n }\n\n /**\n * DESIGN.md §2.3 — swaps the spinner for the placeholder once *it*\n * decodes, not immediately: `item.thumb` is often just `item.src` again,\n * so an undecoded placeholder can leave as long a blank gap as the\n * spinner it replaces.\n *\n * A real bug: `.shoji-slide-open-placeholder`'s CSS unconditionally\n * force-fills the frame (deliberate default: a real photo is usually\n * bigger than the dialog). Wrong for a genuinely small photo with known\n * `item.width`/`item.height` — same \"grows too big, snaps down once real\"\n * symptom the zoom-in transition itself had (§2.3b), but that fix only\n * governs the animated *transform*, not this placeholder's own CSS box\n * once it settles. When `naturalSize` is known, sizes the placeholder\n * explicitly instead, reusing `containedBox` (`zoomTransition.ts`) —\n * unknown-size items keep the original force-fill guess.\n *\n * A second real bug in that same fix: measured `slot.media`'s own rect as\n * \"the container\" — but `slot.media` is the exact element the zoom-in\n * transition (§2.3b) animates a `scale()` transform on. `getBoundingClientRect()`\n * reflects whatever transform is *currently* applied, and the placeholder's\n * own decode typically resolves before that ~300ms animation settles — so\n * this measured a still-small, mid-animation rect (often the origin\n * thumbnail's own tiny size) instead of the real dialog size. `slot.root`\n * (`.shoji-slide`) fixes it: same box, only ever `translateX`'d for\n * pool-offset positioning, never scaled — a stable read regardless of\n * what its child is mid-animation through.\n */\n private revealOpenPlaceholder(\n src: string,\n slot: Slot,\n index: number,\n naturalSize?: { width: number; height: number },\n ): void {\n const img = createOpenPlaceholder(src);\n const reveal = (): void => {\n if (slot.assignedIndex !== index || slot.ready) return; // stale, or the real content already won the race\n // Only loading indicators, not slot.media wholesale — a provider video\n // may have already appended its own (still-hidden) container alongside\n // it. A real bug: this only ever cleared the spinner, not a\n // `.shoji-slide-provider-poster` a provider video's `item.poster`\n // (`showProviderPoster`, below) may have *already* synchronously\n // inserted by the time this async decode resolves — leaving both\n // visible at once, the poster at its own small natural size next to\n // this placeholder's explicitly-computed large box.\n slot.media\n .querySelectorAll('.shoji-slide-spinner, .shoji-slide-provider-poster')\n .forEach((el) => el.remove());\n slot.media.appendChild(img);\n if (naturalSize) {\n const containerRect = slot.root.getBoundingClientRect();\n const box = containedBox(\n { left: 0, top: 0, width: containerRect.width, height: containerRect.height },\n naturalSize.width / naturalSize.height,\n naturalSize,\n );\n img.style.width = `${box.width}px`;\n img.style.height = `${box.height}px`;\n }\n };\n if (typeof img.decode === 'function') {\n img.decode().then(reveal, reveal);\n } else {\n img.addEventListener('load', reveal, { once: true });\n img.addEventListener('error', reveal, { once: true });\n }\n }\n\n /** Native `<video controls>` — real playback, not just a poster. Not autoplayed/muted: a deliberate click-to-play, not a background loop. */\n private renderVideo(\n item: GalleryItem,\n slot: Slot,\n index: number,\n onLoad: (index: number) => void,\n ): void {\n const hasSource = Boolean(item.src) || Boolean(item.sources && item.sources.length > 0);\n if (!hasSource) {\n const placeholder = document.createElement('div');\n placeholder.className = 'shoji-slide-placeholder';\n placeholder.textContent = 'Video';\n this.swapIn(slot, placeholder, item, index);\n onLoad(index);\n return;\n }\n\n const video = document.createElement('video');\n video.className = 'shoji-slide-video';\n video.controls = true;\n video.playsInline = true;\n // Same real bug as the image path's own `img.draggable = false` above\n // (`ensureImageDecoding()`), never fixed here since no e2e test\n // previously drove a real mouse drag over an actual native\n // `<video controls>` element to catch it: a `<video>` is natively\n // draggable by default too (drag-to-save-elsewhere), and left on, a\n // real drag gesture (navigate, vertical-close) can lose the race to the\n // browser's own \"start a native media drag\" recognition — it fires\n // `pointercancel` and hands the interaction to `dragstart`/`drag`\n // instead, so the gesture appears to move once, then stop dead, since\n // every pointer event after the cancel is simply never dispatched here\n // again. Confirmed directly: reproduced consistently (every browser\n // project) for a horizontal drag specifically, though the same\n // mechanism can affect vertical too. Disabling native drag makes\n // Pointer Events own the gesture unconditionally, same as for photos.\n video.draggable = false;\n if (item.poster) video.poster = item.poster;\n // Before `loadedmetadata`, a <video> has no real intrinsic size to\n // contain itself against (object-fit: contain, above) — the UA default\n // (300x150) until then, visibly jumping to the real shape once it\n // loads. item.width/item.height (already known up front, unlike a\n // photo's real dimensions) sidesteps that: same shape from the first\n // frame the poster shows, no jump once metadata actually arrives.\n if (item.width && item.height) {\n video.style.aspectRatio = `${item.width} / ${item.height}`;\n }\n\n if (item.sources && item.sources.length > 0) {\n for (const s of item.sources) {\n const source = document.createElement('source');\n source.src = s.src;\n source.type = s.type;\n video.appendChild(source);\n }\n } else {\n video.src = item.src;\n }\n\n // Inserted immediately (unlike the image path above) — the native\n // `poster` attribute already shows something meaningful without\n // waiting on `loadedmetadata`, so there's no decode-style gap to close\n // here; deferring would only make video slides slower to show anything.\n this.swapIn(slot, video, item, index);\n const reveal = (): void => onLoad(index);\n video.addEventListener('loadedmetadata', reveal, { once: true });\n video.addEventListener('error', reveal, { once: true });\n }\n\n /** DESIGN.md §4-video. Unregistered provider → same placeholder as no-source video. */\n private renderProviderVideo(\n item: GalleryItem,\n slot: Slot,\n index: number,\n onLoad: (index: number) => void,\n ): void {\n const video = item.video!;\n const renderFn =\n video.provider === 'custom' ? video.render : this.videoProviders.get(video.provider);\n\n if (!renderFn) {\n const placeholder = document.createElement('div');\n placeholder.className = 'shoji-slide-placeholder';\n placeholder.textContent = 'Video';\n this.swapIn(slot, placeholder, item, index);\n onLoad(index);\n return;\n }\n\n // DESIGN.md §4.3 — shows item.poster immediately, in place of the\n // generic spinner Phase 2 already put in slot.media, if the host\n // supplied one; same idea as native <video poster>, just applied to a\n // provider embed instead. A renderer without a host-supplied poster can\n // still resolve a fallback of its own via the setPoster callback below.\n if (item.poster) showProviderPoster(slot, item.poster);\n\n const container = document.createElement('div');\n container.className = 'shoji-slide-provider-video';\n container.hidden = true;\n const controller = new AbortController();\n providerAbortControllers.set(container, controller);\n\n // Unlike an <img> decode, an iframe embed only actually loads once\n // attached to the live document — attach now, hidden behind the\n // still-visible spinner (or poster, above), instead of deferring\n // attachment until ready.\n slot.media.appendChild(container);\n\n let revealed = false;\n const onReady = (): void => {\n if (revealed || controller.signal.aborted) return;\n revealed = true;\n container.hidden = false;\n // Whichever loading indicator(s) — Phase 2's spinner, a poster, an\n // open placeholder — are still sitting behind the now-visible embed,\n // never the embed itself. A real bug: a host-supplied `item.poster`\n // (`showProviderPoster`, above) and `Gallery.open()`'s own low-res\n // placeholder (`revealOpenPlaceholder`, below) are added independently\n // of each other and neither replaces the other, so *both* can be\n // sitting in `slot.media` at once by the time this runs.\n // `querySelector(...).remove()` only ever removed the first match in\n // DOM order, permanently leaking whichever indicator lost that\n // race — visible as the leaked one's image bleeding through the\n // transparent letterboxing gaps around a provider embed that isn't\n // exactly the container's own aspect ratio.\n slot.media\n .querySelectorAll(\n '.shoji-slide-spinner, .shoji-slide-open-placeholder, .shoji-slide-provider-poster',\n )\n .forEach((el) => el.remove());\n this.applyAspect(slot, item);\n slot.ready = true;\n this.cache.set(index, { node: container, item });\n onLoad(index);\n };\n const setPoster = (url: string): void => {\n // A host-supplied poster (above) always wins over a renderer's own\n // fallback — and once the real embed is showing, or this slide's\n // moved on, a fallback resolving late (e.g. an async fetch) must\n // never replace or paint over it.\n if (item.poster || revealed || controller.signal.aborted) return;\n showProviderPoster(slot, url);\n };\n renderFn(container, item, onReady, controller.signal, setPoster);\n }\n\n destroy(): void {\n for (const slot of this.slots) {\n releaseVideo(slot.media);\n slot.media.replaceChildren();\n slot.assignedIndex = null; // any decode still in `pending` correctly finds no slot wanting it once it resolves\n slot.ready = false;\n }\n this.cache.clear();\n this.pending.clear();\n this.element.remove();\n }\n}\n\n/** DESIGN.md §4.3 — a provider video's loading placeholder, host-supplied (`item.poster`) or a renderer's own fallback (`setPoster`) — replaces whichever generic indicator (spinner, or an earlier poster call) is currently in `slot.media`, same \"only one loading indicator at a time\" rule `createOpenPlaceholder`'s own callers already follow. Reuses `.shoji-slide-img`'s sizing, same reasoning as that function. */\nfunction showProviderPoster(slot: Slot, url: string): void {\n // A real bug: this only ever cleared the spinner/a previous poster, never\n // an already-shown `.shoji-slide-open-placeholder` (`revealOpenPlaceholder`,\n // above) — that placeholder's async image decode can resolve *before*\n // this (e.g. `setPoster`'s fallback, resolved later than the initial\n // synchronous poster call), leaving both visible at once.\n slot.media\n .querySelectorAll(\n '.shoji-slide-spinner, .shoji-slide-open-placeholder, .shoji-slide-provider-poster',\n )\n .forEach((el) => el.remove());\n const img = document.createElement('img');\n img.className = 'shoji-slide-img shoji-slide-provider-poster';\n img.alt = '';\n img.draggable = false;\n img.src = url;\n slot.media.appendChild(img);\n}\n\n/** DESIGN.md §2.3 — low-res open() placeholder; reuses .shoji-slide-img's sizing so the zoom-in FLIP measures it like real content (zoomTransition.ts's effectiveTargetBox only special-cases the spinner as non-content). */\nfunction createOpenPlaceholder(src: string): HTMLImageElement {\n const img = document.createElement('img');\n img.className = 'shoji-slide-img shoji-slide-open-placeholder';\n img.alt = '';\n img.draggable = false;\n img.src = src;\n return img;\n}\n\n/** DESIGN.md §2.3 — shown in a slot while its content is still decoding/loading, replacing whatever a stale \"keep the old image visible\" approach would have left there (a previous version of this did that; it read as broken, not helpful, since the old image no longer matches the caption/counter/URL that already moved on). Purely CSS-animated (a rotating ring via `border`), no JS-driven layout. */\nfunction createSpinner(): HTMLElement {\n const spinner = document.createElement('div');\n spinner.className = 'shoji-slide-spinner';\n spinner.setAttribute('aria-hidden', 'true');\n return spinner;\n}\n\nconst providerAbortControllers = new WeakMap<HTMLElement, AbortController>();\n\n/** Pause/release a slot's previous <video> or provider content, if any. */\nfunction releaseVideo(media: HTMLElement): void {\n const video = media.querySelector('video');\n if (video) releaseVideoNode(video);\n const provider = media.querySelector<HTMLElement>('.shoji-slide-provider-video');\n if (provider) releaseProviderNode(provider);\n}\n\n/**\n * Pauses whatever's playing in `media` — native `<video>`, or a provider\n * embed wired via `wirePlayableContract` (§4-video) — without\n * releasing/destroying it, unlike `releaseVideo` above. `Gallery` calls\n * this on `close()` and before every navigation, on the outgoing slide.\n */\nexport function pauseMedia(media: HTMLElement | null): void {\n const video = media?.querySelector('video');\n if (video) {\n video.pause();\n return;\n }\n const provider = media?.querySelector<HTMLElement & { pause?: () => void }>(\n '.shoji-slide-provider-video',\n );\n if (provider && typeof provider.pause === 'function') provider.pause();\n}\n\n/** Cache-eviction counterpart — the node itself might *be* the content. */\nfunction releaseVideoNode(node: HTMLElement): void {\n if (node.tagName !== 'VIDEO') {\n releaseProviderNode(node);\n return;\n }\n const video = node as HTMLVideoElement;\n video.pause();\n video.removeAttribute('src');\n video.load();\n}\n\nfunction releaseProviderNode(node: HTMLElement): void {\n const controller = providerAbortControllers.get(node);\n if (!controller) return;\n providerAbortControllers.delete(node);\n controller.abort();\n}\n","/**\n * DESIGN.md §2.5 — built-in presets, generated from a small config→transform\n * DSL rather than ~20 hand-written keyframe functions. `enter(direction)` is\n * where the incoming slide starts (then transitions to natural: `transform:\n * none; opacity: 1`); `leave(direction)` is where the outgoing clone (see\n * `SlideTransition.ts`) ends up (starts at natural, transitions to this).\n * `direction` is `1` for next()-style, `-1` for prev()-style — presets that\n * don't care (fade, zoom, flipX, deck, drop...) never read it. transform/\n * opacity only, per CLAUDE.md's hardware-accelerated-CSS3 constraint.\n */\n\nexport interface TransitionState {\n transform: string;\n opacity: number;\n}\n\nexport interface TransitionPreset {\n name: string;\n enter(direction: 1 | -1): TransitionState;\n leave(direction: 1 | -1): TransitionState;\n}\n\n/** One end of one preset. `directional: true` multiplies translateX/Y/rotateZ/Y by nav direction (mirror-image next vs prev — the \"slide\" family); omitted means the same fixed transform either way (the \"fade\" family — fadeUp always rises). */\ninterface AxisConfig {\n directional?: boolean;\n translateX?: number; // %\n translateY?: number; // %\n scale?: number;\n rotateX?: number; // deg, never direction-multiplied\n rotateY?: number; // deg\n rotateZ?: number; // deg\n perspective?: number; // px, needed alongside rotateX/Y for real 3D depth\n opacity?: number; // default 1\n}\n\nfunction resolveAxis(config: AxisConfig, direction: 1 | -1): TransitionState {\n const sign = config.directional ? direction : 1;\n const parts: string[] = [];\n if (config.perspective) parts.push(`perspective(${config.perspective}px)`);\n const tx = config.translateX !== undefined ? config.translateX * sign : 0;\n const ty = config.translateY ?? 0;\n if (tx !== 0 || ty !== 0) parts.push(`translate(${tx}%, ${ty}%)`);\n if (config.rotateY !== undefined) parts.push(`rotateY(${config.rotateY * sign}deg)`);\n if (config.rotateX !== undefined) parts.push(`rotateX(${config.rotateX}deg)`);\n if (config.rotateZ !== undefined) parts.push(`rotateZ(${config.rotateZ * sign}deg)`);\n if (config.scale !== undefined) parts.push(`scale(${config.scale})`);\n return { transform: parts.length > 0 ? parts.join(' ') : 'none', opacity: config.opacity ?? 1 };\n}\n\ninterface PresetSpec {\n name: string;\n enter: AxisConfig;\n leave: AxisConfig;\n}\n\nfunction preset(spec: PresetSpec): TransitionPreset {\n return {\n name: spec.name,\n enter: (direction) => resolveAxis(spec.enter, direction),\n leave: (direction) => resolveAxis(spec.leave, direction),\n };\n}\n\nconst PRESET_LIST: TransitionPreset[] = [\n // The four DESIGN.md calls out by name: default mode, plus fade/zoom/deck.\n preset({\n name: 'slide',\n enter: { directional: true, translateX: 100 },\n leave: { directional: true, translateX: -100 },\n }),\n preset({\n name: 'fade',\n enter: { opacity: 0 },\n leave: { opacity: 0 },\n }),\n preset({\n name: 'zoom',\n enter: { scale: 0.6, opacity: 0 },\n leave: { scale: 1.4, opacity: 0 },\n }),\n preset({\n // \"lg-style deck\": the incoming slide drops into place from slightly\n // above/small while the outgoing one recedes back and fades, like a\n // card being placed on top of a deck — not direction-dependent, a deck\n // doesn't care whether the card came from the left or right.\n name: 'deck',\n enter: { translateY: 6, scale: 0.85, opacity: 0 },\n leave: { translateY: -4, scale: 0.92, opacity: 0 },\n }),\n\n // Positional siblings of `slide`.\n preset({\n name: 'slideVertical',\n enter: { directional: true, translateY: 100 },\n leave: { directional: true, translateY: -100 },\n }),\n preset({\n name: 'push',\n // Same geometry as `slide` but travels further (120% vs 100%) and with\n // no cross-fade at all — a harder, snappier \"shove it off\" feel.\n enter: { directional: true, translateX: 120 },\n leave: { directional: true, translateX: -120 },\n }),\n\n // Fixed-direction fades (not nav-direction-aware — a named entrance style).\n preset({\n name: 'fadeUp',\n enter: { translateY: 12, opacity: 0 },\n leave: { translateY: -12, opacity: 0 },\n }),\n preset({\n name: 'fadeDown',\n enter: { translateY: -12, opacity: 0 },\n leave: { translateY: 12, opacity: 0 },\n }),\n preset({\n name: 'fadeLeft',\n enter: { translateX: -12, opacity: 0 },\n leave: { translateX: 12, opacity: 0 },\n }),\n preset({\n name: 'fadeRight',\n enter: { translateX: 12, opacity: 0 },\n leave: { translateX: -12, opacity: 0 },\n }),\n\n // Zoom family.\n preset({ name: 'zoomOut', enter: { scale: 1.4, opacity: 0 }, leave: { scale: 0.6, opacity: 0 } }),\n preset({ name: 'scaleUp', enter: { scale: 0.7 }, leave: { scale: 1.3 } }),\n preset({ name: 'scaleDown', enter: { scale: 1.3 }, leave: { scale: 0.7 } }),\n\n // Rotation family — `rotate` is fixed-direction; `rotateLeft`/`rotateRight`\n // tie their spin to nav direction instead.\n preset({\n name: 'rotate',\n enter: { rotateZ: 15, opacity: 0 },\n leave: { rotateZ: -15, opacity: 0 },\n }),\n preset({\n name: 'rotateLeft',\n enter: { directional: true, rotateZ: -25, opacity: 0 },\n leave: { directional: true, rotateZ: 25, opacity: 0 },\n }),\n preset({\n name: 'rotateRight',\n enter: { directional: true, rotateZ: 25, opacity: 0 },\n leave: { directional: true, rotateZ: -25, opacity: 0 },\n }),\n\n // 3D family — needs `perspective()` in the same transform for real depth.\n preset({\n name: 'flipX',\n enter: { rotateX: 90, perspective: 1200, opacity: 0 },\n leave: { rotateX: -90, perspective: 1200, opacity: 0 },\n }),\n preset({\n name: 'flipY',\n enter: { directional: true, rotateY: 90, perspective: 1200, opacity: 0 },\n leave: { directional: true, rotateY: -90, perspective: 1200, opacity: 0 },\n }),\n preset({\n name: 'cube',\n enter: { directional: true, rotateY: 70, translateX: 40, perspective: 1600, opacity: 1 },\n leave: { directional: true, rotateY: -70, translateX: -40, perspective: 1600, opacity: 1 },\n }),\n preset({\n name: 'coverflow',\n enter: {\n directional: true,\n rotateY: 35,\n translateX: 60,\n scale: 0.8,\n perspective: 1400,\n opacity: 0,\n },\n leave: {\n directional: true,\n rotateY: -35,\n translateX: -60,\n scale: 0.8,\n perspective: 1400,\n opacity: 0,\n },\n }),\n\n // Fixed-direction drop.\n preset({\n name: 'drop',\n enter: { translateY: -30, opacity: 0 },\n leave: { translateY: 20, opacity: 0 },\n }),\n];\n\n/** Keyed by name for `Gallery`'s `mode` lookup — an unrecognized string is treated as a custom CSS-class-pair mode instead (see `SlideTransition.animateCustom`), not an error. */\nexport const TRANSITION_PRESETS: Readonly<Record<string, TransitionPreset>> = Object.fromEntries(\n PRESET_LIST.map((p) => [p.name, p]),\n);\n","import type { SlideManager } from '../core/SlideManager';\nimport type { TransitionPreset } from './presets';\n\nfunction prefersReducedMotion(): boolean {\n return (\n typeof window.matchMedia === 'function' &&\n window.matchMedia('(prefers-reduced-motion: reduce)').matches\n );\n}\n\n/** `\"300ms\"` / `\"0.3s\"` → milliseconds — same parser as `zoomTransition.ts`, duplicated rather than shared: two independent transition subsystems that happen to share a timing format. */\nfunction parseCssTime(value: string): number {\n const first = value.split(',')[0]?.trim() ?? '';\n if (first.endsWith('ms')) return parseFloat(first);\n if (first.endsWith('s')) return parseFloat(first) * 1000;\n return 0;\n}\n\n/** Waits for `transitionend` *or* `animationend` (with a timeout fallback) then calls `cb` once — the custom CSS-class-pair path (§2.5) may use either, since \"no JS needed\" for the host means Shoji can't assume which. */\nfunction waitForEnd(el: HTMLElement, cb: () => void): void {\n const style = getComputedStyle(el);\n const durationMs = Math.max(\n parseCssTime(style.transitionDuration),\n parseCssTime(style.animationDuration),\n );\n let done = false;\n const finish = (): void => {\n if (done) return;\n done = true;\n el.removeEventListener('transitionend', onEnd);\n el.removeEventListener('animationend', onEnd);\n cb();\n };\n const onEnd = (event: Event): void => {\n if (event.target === el) finish();\n };\n el.addEventListener('transitionend', onEnd);\n el.addEventListener('animationend', onEnd);\n setTimeout(finish, durationMs + 100);\n}\n\n/** Incoming slide: instant jump to `(transform, opacity)` (same FLIP trick as `zoomTransition.ts`'s `zoomIn`), then transitions to natural. */\nfunction applyEnter(el: HTMLElement, transform: string, opacity: number): void {\n el.style.transition = 'none';\n el.style.transformOrigin = 'center';\n el.style.transform = transform;\n el.style.opacity = String(opacity);\n void el.offsetHeight; // force the instant jump to commit before transitioning away from it\n el.style.transition =\n 'transform var(--shoji-duration) var(--shoji-easing), opacity var(--shoji-duration) var(--shoji-easing)';\n el.style.transform = 'none';\n el.style.opacity = '1';\n}\n\n/** Outgoing ghost: already sits at natural (a fresh clone has no inline transform yet), so it transitions the other way — natural → `(transform, opacity)`, mirroring `zoomOut`'s shape. */\nfunction applyLeave(el: HTMLElement, transform: string, opacity: number): void {\n el.style.transformOrigin = 'center';\n el.style.transition =\n 'transform var(--shoji-duration) var(--shoji-easing), opacity var(--shoji-duration) var(--shoji-easing)';\n void el.offsetHeight;\n el.style.transform = transform;\n el.style.opacity = String(opacity);\n}\n\nfunction clearInlineTransform(el: HTMLElement): void {\n el.style.transition = '';\n el.style.transform = '';\n el.style.opacity = '';\n el.style.transformOrigin = '';\n}\n\n/**\n * DESIGN.md §2.5 — animates a slide-to-slide navigation via a temporary\n * \"ghost\": a clone of the outgoing `.shoji-slide-media`, positioned over\n * the real slot, playing the preset's `leave` keyframe while `swapContent`\n * (the real `SlideManager.render()` reassignment) runs invisibly underneath\n * it. The newly-swapped-in real active media then plays `enter`. Both\n * animate concurrently.\n *\n * Deliberately separate from the gesture engine's own live-drag/settle\n * animation (`GestureController.ts`, §2.4): `mode` is a discrete \"jump\" for\n * programmatic navigation, not layered onto a continuous drag — a completed\n * swipe always uses its own pool-shift regardless of `mode`. See\n * `Gallery.navigate()`'s `animate` flag.\n */\nexport class SlideTransition {\n constructor(\n private readonly slides: SlideManager,\n private readonly decorateLeave: (clonedMedia: HTMLElement) => () => void = () => () => {},\n ) {}\n\n /** A recognized built-in preset (`TRANSITION_PRESETS`). */\n animate(preset: TransitionPreset, direction: 1 | -1, swapContent: () => void): void {\n this.run(swapContent, (ghost, incoming) => {\n const leave = preset.leave(direction);\n applyLeave(ghost, leave.transform, leave.opacity);\n const enter = preset.enter(direction);\n applyEnter(incoming, enter.transform, enter.opacity);\n });\n }\n\n /** An unrecognized `mode` string: a host-supplied CSS class pair (`shoji-transition-<mode>-leave`/`-enter`), not an error — §2.5's \"custom animation = a CSS class pair, no JS needed.\" */\n animateCustom(mode: string, direction: 1 | -1, swapContent: () => void): void {\n const dirAttr = direction === 1 ? 'next' : 'prev';\n this.run(\n swapContent,\n (ghost, incoming) => {\n ghost.dataset.shojiDirection = dirAttr;\n ghost.classList.add(`shoji-transition-${mode}-leave`);\n incoming.dataset.shojiDirection = dirAttr;\n incoming.classList.add(`shoji-transition-${mode}-enter`);\n },\n (incoming) => {\n incoming.classList.remove(`shoji-transition-${mode}-enter`);\n delete incoming.dataset.shojiDirection;\n },\n );\n }\n\n private run(\n swapContent: () => void,\n kickOff: (ghost: HTMLElement, incoming: HTMLElement) => void,\n cleanupIncoming: (incoming: HTMLElement) => void = clearInlineTransform,\n ): void {\n if (prefersReducedMotion()) {\n swapContent();\n return;\n }\n const outgoing = this.slides.getActiveMedia();\n if (!outgoing) {\n swapContent();\n return;\n }\n\n const ghost = document.createElement('div');\n ghost.className = 'shoji-slide-ghost';\n const clonedMedia = ghost.appendChild(outgoing.cloneNode(true) as HTMLElement);\n this.slides.element.appendChild(ghost);\n // DESIGN.md §2.5/§4.5 — plugins with a per-slide visual override (e.g.\n // RotateFlip's rotate/flip, Zoom's scale/pan) freeze it onto the clone\n // here, before the reflow below commits it as the leave animation's\n // start — the real node was already reset by now, so the clone alone\n // wouldn't otherwise carry it. `settleLeave` (called after `kickOff`\n // below) is what starts each one transitioning back to neutral.\n const settleLeave = this.decorateLeave(clonedMedia);\n void ghost.offsetHeight; // commit the ghost's (and clone's) starting appearance before anything changes it\n\n swapContent();\n\n const incoming = this.slides.getActiveMedia();\n if (!incoming) {\n ghost.remove();\n return;\n }\n\n kickOff(ghost, incoming);\n settleLeave();\n waitForEnd(ghost, () => ghost.remove());\n waitForEnd(incoming, () => cleanupIncoming(incoming));\n }\n}\n","import {\n lockBodyScroll,\n unlockBodyScroll,\n markIntentionalScroll as markIntentionalScrollInternal,\n} from './bodyScrollLock';\nimport { EventBus, type Unsubscribe } from './EventBus';\nimport { buildLightboxDom, type LightboxDom } from './dom';\nimport { FocusTrap } from './FocusTrap';\nimport { GestureController, INTERACTIVE_CONTROL_SELECTOR } from './GestureController';\nimport { LiveRegion } from './LiveRegion';\nimport type { ButtonSpec, PluginContext, ShojiPlugin, VideoProviderRenderer } from './plugin';\nimport { DEFAULT_SELECTOR, resolveDynamicVideoItems, scanContainer } from './scan';\nimport { pauseMedia, SlideManager } from './SlideManager';\nimport type {\n DangerousHtmlCaption,\n GalleryEvents,\n GalleryItem,\n GalleryItemInput,\n GalleryOptions,\n} from './types';\nimport { TRANSITION_PRESETS } from '../transitions/presets';\nimport { SlideTransition } from '../transitions/SlideTransition';\nimport { zoomIn, zoomOut, type Box, type FrozenDragTransform } from './zoomTransition';\n\nfunction itemKey(item: GalleryItem | GalleryItemInput): string {\n return item.id ?? item.src;\n}\n\nfunction resolveElement(target: HTMLElement | string): HTMLElement {\n if (typeof target !== 'string') return target;\n const el = document.querySelector<HTMLElement>(target);\n if (!el) throw new Error(`Shoji: no element matches selector \"${target}\"`);\n return el;\n}\n\n/**\n * DESIGN.md §2.7 — lets code that never held a closure reference to a\n * gallery (a callback resolving long after the call site is gone, a\n * separate `<script>`, devtools) reach the live instance anyway, via\n * `Gallery.getInstance()`/`Gallery.instances()`. Two structures kept in\n * lockstep rather than one: the `WeakMap` gives O(1) lookup-by-element\n * without leaking (an entry is only reachable through an element the host\n * still holds a reference to itself), but WeakMaps aren't iterable, so\n * `instances()` needs the parallel `Set` too. The constructor adds to\n * both; `destroy()` removes from both.\n */\nconst instanceRegistry = new WeakMap<HTMLElement, Gallery>();\nconst allInstances = new Set<Gallery>();\n\n/** CLAUDE.md — all user-visible strings go through `locale`; these are the fallback defaults. */\nconst DEFAULT_LOCALE = {\n close: 'Close',\n previous: 'Previous image',\n next: 'Next image',\n showCaption: 'Show caption',\n hideCaption: 'Hide caption',\n fullCaption: 'Full caption', // DESIGN.md §2.3a\n moreControls: 'More controls', // DESIGN.md §3.1a\n};\n\n/**\n * A click that isn't on the image, a button, a video, or an overlay\n * (counter/caption) counts as \"clicked outside the content.\" Walks\n * `event.composedPath()` (captured at dispatch time) rather than\n * `event.target.closest(...)` — a click handler earlier in the same bubble\n * phase can synchronously replace the clicked element's subtree (e.g. a\n * toolbar button swapping its own icon via `innerHTML` in its click\n * handler), which detaches the original target from the document before\n * this listener (attached higher up, on `.shoji-outer`) ever runs;\n * `.closest()` on a detached node finds no ancestors and would wrongly\n * read as \"clicked outside,\" closing the gallery. `composedPath()` isn't\n * affected by DOM mutations that happen after dispatch.\n *\n * Provider video (YouTube/Vimeo): exclude only the actual iframe/mount\n * element, not the entire `.shoji-slide-provider-video` container. This\n * allows clicks on the blank space around the embed (sides, padding-top\n * toolbar inset) to close the gallery, matching image and HTML5 video\n * behaviour. Clicks that land inside the cross-origin iframe never bubble\n * to the parent at all, so no special handling is needed there; `iframe`\n * covers the edge case where the frame hasn't yet captured focus and a\n * click registers on the element itself from the parent-page perspective.\n * `.shoji-video-mount` is the Vimeo SDK wrapper; YouTube uses a bare\n * `<iframe>` directly inside `.shoji-slide-provider-video`.\n *\n * `caption` — a real bug, reported from real usage: `.shoji-caption--video`\n * (DESIGN.md §2.3a) is `pointer-events: none` so a click on it reaches the\n * video's own native controls underneath, which works fine when the video\n * fills enough of the slide to actually BE underneath it. A letterboxed\n * video (narrower or shorter than the dialog, so the caption's own\n * bottom-left position sits over plain `.shoji-slide-media` background\n * instead) has nothing there to click through *to* — the click fell all\n * the way through to a genuine backdrop click, closing the gallery on what\n * was meant as an interaction with the caption/video area, not \"click\n * outside to close.\" `pointer-events: none` is exactly what removes the\n * caption from `composedPath()` in the first place, so this can't be fixed\n * by adding `.shoji-caption` to the selector above (already there — it\n * already protects a normal, non-click-through caption on a photo slide)\n * — checked by coordinates instead, the one place in this function that\n * has to be.\n */\nfunction isBackdropClick(event: MouseEvent, caption: HTMLElement | null): boolean {\n const hitsProtectedElement = event\n .composedPath()\n .some(\n (node) =>\n node instanceof Element &&\n node.matches(\n `.shoji-slide-img, .shoji-video-mount, iframe, .shoji-counter, ${INTERACTIVE_CONTROL_SELECTOR}`,\n ),\n );\n if (hitsProtectedElement) return false;\n\n if (caption && !caption.hidden && caption.classList.contains('shoji-caption--video')) {\n const rect = caption.getBoundingClientRect();\n const withinCaption =\n event.clientX >= rect.left &&\n event.clientX <= rect.right &&\n event.clientY >= rect.top &&\n event.clientY <= rect.bottom;\n if (withinCaption) return false;\n }\n\n return true;\n}\n\nfunction isDangerousHtmlCaption(value: unknown): value is DangerousHtmlCaption {\n return (\n typeof value === 'object' &&\n value !== null &&\n typeof (value as DangerousHtmlCaption).dangerouslySetInnerHTML === 'string'\n );\n}\n\n/**\n * Core lifecycle (DESIGN.md §2.2), item model / DOM scanning (§2.1), and the\n * lightbox itself: pooled slides (§2.3), dialog semantics/focus trap/live\n * region (§2.6, non-optional), gestures (§2.4, via `GestureController`) and\n * slide-to-slide transitions (§2.5, via `SlideTransition`).\n */\nexport class Gallery {\n /** DESIGN.md §2.7 — the live instance mounted on `el`, or `undefined` if none exists there (or it's since been destroyed). */\n static getInstance(el: HTMLElement): Gallery | undefined {\n return instanceRegistry.get(el);\n }\n\n /** DESIGN.md §2.7 — every live instance, e.g. for page-wide teardown or devtools inspection. */\n static instances(): IterableIterator<Gallery> {\n return allInstances.values();\n }\n\n /** Not `readonly` — `reinit()` (§2.7) reassigns this + every option-derived field below; initializers here just satisfy strict-init. */\n options: GalleryOptions = {};\n readonly element: HTMLElement;\n\n private readonly bus = new EventBus<GalleryEvents>();\n private selector: string = DEFAULT_SELECTOR;\n private isDynamicMode = false;\n private preload = 1;\n private locale: typeof DEFAULT_LOCALE = DEFAULT_LOCALE;\n private showCounter = true;\n private captionVisibleOnVideo = false; // DESIGN.md §2.3a\n // DESIGN.md §2.5 — true from the moment transitionCaption()/open()'s own\n // fade-in starts hiding the caption until the midpoint (or, for open(),\n // the end) where content is swapped and it's revealed again. The async\n // image-load callback in renderCurrentSlide() checks this too, not just\n // the synchronous call in the same function — decode() resolves on a\n // microtask, which can beat the caption's own self-timed reveal for an\n // already-preloaded slide, and updating caption content the instant it\n // fires would show the *new* slide's caption fading, not the outgoing\n // one — same content-swap-timing bug either call site could cause alone.\n private captionFadePending = false;\n private loop = true;\n private closable = true;\n private autoHideDelay: number | false = 5000;\n /** DESIGN.md §3.1a — GalleryOptions.maxPinnedToolbarButtons; see measureToolbarOverflow(). */\n private maxPinnedToolbarButtons = 2;\n private readonly focusTrap = new FocusTrap();\n private readonly liveRegion = new LiveRegion();\n private autoHideTimer: ReturnType<typeof setTimeout> | null = null;\n private autoHidden = false;\n /**\n * Every control `wireControlHover()` has ever wired — the full candidate\n * list `reconcileHover()` (below) checks `:hover` against, kept separate\n * from `hoveringElements` (elements *believed* to be hovered) so\n * reconciliation can also *add* one currently missing from that set, not\n * just remove a stale one.\n */\n private readonly hoverableElements = new Set<HTMLElement>();\n /**\n * The subset of `hoverableElements` the pointer is currently considered\n * to be over — a `Set`, not a plain incrementing counter, so\n * `onActivity()` (below) can reconcile it against the browser's own live\n * `:hover` ground truth on every real pointer movement, self-healing from\n * a `pointerenter`/`pointerleave` pair that never fired at all. Two real\n * bugs, both reported from real usage, both variations on the same\n * browser behavior already leveraged elsewhere in this file (`hidden`\n * doesn't fire `pointerleave` under a stationary cursor, §2.3a): (1) a\n * control that synchronously replaces its own children while the pointer\n * sits stationary over it (Autoplay's own play/pause icon swap) can\n * desync the browser's internal hover-chain tracking badly enough that\n * the *next* real pointer movement away from it never fires `pointerleave`\n * — a plain counter has no way to detect a leave that never reports\n * itself. (2) the mirror image: a control that *appears* under an\n * already-stationary cursor (the common case right after `open()` —\n * whatever the viewer clicked to get here is often right where a\n * toolbar/caption/nav control ends up) never gets a `pointerenter`\n * either, since browsers only recompute hover state on actual pointer\n * movement, not on an element materializing under one already sitting\n * still — so idle auto-hide could fire and hide a control the viewer's\n * cursor is, visibly, still resting directly on top of. Re-deriving \"is\n * this actually hovered\" fresh from the browser on every real pointer\n * move fixes both directions at once, since neither depends on any\n * event having fired correctly in the first place.\n */\n private readonly hoveringElements = new Set<HTMLElement>();\n private isClosing = false;\n /** True while a vertical drag has hidden controls past its own distance threshold (`setControlsHiddenForDrag`) — `onActivity()` defers to it, since the drag's own continuous pointermove stream would otherwise immediately re-reveal what it just hid on every single move. */\n private controlsHiddenByDrag = false;\n private itemList: GalleryItem[] = [];\n private scannedElements: HTMLElement[] = [];\n private activeIndex = 0;\n private opened = false;\n private destroyed = false;\n private slides: SlideManager | null = null;\n private dom: LightboxDom | null = null;\n private gesture: GestureController | null = null;\n private transition: SlideTransition | null = null;\n /** DESIGN.md §2.3a — measures `.shoji-toolbar`'s real rendered height (it can wrap to multiple rows on a narrow viewport with many toolbar buttons registered) so the caption's own height cap can reserve exactly that much space, not a fixed single-row guess. */\n private toolbarHeightObserver: ResizeObserver | null = null;\n private toolbarHeightFrame: number | null = null;\n private captionTruncationFrame: number | null = null;\n /** DESIGN.md §2.3a — a truncated caption's own click/Enter/Space target opens this; also gates `GestureController`'s `isZoomed` (alongside the real zoom gate) so a drag over the open modal can't also navigate/close the lightbox underneath it. */\n private captionModalOpen = false;\n private captionModalReturnFocus: HTMLElement | null = null;\n /**\n * DESIGN.md §2.3a — every real path into `openCaptionModal()` starts from\n * a click *or* a keydown on the caption, so `captionModalReturnFocus`\n * above is always the caption itself either way; this instead\n * distinguishes *how* it got there, so `closeCaptionModal()` only\n * actually calls `.focus()` for the keyboard path (a real Tab+Enter user,\n * where restoring focus continues their tab sequence correctly) and\n * leaves it alone for a mouse click (where the resulting focus was\n * purely incidental — nothing about clicking to read a caption means the\n * *next* keypress, e.g. a plugin's own Space shortcut, should still\n * silently target it).\n */\n private captionModalOpenedViaKeyboard = false;\n /**\n * DESIGN.md §3.1a — every `ctx.ui.toolbar()`-registered button, in\n * registration order, alongside the slot it was registered into (so a\n * collapsed one can be restored to the right place, not just anywhere).\n * Registration order is what decides overflow priority: up to\n * `maxPinnedToolbarButtons` stay pinned, the rest collapse into the\n * popover before them, latest-registered first — see\n * `measureToolbarOverflow()`.\n */\n private readonly pluginToolbarButtons: Array<{\n el: HTMLElement;\n slot: 'left' | 'center' | 'right';\n }> = [];\n private toolbarOverflowOpen = false;\n private toolbarOverflowReturnFocus: HTMLElement | null = null;\n private readonly shortcuts = new Map<string, (e: KeyboardEvent) => void>();\n private readonly pluginStorage = new Map<string, unknown>();\n /** Backs `getActivePlugins()`. */\n private readonly activePluginNames = new Set<string>();\n private readonly videoProviders = new Map<string, VideoProviderRenderer>();\n private zoomGate: (() => boolean) | null = null;\n /** DESIGN.md §2.6a/§4.6 — the zoomed `<img>`'s own real on-screen rect, read by `beginClose()` before `beforeClose` fires (see `registerZoomStartProvider()`), so a button-close continues the zoom-out from wherever the viewer was actually zoomed/panned to instead of snapping back to neutral first. */\n private zoomStartProvider: (() => Box | null) | null = null;\n /** DESIGN.md §2.5/§4.5 — plugins with a per-slide visual override (RotateFlip's rotate/flip, Zoom's scale/pan) that reset unanimated on `beforeSlide` (must clear before `SlideManager.render()` reparents the outgoing node). Multi-slot: more than one can be active on the same slide, each targeting a different part of the clone, so they don't conflict. See `registerSlideLeaveDecorator()`. */\n private readonly slideLeaveDecorators = new Set<\n (clonedMedia: HTMLElement) => (() => void) | void\n >();\n private pluginCleanups: Array<() => void> = [];\n\n private readonly onContainerClick = (event: MouseEvent): void => {\n let node = event.target as Node | null;\n while (node && node !== this.element) {\n if (node instanceof HTMLElement) {\n const index = this.scannedElements.indexOf(node);\n if (index !== -1) {\n event.preventDefault();\n this.open(index);\n return;\n }\n }\n node = node.parentNode;\n }\n };\n\n private readonly onOuterClick = (event: MouseEvent): void => {\n if (this.closable && isBackdropClick(event, this.dom?.caption ?? null)) this.close();\n };\n\n /**\n * DESIGN.md §2.8 — any interaction re-shows controls, restarts the idle\n * clock. `autoHideDelay: 0` = \"never show controls\" — a no-op here.\n * `autoHideDelay: false` = \"always visible\" — also a no-op: nothing to\n * reveal (already shown) or reschedule (no timer ever runs). Also a no-op\n * once `isClosing` — a real bug, reported from real usage: moving the\n * mouse during close()'s own controls-fade-then-zoom-out sequence (§2.6a)\n * re-showed the just-hidden controls mid-animation, since these listeners\n * stay wired for the whole close sequence. Same reasoning for\n * `controlsHiddenByDrag` (§2.4/§2.8): a vertical drag's own `pointermove`\n * stream would otherwise re-reveal what it just hid, every single frame.\n */\n private readonly onActivity = (): void => {\n this.reconcileHover();\n if (\n this.autoHideDelay === 0 ||\n this.autoHideDelay === false ||\n this.isClosing ||\n this.controlsHiddenByDrag ||\n // DESIGN.md §2.3a — requested directly: mouse movement/interaction\n // over the open caption modal shouldn't re-reveal controls already\n // hidden behind it — `dom.outer`'s own pointermove/pointerdown/etc.\n // listeners (below) still see it bubble through, same as any other\n // activity, so without this an idle-hidden toolbar would pop back\n // the instant the viewer so much as moved the mouse to read.\n this.captionModalOpen\n )\n return;\n this.showControls();\n this.scheduleAutoHide();\n };\n\n /** x */\n private controlsHiddenAtGestureStart = false;\n private readonly captureGestureStartState = (): void => {\n this.controlsHiddenAtGestureStart = this.autoHidden;\n };\n\n private readonly onKeydown = (event: KeyboardEvent): void => {\n // A focused <video>'s native controls own arrow/space/home/end for\n // seeking, volume, and play/pause — don't let slide navigation hijack\n // those while the viewer is actually interacting with the video. Escape\n // still closes the gallery regardless.\n if (document.activeElement instanceof HTMLVideoElement && event.key !== 'Escape') return;\n\n this.onActivity();\n switch (event.key) {\n case 'Escape':\n if (this.closable) this.close();\n break;\n case 'ArrowLeft':\n case 'a':\n case 'A':\n this.prev();\n break;\n case 'ArrowRight':\n case 'd':\n case 'D':\n this.next();\n break;\n case 'Home':\n this.goTo(0);\n break;\n case 'End':\n this.goTo(this.itemList.length - 1);\n break;\n default: {\n // DESIGN.md §3 — ctx.ui.registerShortcut(); unhandled keys still\n // fall through with no preventDefault, same as before plugins existed.\n const shortcut = this.shortcuts.get(event.key);\n if (!shortcut) return;\n shortcut(event);\n }\n }\n event.preventDefault();\n };\n\n /**\n * DESIGN.md §2.3a — capture phase, added only while the caption modal is\n * open (not from `open()` onward like `onKeydown`, which is bubble\n * phase): capture always finishes before bubble starts, so\n * `stopPropagation()` here reliably beats `onKeydown`'s own bubble-phase\n * handling regardless of where in the dialog focus happens to be. A real\n * bug, reported from real usage: an earlier version of this only special-\n * cased `Escape`, leaving every other key (`Space` — Autoplay's own\n * play/pause shortcut if that plugin's loaded, arrow keys, `w`/`s` for\n * Zoom, any plugin-registered shortcut) to fall straight through to\n * `onKeydown` while the modal sat open on screen. A modal dialog should\n * make the background fully inert to keyboard input while it's open, not\n * just for one key — so this now stops propagation for *every* key\n * unconditionally, closing the modal as the one piece of extra behavior\n * layered on top for `Escape` specifically. Deliberately no\n * `preventDefault()` here — the modal's own contents (the close button,\n * a scrollable panel) keep their normal native key behavior (Space\n * activating a focused button, arrow/Space scrolling), only the\n * *background* gallery is what this isolates it from.\n */\n private readonly onCaptionModalKeydown = (event: KeyboardEvent): void => {\n event.stopPropagation();\n if (event.key === 'Escape') this.closeCaptionModal();\n };\n\n constructor(target: HTMLElement | string, options: GalleryOptions = {}) {\n this.element = resolveElement(target);\n instanceRegistry.set(this.element, this);\n allInstances.add(this);\n this.applyOptions(options);\n }\n\n /** Everything the constructor does after `this.element` is resolved — shared with `reinit()` (§2.7). */\n private applyOptions(options: GalleryOptions): void {\n this.options = options;\n this.selector = options.selector ?? DEFAULT_SELECTOR;\n this.isDynamicMode = options.items !== undefined;\n this.preload = options.preload ?? 1;\n this.locale = { ...DEFAULT_LOCALE, ...options.locale };\n this.showCounter = options.counter ?? true;\n this.loop = options.loop ?? true;\n this.closable = options.closable ?? true;\n this.autoHideDelay = options.autoHideDelay ?? 5000;\n this.maxPinnedToolbarButtons = options.maxPinnedToolbarButtons ?? 2;\n // Reset explicitly — a reinit() call must not inherit these from before.\n this.activeIndex = 0;\n this.scannedElements = [];\n\n if (this.isDynamicMode) {\n this.itemList = resolveDynamicVideoItems(options.items ?? []);\n } else {\n const scanned = scanContainer(this.element, this.selector);\n this.itemList = scanned.map((s) => s.item);\n this.scannedElements = scanned.map((s) => s.element);\n this.element.addEventListener('click', this.onContainerClick);\n }\n\n // DESIGN.md §3.1 — plugins init here, at construction, not lazily on\n // first open(): a layout plugin needs to render the *inline* container\n // (this.element, always available) before open() is ever called — it's\n // what the viewer clicks to open in the first place. ensureLightbox()'s\n // `if (this.dom) return;` guard makes the later, harmless no-op call\n // from open() safe; building the (hidden, display:none until opened)\n // lightbox DOM this early costs one inert subtree per instance, which is\n // negligible next to the thumbnail images already loading regardless.\n this.ensureLightbox();\n // Set after ensureLightbox() (not inside it) so a reinit() with a\n // different `closable` value updates the already-built button too —\n // ensureLightbox() only builds the DOM once (`if (this.dom) return;`).\n this.dom!.closeButton.hidden = !this.closable;\n\n // Default is closed — the host clicks a thumbnail or calls open()\n // itself. openOnInit is the explicit, opt-in exception (deep-link-style\n // integrations); guarded on having at least one item so an empty\n // gallery can't \"open\" onto nothing.\n if (options.openOnInit && this.itemList.length > 0) {\n this.open(options.index ?? 0);\n }\n }\n\n get items(): readonly GalleryItem[] {\n return this.itemList;\n }\n\n get currentIndex(): number {\n return this.activeIndex;\n }\n\n /** True from the first `open()` until `close()`/`destroy()`. */\n get isOpen(): boolean {\n return this.opened;\n }\n\n /** True once `destroy()` has run. */\n get isDestroyed(): boolean {\n return this.destroyed;\n }\n\n /** Whether auto-hide (§2.8) currently has controls faded. */\n get controlsHidden(): boolean {\n return this.autoHidden;\n }\n\n /** Names of plugins that actually initialized. */\n getActivePlugins(): string[] {\n return [...this.activePluginNames];\n }\n\n /** The active slide's `.shoji-slide-media` container. Empty before the first `open()`; `null` only after `destroy()`. */\n getActiveMedia(): HTMLElement | null {\n return this.slides?.getActiveMedia() ?? null;\n }\n\n /** DESIGN.md §4-zoom — suspends drag-to-navigate/close while zoomed. Single slot, not a multi-subscriber event. */\n registerZoomGate(isZoomed: () => boolean): () => void {\n this.zoomGate = isZoomed;\n return () => {\n if (this.zoomGate === isZoomed) this.zoomGate = null;\n };\n }\n\n /** DESIGN.md §2.6a/§4.6 — returns the zoomed image's current on-screen rect (`getBoundingClientRect()`), or `null` when not zoomed. Single slot, same pattern as `registerZoomGate` above. */\n registerZoomStartProvider(provider: () => Box | null): () => void {\n this.zoomStartProvider = provider;\n return () => {\n if (this.zoomStartProvider === provider) this.zoomStartProvider = null;\n };\n }\n\n /**\n * DESIGN.md §2.5/§4.5 — called once per navigation with the leave-\n * ghost's clone, right after `SlideTransition` creates it. Freeze\n * whatever per-slide visual state is about to be reset onto the clone\n * (or a descendant, e.g. Zoom's own `<img>`) and return a `() => void`\n * to trigger the transition back to neutral once committed — or return\n * nothing if there's nothing to animate away this time.\n */\n registerSlideLeaveDecorator(\n decorator: (clonedMedia: HTMLElement) => (() => void) | void,\n ): () => void {\n this.slideLeaveDecorators.add(decorator);\n return () => {\n this.slideLeaveDecorators.delete(decorator);\n };\n }\n\n on<K extends keyof GalleryEvents>(event: K, fn: (detail: GalleryEvents[K]) => void): Unsubscribe {\n return this.bus.on(event, fn);\n }\n\n private clampToRange(index: number): number {\n return Math.min(Math.max(index, 0), Math.max(this.itemList.length - 1, 0));\n }\n\n private ensureLightbox(): void {\n if (this.dom) return;\n this.slides = new SlideManager({\n preload: this.preload,\n videoProviders: this.videoProviders,\n });\n this.transition = new SlideTransition(this.slides, (clonedMedia) => {\n const settlers: Array<() => void> = [];\n for (const decorate of this.slideLeaveDecorators) {\n const settle = decorate(clonedMedia);\n if (settle) settlers.push(settle);\n }\n return () => {\n for (const settle of settlers) settle();\n };\n });\n const dom = buildLightboxDom(this.slides.element, this.locale);\n this.dom = dom;\n if (this.options.backdropOpacity != null) {\n const clamped = Math.min(Math.max(this.options.backdropOpacity, 0), 1);\n dom.outer.style.setProperty('--shoji-backdrop-opacity', String(clamped));\n }\n if (this.options.transitionDuration != null) {\n const clamped = Math.max(this.options.transitionDuration, 0);\n dom.outer.style.setProperty('--shoji-duration', `${clamped}ms`);\n }\n // DESIGN.md §2.8 — autoHideDelay: 0 means Shoji's own controls stay\n // permanently invisible (a host building fully custom chrome), not that\n // the whole gallery should behave like nothing is there — the mouse\n // cursor itself should still behave normally, not force-hidden along\n // with controls that were never meant to be seen in the first place.\n // Requested directly; `.shoji-cursor-visible` overrides the\n // `.shoji-controls-hidden` cursor:none rule via higher specificity.\n if (this.autoHideDelay === 0) dom.dialog.classList.add('shoji-cursor-visible');\n dom.outer.appendChild(this.liveRegion.element);\n dom.closeButton.addEventListener('click', () => this.close());\n dom.prevButton.addEventListener('click', () => this.prev());\n dom.nextButton.addEventListener('click', () => this.next());\n dom.captionToggleButton.addEventListener('click', () => {\n this.captionVisibleOnVideo = !this.captionVisibleOnVideo;\n this.updateCaptionVisibility();\n });\n dom.caption.addEventListener('click', this.onCaptionActivate);\n dom.caption.addEventListener('keydown', this.onCaptionActivate);\n dom.captionModalCloseButton.addEventListener('click', () => this.closeCaptionModal());\n // Closes on a genuine backdrop click (the modal element itself, not a\n // descendant) — always stops propagation regardless, since without it\n // *any* click anywhere inside the modal (the panel, its content, even\n // the close button) would keep bubbling up to onOuterClick below and\n // get misread as \"clicked outside the real content,\" closing the whole\n // lightbox out from under it.\n dom.captionModal.addEventListener('click', (event) => {\n event.stopPropagation();\n if (event.target === dom.captionModal) this.closeCaptionModal();\n });\n dom.toolbarOverflowButton.addEventListener('click', (event) => {\n event.stopPropagation();\n this.toggleToolbarOverflow();\n });\n // Closes on any click outside the panel and off the caret itself —\n // registered ahead of onOuterClick below (same-node listeners run in\n // registration order) so it can stop the click there too, the same\n // reasoning the caption modal's own backdrop-click listener above uses:\n // without it, a click that closes the popover would also keep bubbling\n // and get misread by onOuterClick as \"clicked outside the real\n // content,\" closing the whole lightbox in the same click.\n dom.outer.addEventListener('click', (event) => {\n if (!this.toolbarOverflowOpen) return;\n // event.composedPath(), not event.target — a real bug, found testing\n // this against Autoplay's own play/pause button (icon-swap-on-click,\n // same class of button CLAUDE.md/DESIGN.md §2.8 already documents):\n // if the click's own handler replaces the button's innerHTML\n // synchronously (as that pattern does), `event.target` can already be\n // a now-detached node by the time this bubble-phase listener runs —\n // `.contains(target)` then reads false even though the click\n // genuinely originated inside the panel, and the popover (wrongly)\n // closes out from under the very button that was just clicked.\n // composedPath() is captured at dispatch time, before any such\n // mutation, so it stays accurate regardless.\n const insidePanel = event\n .composedPath()\n .some((node) => node === dom.toolbarOverflowPanel || node === dom.toolbarOverflowButton);\n if (insidePanel) return;\n event.stopPropagation();\n this.closeToolbarOverflow();\n });\n dom.outer.addEventListener('click', this.onOuterClick);\n dom.outer.addEventListener('pointermove', this.onActivity, { passive: true });\n // Registered before onActivity's own pointerdown listener — order matters, see captureGestureStartState's doc comment.\n dom.outer.addEventListener('pointerdown', this.captureGestureStartState, { passive: true });\n dom.outer.addEventListener('pointerdown', this.onActivity, { passive: true });\n dom.outer.addEventListener('touchstart', this.onActivity, { passive: true });\n dom.outer.addEventListener('wheel', this.onActivity, { passive: true });\n dom.outer.addEventListener('focusin', this.onActivity);\n dom.outer.addEventListener('focusout', () => this.scheduleAutoHide());\n\n // Hovering pauses auto-hide (pointermove alone wouldn't catch a still\n // mouse) — every part of the overlay, not just individual buttons\n // (toolbarLeft/Center/Right cover the bar's own padding/gaps too, not\n // just the buttons already nested inside them). Plugin toolbar\n // buttons/overlays get this wiring at their own call sites.\n for (const el of [\n dom.closeButton,\n dom.prevButton,\n dom.nextButton,\n dom.captionToggleButton,\n dom.caption,\n dom.captionModalPanel,\n dom.counter,\n dom.toolbarLeft,\n dom.toolbarCenter,\n dom.toolbarRight,\n ]) {\n this.wireControlHover(el);\n }\n\n document.body.appendChild(dom.outer);\n\n // DESIGN.md §2.3a — a fixed single-row assumption undercounted a busy\n // toolbar (many plugins registering buttons) wrapping to multiple rows\n // on a narrow viewport, letting the caption's own height cap grow up\n // over the real controls instead of stopping below them. Measuring the\n // real rendered height instead of guessing is correct regardless of\n // plugin count or viewport width. rAF-batched, same pattern the Layout\n // plugin's own resize handling already uses (CLAUDE.md: batch DOM\n // writes that can thrash layout).\n this.toolbarHeightObserver = new ResizeObserver(() => this.scheduleToolbarOverflowMeasure());\n this.toolbarHeightObserver.observe(dom.toolbar);\n\n // DESIGN.md §2.4 — attached to the whole dialog (not just .shoji-slides)\n // so vertical swipe-to-close works from anywhere in it, not only over\n // the image; shouldIgnoreGesture() excludes real controls so their\n // native click/touch behavior is unaffected.\n this.gesture = new GestureController(\n {\n dialog: dom.dialog,\n caption: dom.caption,\n slides: this.slides,\n canGoNext: () => this.loop || this.activeIndex < this.itemList.length - 1,\n canGoPrev: () => this.loop || this.activeIndex > 0,\n // animate:false — a completed swipe already played its own\n // live-drag/settle animation (§2.4); running the §2.5 `mode`\n // transition on top would double-animate. See navigate()'s doc.\n next: () => this.navigate(this.nextIndex(), 1, false),\n prev: () => this.navigate(this.prevIndex(), -1, false),\n close: () => this.close(),\n closeFromSwipe: (frozenDrag) => this.closeFromSwipe(frozenDrag),\n setControlsHiddenForDrag: (hidden) => this.setControlsHiddenForDrag(hidden),\n canClose: () => this.closable,\n onActivity: () => this.onActivity(),\n // DESIGN.md §2.3a — also suspends drag-to-navigate/close while the\n // caption modal is open, same mechanism the Zoom plugin's own gate\n // already uses, so a drag starting over the modal can't also\n // navigate/close the lightbox underneath it.\n isZoomed: () =>\n (this.zoomGate?.() ?? false) || this.captionModalOpen || this.toolbarOverflowOpen,\n },\n {\n onTap: (x, y) =>\n this.bus.emit('tap', {\n x,\n y,\n controlsWereHidden: this.controlsHiddenAtGestureStart,\n }),\n onDoubleTap: (x, y) => this.bus.emit('doubleTap', { x, y }),\n onPinchStart: (centerX, centerY) => this.bus.emit('pinchStart', { centerX, centerY }),\n onPinchMove: (scale, centerX, centerY) =>\n this.bus.emit('pinchMove', { scale, centerX, centerY }),\n onPinchEnd: () => this.bus.emit('pinchEnd', {}),\n onWheelZoom: (deltaScale, x, y) => this.bus.emit('wheelZoom', { deltaScale, x, y }),\n },\n this.options.gestures,\n );\n\n this.initPlugins();\n }\n\n /**\n * DESIGN.md §3 — which of `declared`'s plugins will actually load,\n * resolved as a fixed point rather than a single pass: start with every\n * *structurally* valid entry (a real object with an `init` function),\n * then repeatedly drop any whose `requires` points at a name no longer\n * in the set, until a full pass drops nothing. That repetition is what\n * makes a chain cascade correctly — if a plugin gets dropped because\n * *its* own requirement failed, anything requiring that plugin drops on\n * the very next pass, the same as if the original problem were its own.\n * A genuine mutual requirement (A needs B, B needs A, both otherwise\n * fine) never gets caught in this — each still finds the other present\n * whenever it's checked, so both stay valid. That's correct, not a\n * missed case: this only ever decides *whether* something loads, never\n * *when* — nothing here reorders execution, so two plugins depending on\n * each other creates no actual ordering conflict to detect in the first\n * place.\n */\n private resolveValidPluginNames(declared: readonly ShojiPlugin[]): Set<string> {\n const validNames = new Set<string>();\n for (const plugin of declared) {\n if (plugin && typeof plugin.init === 'function') validNames.add(plugin.name);\n }\n let changed = true;\n while (changed) {\n changed = false;\n for (const plugin of declared) {\n if (!plugin || typeof plugin.init !== 'function' || !validNames.has(plugin.name)) continue;\n const unmet = (plugin.requires ?? []).some((name) => !validNames.has(name));\n if (unmet) {\n validNames.delete(plugin.name);\n changed = true;\n }\n }\n }\n return validNames;\n }\n\n /**\n * DESIGN.md §3 — plugins init here, not the constructor. `requires` is\n * resolved against the *whole* declared `plugins` list up front, not\n * registration order — a real friction point, reported directly: a host\n * with a `requires` chain had to carefully order the array by hand, with\n * no actual reason to (nothing about *execution* order needs to match\n * declaration order for a name-presence check). `resolveValidPluginNames()`\n * decides who's actually going to load, independent of position; this\n * loop still runs — and every `ctx.ui.toolbar()` registration still\n * lands — in exactly the array's own order regardless, so toolbar/\n * collapse-priority order (also array-order-driven, DESIGN.md §3.1a)\n * is completely unaffected by this. Cleared up front so a `reinit()`\n * doesn't inherit names from before.\n */\n private initPlugins(): void {\n this.activePluginNames.clear();\n this.videoProviders.clear();\n const declared = this.options.plugins ?? [];\n const validNames = this.resolveValidPluginNames(declared);\n\n for (const plugin of declared) {\n // Guards a real, easy-to-hit host mistake: `plugins: [Shoji.SomePlugin]`\n // silently becomes `plugins: [undefined]` if the referenced static\n // (e.g. Shoji.Autoplay) doesn't actually exist yet — a stale dist\n // build predating that plugin's addition is the most common cause.\n // One bad entry shouldn't crash plugin init for every entry after it.\n if (!plugin || typeof plugin.init !== 'function') {\n console.error(\n 'Shoji: skipping an invalid plugins[] entry (not a ShojiPlugin object):',\n plugin,\n );\n continue;\n }\n if (!validNames.has(plugin.name)) {\n const missing = (plugin.requires ?? []).filter((name) => !validNames.has(name));\n console.error(\n `Shoji: plugin \"${plugin.name}\" requires [${missing.join(', ')}], which failed to load (missing, invalid, or itself had an unmet requires) — skipping.`,\n );\n continue;\n }\n\n const pluginOptions: Record<string, unknown> = {\n ...plugin.defaults,\n ...(this.options[plugin.name] as Record<string, unknown> | undefined),\n };\n const ctx: PluginContext = {\n gallery: this,\n options: pluginOptions,\n on: this.on.bind(this),\n emit: (event, detail) => this.bus.emit(event, detail),\n ui: {\n toolbar: (slot, el) => this.pluginToolbar(slot, el),\n overlay: (el, layer) => this.pluginOverlay(el, layer),\n outer: () => this.dom!.outer,\n registerShortcut: (key, fn) => {\n this.shortcuts.set(key, fn);\n return () => this.shortcuts.delete(key);\n },\n registerVideoProvider: (name, render) => {\n this.videoProviders.set(name, render);\n return () => {\n if (this.videoProviders.get(name) === render) this.videoProviders.delete(name);\n };\n },\n },\n storage: {\n get: (key) => this.pluginStorage.get(key),\n set: (key, value) => {\n this.pluginStorage.set(key, value);\n },\n },\n };\n\n const cleanup = plugin.init(ctx);\n if (typeof cleanup === 'function') this.pluginCleanups.push(cleanup);\n this.activePluginNames.add(plugin.name);\n }\n }\n\n /**\n * DESIGN.md §3.1 — 'right' inserts immediately before the overflow caret\n * (DESIGN.md §3.1a — never after, and never after close either), so\n * close stays fixed absolute-rightmost, the caret sits just to its left,\n * and plugins cluster further left still, in registration order:\n * `plugins: [A, B, C]` reads A, B, C, caret, close. 'left'/'center' are\n * independent zones for a plugin that doesn't want to sit next to close.\n */\n private pluginToolbar(\n slot: 'left' | 'center' | 'right',\n el: HTMLElement | ButtonSpec,\n ): Unsubscribe {\n const dom = this.dom!;\n // buildToolbarButton() below already wires hover for ButtonSpec; a raw\n // element needs it here instead, once, not double-wired either way.\n const isRawElement = el instanceof HTMLElement;\n const node = isRawElement ? el : this.buildToolbarButton(el);\n const unhover = isRawElement ? this.wireControlHover(node) : null;\n if (slot === 'right') {\n dom.toolbarRight.insertBefore(node, dom.toolbarOverflowButton);\n } else {\n (slot === 'left' ? dom.toolbarLeft : dom.toolbarCenter).appendChild(node);\n }\n this.pluginToolbarButtons.push({ el: node, slot });\n this.scheduleToolbarOverflowMeasure();\n return () => {\n unhover?.();\n node.remove();\n const index = this.pluginToolbarButtons.findIndex((b) => b.el === node);\n if (index !== -1) this.pluginToolbarButtons.splice(index, 1);\n this.scheduleToolbarOverflowMeasure();\n };\n }\n\n private buildToolbarButton(spec: ButtonSpec): HTMLButtonElement {\n const button = document.createElement('button');\n button.type = 'button';\n button.className = 'shoji-toolbar-button';\n if (spec.icon) {\n button.ariaLabel = button.title = spec.label;\n button.innerHTML = spec.icon;\n } else {\n button.textContent = spec.label;\n }\n button.addEventListener('click', spec.onClick);\n this.wireControlHover(button);\n return button;\n }\n\n /**\n * DESIGN.md §3.1a — coalesces both toolbar-overflow measurement and the\n * pre-existing `--shoji-toolbar-height` update into one rAF-batched pass\n * (CLAUDE.md: batch DOM writes that can thrash layout), triggered by the\n * toolbar's own `ResizeObserver` (width *or* height changes — a busy\n * toolbar wrapping counts as both) and by `pluginToolbar()` registering\n * or unregistering a button. Order matters: overflow collapse has to run\n * *before* the height read, so the height custom property reflects the\n * settled (ideally single-row, post-collapse) toolbar, not a momentarily\n * wrapped one the caption's own height cap would otherwise over-reserve\n * space for.\n */\n private scheduleToolbarOverflowMeasure(): void {\n if (this.toolbarHeightFrame !== null) return;\n this.toolbarHeightFrame = requestAnimationFrame(() => {\n this.toolbarHeightFrame = null;\n if (!this.dom) return;\n this.measureToolbarOverflow();\n this.dom.dialog.style.setProperty(\n '--shoji-toolbar-height',\n `${this.dom.toolbar.getBoundingClientRect().height}px`,\n );\n });\n }\n\n /**\n * DESIGN.md §3.1a — collapses plugin toolbar buttons into\n * `toolbarOverflowPanel` once they don't fit in one row, instead of\n * letting the toolbar wrap to a second/third one. Always restores every\n * plugin button to its own original slot first and recomputes from that\n * clean slate, rather than incrementally adjusting whatever the previous\n * pass left behind — the *set* of buttons and the viewport width can\n * both have changed since then, and a fresh, deterministic pass is both\n * simpler and self-correcting than trying to reason about a delta.\n *\n * \"Fits in one row\" is measured, not assumed: each slot's own rendered\n * height is compared against `closeButton`'s (always exactly one row\n * tall, always present) — a slot has wrapped if it's taller than that,\n * regardless of *which* slot a plugin happened to register into (the\n * three slots wrap independently, DESIGN.md §3.1a's own CSS notes).\n *\n * Collapses latest-registered first (DESIGN.md §3's own registration-\n * order-is-priority convention — `plugins: [A, B, C]` keeps A pinned\n * before B/C). `maxPinnedToolbarButtons` (default 2, see\n * `GalleryOptions.maxPinnedToolbarButtons`) is a ceiling, not a\n * guarantee: collapsing always goes down to at most that many pinned,\n * but keeps going *below* it — down to zero if it must — whenever even\n * that many still leaves a slot wrapped. `closeButton` and the counter\n * (`toolbarLeft`) must never wrap onto their own line, and `fitsOneRow()`\n * already checks every slot's height, not just `toolbarRight`'s, so a\n * wide counter or other left-slot content competing for the same row\n * pushes the pinned count down too, not just the right slot's own\n * button count. `closeButton`/`toolbarOverflowButton` themselves are\n * never candidates for collapse — everything else relocates into the\n * popover, which renders directly below that row.\n */\n private measureToolbarOverflow(): void {\n if (!this.dom) return;\n const dom = this.dom;\n for (const { el, slot } of this.pluginToolbarButtons) {\n if (slot === 'right') dom.toolbarRight.insertBefore(el, dom.toolbarOverflowButton);\n else (slot === 'left' ? dom.toolbarLeft : dom.toolbarCenter).appendChild(el);\n }\n dom.toolbarOverflowPanel.replaceChildren();\n dom.toolbarOverflowButton.hidden = true;\n if (this.toolbarOverflowOpen) this.closeToolbarOverflow();\n\n const rowHeight = dom.closeButton.getBoundingClientRect().height;\n const fitsOneRow = (): boolean =>\n [dom.toolbarLeft, dom.toolbarCenter, dom.toolbarRight].every(\n (slotEl) => slotEl.getBoundingClientRect().height <= rowHeight + 1,\n );\n\n if (fitsOneRow()) return;\n\n const pinnedCeiling = Math.min(this.maxPinnedToolbarButtons, this.pluginToolbarButtons.length);\n dom.toolbarOverflowButton.hidden = false;\n for (let i = this.pluginToolbarButtons.length - 1; i >= 0; i--) {\n // Below the ceiling, stop as soon as it fits; at/above it, keep\n // collapsing regardless of fit — the ceiling is enforced first, then\n // fit is what decides whether to go further still.\n if (i < pinnedCeiling && fitsOneRow()) break;\n // Collapsed latest-registered first (the decision above), but\n // inserted at the *front* of the panel each time — the panel should\n // read in the same left-to-right registration order the toolbar row\n // itself would have shown, not reversed just because that's the order\n // they were evaluated in.\n dom.toolbarOverflowPanel.insertBefore(\n this.pluginToolbarButtons[i]!.el,\n dom.toolbarOverflowPanel.firstChild,\n );\n }\n }\n\n private readonly onToolbarOverflowKeydown = (event: KeyboardEvent): void => {\n event.stopPropagation();\n if (event.key === 'Escape') this.closeToolbarOverflow();\n };\n\n private toggleToolbarOverflow(): void {\n if (this.toolbarOverflowOpen) this.closeToolbarOverflow();\n else this.openToolbarOverflow();\n }\n\n private openToolbarOverflow(): void {\n if (!this.dom || this.toolbarOverflowOpen) return;\n this.toolbarOverflowReturnFocus = document.activeElement as HTMLElement | null;\n this.dom.toolbarOverflowPanel.hidden = false;\n this.positionToolbarOverflowPanel();\n this.dom.toolbarOverflowButton.setAttribute('aria-expanded', 'true');\n this.toolbarOverflowOpen = true;\n this.focusTrap.retarget(this.dom.toolbarOverflowPanel);\n document.addEventListener('keydown', this.onToolbarOverflowKeydown, true);\n }\n\n /**\n * DESIGN.md §3.1a — the panel's `right` offset is set here, per open,\n * rather than as a fixed CSS value: the caret's own x-position isn't\n * fixed, it shifts with how many buttons are pinned ahead of it\n * (`maxPinnedToolbarButtons` is host-configurable, and can itself be\n * reduced further at measure time, DESIGN.md §3.1a), so a static\n * `right: var(--shoji-spacing-md)` only happened to line up with the\n * caret at one particular pinned-button count and viewport width. It\n * otherwise anchored to the toolbar/dialog's own right edge — under\n * `closeButton`, which sits to the right of the caret, not under the\n * caret that actually opens it.\n *\n * Aligns the panel's own *content* edge (inside its padding), not just\n * its border box, with the caret's right edge — the panel's grid pitch\n * (44px columns, `--shoji-spacing-sm` gaps, `.shoji-toolbar-overflow-\n * panel` in shoji.css) already matches the toolbar row's own button\n * size/gap, so subtracting the panel's own right padding here is what\n * makes the popover's icon columns actually line up with the toolbar\n * row's icons above them, not just the panel block sitting roughly\n * nearby.\n *\n * Also sets the grid's own column count to match the toolbar row's own\n * column count right now: however many buttons are *actually* still\n * pinned on `toolbarRight`, *plus the caret itself* — requested directly:\n * the popover should read as the same row of columns continuing\n * downward, caret included, not a fixed 3 columns regardless of how many\n * ended up pinned. At the default `maxPinnedToolbarButtons` (2), that's 2\n * pinned + the caret = 3 columns. Collapsed buttons beyond that count\n * still wrap onto further rows within it, same as before.\n *\n * **A real bug: \"pinned\" is counted from `this.pluginToolbarButtons`\n * (registered via `ctx.ui.toolbar()`), never by querying `toolbarRight`'s\n * DOM children.** The old filter (`toolbarRight.children`, excluding\n * `closeButton`/`toolbarOverflowButton` by identity, `!el.hidden`) broke\n * once a host appended an unrelated element straight into `toolbarRight`\n * — not a documented extension point, but nothing stops it. Reported from\n * real usage: a plugin's own loading spinner, toggled via `style.display`\n * rather than the `hidden` attribute, passed `!el.hidden` and got\n * miscounted as a pinned button, turning a 3-column popover into 4.\n * Counting from the registry instead is immune to this regardless of how\n * such an element manages its own visibility. `closeButton`/\n * `toolbarOverflowButton` were never in `pluginToolbarButtons`, so\n * excluding them by identity is no longer needed either.\n */\n private positionToolbarOverflowPanel(): void {\n if (!this.dom) return;\n const {\n dialog,\n toolbarOverflowButton,\n toolbarOverflowPanel,\n toolbarRight,\n captionToggleButton,\n } = this.dom;\n const dialogRight = dialog.getBoundingClientRect().right;\n const caretRight = toolbarOverflowButton.getBoundingClientRect().right;\n const paddingRight = parseFloat(getComputedStyle(toolbarOverflowPanel).paddingRight) || 0;\n const right = dialogRight - caretRight - paddingRight;\n toolbarOverflowPanel.style.right = `${Math.max(0, right)}px`;\n\n // A second real bug, reported from real usage on a video slide: still\n // over/undercounts, even immune to the DOM-parentage issue above.\n // Undercounts by not knowing about `captionToggleButton` at all — a\n // real, space-consuming, never-collapsible button that only exists on\n // video slides, added directly in dom.ts rather than through\n // `ctx.ui.toolbar()`, so it was never in `pluginToolbarButtons` to\n // begin with. Overcounts separately, in the other direction: a\n // registered button hidden for its own content reasons (e.g. Zoom's\n // zoomIn/zoomOut/actualSize buttons hiding themselves on video slides,\n // §4.6) still passes the parentage check — hidden means zero layout\n // size, so `measureToolbarOverflow()`'s collapse loop never needed to\n // move it into the panel, leaving it sitting in `toolbarRight`,\n // invisible but still counted as if it occupied a column. Both fixed\n // by also checking `!b.el.hidden`, and adding one more for\n // `captionToggleButton` whenever it's currently shown.\n const pinnedCount =\n this.pluginToolbarButtons.filter(\n (b) => b.slot === 'right' && b.el.parentElement === toolbarRight && !b.el.hidden,\n ).length + (captionToggleButton.hidden ? 0 : 1);\n const columnCount = pinnedCount + 1; // + the caret itself\n toolbarOverflowPanel.style.gridTemplateColumns = `repeat(${columnCount}, 44px)`;\n }\n\n private closeToolbarOverflow(): void {\n if (!this.dom || !this.toolbarOverflowOpen) return;\n document.removeEventListener('keydown', this.onToolbarOverflowKeydown, true);\n this.dom.toolbarOverflowPanel.hidden = true;\n this.dom.toolbarOverflowButton.setAttribute('aria-expanded', 'false');\n this.toolbarOverflowOpen = false;\n this.focusTrap.retarget(this.dom.dialog);\n this.toolbarOverflowReturnFocus?.focus({ preventScroll: true });\n this.toolbarOverflowReturnFocus = null;\n }\n\n private pluginOverlay(el: HTMLElement, layer?: number): Unsubscribe {\n if (layer !== undefined) el.style.zIndex = String(layer);\n this.dom!.dialog.appendChild(el);\n const unhover = this.wireControlHover(el);\n return () => {\n unhover();\n el.remove();\n };\n }\n\n /** DESIGN.md §2.8/§3 — pauses auto-hide while genuinely hovered: controls, caption, and any plugin overlay (`ctx.ui.overlay()`). Unsubscribe also corrects the set if removed mid-hover — a real risk for overlay content a plugin can toggle while the gallery stays open, unlike static buttons. */\n private wireControlHover(el: HTMLElement): Unsubscribe {\n this.hoverableElements.add(el);\n const onEnter = (): void => {\n this.hoveringElements.add(el);\n this.onActivity();\n };\n const onLeave = (): void => {\n if (!this.hoveringElements.delete(el)) return;\n this.scheduleAutoHide();\n };\n el.addEventListener('pointerenter', onEnter);\n el.addEventListener('pointerleave', onLeave);\n return () => {\n el.removeEventListener('pointerenter', onEnter);\n el.removeEventListener('pointerleave', onLeave);\n this.hoverableElements.delete(el);\n if (this.hoveringElements.delete(el)) this.scheduleAutoHide();\n };\n }\n\n /**\n * DESIGN.md §2.8 — paused only by a real *hover*. Used to also treat a\n * *focused* control as active via `document.activeElement`, but a click\n * leaves a `<button>` focused indefinitely, permanently blocking\n * `hideControls()`. Focus still counts as activity (`focusin` →\n * `onActivity()`), it just no longer blocks the eventual hide.\n */\n private isControlActive(): boolean {\n return this.hoveringElements.size > 0;\n }\n\n /**\n * Syncs `hoveringElements` to the browser's own live `:hover` truth for\n * every registered control — both directions, not just dropping a stale\n * entry: also picks up one that's genuinely hovered but never got a\n * `pointerenter` of its own (see `hoveringElements`'s own doc comment for\n * why either direction can happen). Whichever ends up true here is what\n * `isControlActive()` reads immediately after, in the same `onActivity()`\n * call — no separate reveal step needed for a newly-added element; that\n * call already does it for any activity, hover included. Cheap in\n * practice: `hoverableElements` is normally single digits, and `:hover`\n * matching is a native, already-computed browser check, not a\n * layout-triggering one.\n */\n private reconcileHover(): void {\n for (const el of this.hoverableElements) {\n if (el.matches(':hover')) this.hoveringElements.add(el);\n else this.hoveringElements.delete(el);\n }\n }\n\n /**\n * The thumbnail for `index` — what the zoom transition animates to/from,\n * and what `activeThumbnail` marks/scrolls-to. `data-shoji-id` is an\n * explicit opt-in that works in any mode (needed for dynamic mode, which\n * has no scanned DOM); checked first so it can override selector mode's\n * default `scannedElements[index]` too, e.g. to target an inner element.\n */\n getOriginElement(index: number): HTMLElement | null {\n const item = this.itemList[index];\n if (item?.id && typeof CSS !== 'undefined' && typeof CSS.escape === 'function') {\n const marked = document.querySelector<HTMLElement>(\n `[data-shoji-id=\"${CSS.escape(item.id)}\"]`,\n );\n if (marked) return marked;\n }\n return this.scannedElements[index] ?? null;\n }\n\n /** Lets a plugin's background-page `scrollIntoView()` survive `close()`'s scroll restore, routed through this shared instance rather than importing `bodyScrollLock` directly — required for the standalone core+plugins distribution, where a direct import would get its own disconnected module copy (DESIGN.md §4.2/§10). Same reasoning `getOriginElement`/`getActiveMedia` were made public for. */\n markIntentionalScroll(): void {\n markIntentionalScrollInternal();\n }\n\n private scheduleAutoHide(): void {\n if (this.autoHideTimer !== null) {\n clearTimeout(this.autoHideTimer);\n this.autoHideTimer = null;\n }\n if (!this.opened) return;\n if (this.autoHideDelay === false) return; // always visible — no timer to arm\n if (this.autoHideDelay === 0) {\n this.hideControls(); // 0 = hidden immediately, no reveal — see onActivity\n return;\n }\n this.autoHideTimer = setTimeout(() => this.hideControls(), this.autoHideDelay);\n }\n\n /**\n * Forces the same fade §2.8's idle timer would eventually trigger —\n * public so a plugin can hide controls on its own trigger. Same\n * `isControlActive()` guard as the timer. Also a no-op under\n * `autoHideDelay: false` — a real bug, reported from real usage: Autoplay's\n * tap-to-toggle-chrome behavior called this directly and ignored `false`\n * entirely, since only the idle timer checked it. `false` has to hold for\n * every caller, not just the timer. `forceHideControls()`/\n * `setControlsHiddenForDrag()` (close, drag-to-close) deliberately don't\n * check this — direct user actions with their own feedback, not auto-hide.\n */\n hideControls(): void {\n // A real bug, reported from real usage: an idle timer already ticking\n // down before the caption modal opened isn't cancelled by opening it\n // (DESIGN.md §2.3a) — `onActivity()`'s own `captionModalOpen` guard\n // only stops a *new* timer being armed while it's open, it doesn't stop\n // one already in flight from firing. Left unguarded here, that timer\n // could still call this mid-read, and `.shoji-controls-hidden`'s own\n // `cursor: none` (shoji.css) applies to the whole dialog including the\n // modal's own backdrop/panel — the cursor visibly vanishing over text\n // the viewer is actively reading, not idling on.\n //\n // A real bug, found testing this cross-browser: `hoveringElements` is\n // normally kept in sync by `reconcileHover()` running inside\n // `onActivity()`, on every real pointer event — but that only helps if\n // one actually fires *after* the pointer reaches wherever it's really\n // ending up. Confirmed directly: some browser pointer-simulation paths\n // (reproduced consistently under Firefox automation, though nothing\n // rules out a genuine — if rarer — real-hardware equivalent) fire their\n // last bubbling pointer event mid-transition, before the browser's own\n // `:hover` state has caught up to the pointer's actual final position —\n // reconciling *then* just recorded the wrong, stale answer, and with no\n // further pointer event ever arriving, nothing was left to correct it\n // before this timer fired. A plain counter would have the identical gap\n // (whatever event it's trusted to fire is the same one that didn't).\n // Fixed by re-reconciling right here too, immediately before the\n // decision that actually depends on the answer being fresh — this is\n // the one moment `hoveringElements` being even briefly stale actually\n // matters, so it's also the one moment worth an extra, authoritative\n // check instead of trusting whatever the last activity event happened\n // to leave behind.\n this.reconcileHover();\n if (\n !this.dom ||\n this.autoHidden ||\n this.isControlActive() ||\n this.autoHideDelay === false ||\n this.captionModalOpen ||\n // DESIGN.md §3.1a — same reasoning as `captionModalOpen` immediately\n // above: an idle timer already ticking down before the toolbar\n // overflow popover opened isn't cancelled by opening it, and the\n // popover is anchored to (visually part of) the toolbar this would\n // hide out from under it.\n this.toolbarOverflowOpen\n )\n return;\n this.autoHidden = true;\n this.dom.dialog.classList.add('shoji-controls-hidden');\n this.bus.emit('controls:hide', {});\n }\n\n private showControls(): void {\n if (!this.dom || !this.autoHidden) return;\n this.autoHidden = false;\n this.dom.dialog.classList.remove('shoji-controls-hidden', 'shoji-controls-hidden-for-close');\n this.bus.emit('controls:show', {});\n }\n\n /**\n * DESIGN.md §2.1 — `caption` is `string | HTMLElement | DangerousHtmlCaption`:\n * a plain string renders as text (`textContent`, auto-escaped, safe by\n * default); an `HTMLElement` is appended as real DOM (the host built it);\n * `{ dangerouslySetInnerHTML }` renders raw, unescaped HTML via `innerHTML`\n * — Shoji does not sanitize it, the host must, same contract as React's\n * identically-named escape hatch. Returns whether the caption element\n * should be hidden.\n */\n private renderCaption(\n el: HTMLElement,\n caption: string | HTMLElement | DangerousHtmlCaption | undefined,\n ): boolean {\n if (caption instanceof HTMLElement) {\n el.replaceChildren(caption);\n return false;\n }\n if (isDangerousHtmlCaption(caption)) {\n el.innerHTML = caption.dangerouslySetInnerHTML;\n return caption.dangerouslySetInnerHTML === '';\n }\n el.textContent = caption ?? '';\n return !caption;\n }\n\n /**\n * `.shoji-toolbar-button` only — never `.shoji-close`/`.shoji-nav`, which\n * are different classes entirely, so this can't accidentally block\n * closing or navigating away from a slow-loading slide. `tabIndex = -1`\n * (not just the CSS below) so a keyboard user tabbing through the\n * toolbar skips these entirely while disabled, not just visually dims\n * them — `pointer-events: none` alone wouldn't stop Enter/Space\n * activating an already-focused button.\n */\n private setSlideLoading(loading: boolean): void {\n if (!this.dom) return;\n this.dom.outer.classList.toggle('shoji-slide-loading', loading);\n for (const button of this.dom.outer.querySelectorAll<HTMLButtonElement>(\n '.shoji-toolbar-button',\n )) {\n button.ariaDisabled = loading ? 'true' : null;\n if (loading) button.tabIndex = -1;\n else button.removeAttribute('tabindex');\n }\n }\n\n /**\n * DESIGN.md §2.5 — the caption's disappear-then-reappear fits inside the\n * *same* `--shoji-duration` window the slide's own mode animation runs\n * in, not a separate one after it: fades out over the first half, swaps\n * content invisibly at the midpoint, fades back in over the second half\n * — timed to finish exactly when the mode animation does, not still\n * catching up a beat behind it. Self-timed off the resolved\n * `--shoji-duration` value rather than hooked to the mode animation's\n * own completion event: both are driven by the same duration either way,\n * and the fade-in's own completion is what has to line up, which a\n * \"start on completion, then animate\" hook can't give — by definition,\n * a fade started only once something else finishes hasn't finished\n * *with* it.\n */\n private transitionCaption(): void {\n const dom = this.dom;\n if (!dom) return;\n if (\n typeof window.matchMedia === 'function' &&\n window.matchMedia('(prefers-reduced-motion: reduce)').matches\n ) {\n return; // --shoji-duration is already 0ms; nothing to sequence\n }\n\n // .shoji-caption's own `transition: opacity var(--shoji-duration) ...`\n // rule transitions a single property, so the computed shorthand\n // resolves to exactly --shoji-duration's own value — same read as\n // `zoomTransition.ts`/`SlideTransition.ts` already use elsewhere,\n // rather than `getPropertyValue('--shoji-duration')` directly.\n const raw = getComputedStyle(dom.caption).transitionDuration.split(',')[0]?.trim() ?? '';\n const fullMs = raw.endsWith('ms')\n ? parseFloat(raw)\n : raw.endsWith('s')\n ? parseFloat(raw) * 1000\n : 0;\n const half = fullMs / 2;\n\n this.captionFadePending = true;\n dom.caption.style.transitionDuration = `${half}ms`;\n dom.caption.style.opacity = '0';\n\n setTimeout(() => {\n if (!this.dom) return;\n this.captionFadePending = false;\n this.updateCaptionVisibility();\n this.dom.caption.style.opacity = '';\n setTimeout(() => {\n // Only the temporary half-duration override — restores the\n // caption's normal full-duration transition (e.g. for idle\n // auto-hide) once this fade-in has actually finished with it.\n if (this.dom) this.dom.caption.style.transitionDuration = '';\n }, half);\n }, half);\n }\n\n /**\n * Content is always kept current (correct the instant loading finishes,\n * no text flash) — only `hidden` also gates on `isActiveReady()`, so a\n * caption for the *new* slide can't sit there readable while the\n * image/video it describes is still a spinner.\n */\n private updateCaptionVisibility(): void {\n if (!this.dom || !this.slides) return;\n const item = this.itemList[this.activeIndex];\n const noCaption = this.renderCaption(this.dom.caption, item?.caption);\n const isVideo = !!item?.video;\n this.dom.caption.classList.toggle('shoji-caption--video', isVideo);\n const hiddenByToggle = isVideo && !this.captionVisibleOnVideo;\n this.dom.caption.hidden = noCaption || !this.slides.isActiveReady() || hiddenByToggle;\n\n const btn = this.dom.captionToggleButton;\n btn.hidden = !isVideo || noCaption;\n if (!btn.hidden) {\n btn.setAttribute('aria-pressed', String(this.captionVisibleOnVideo));\n btn.ariaLabel = btn.title = this.captionVisibleOnVideo\n ? this.locale.hideCaption\n : this.locale.showCaption;\n }\n this.updateCaptionTruncation();\n // A real bug, reported from real usage: reopening an already-loaded\n // slide (e.g. clicking the same thumbnail twice) skips the async\n // image-decode path entirely — nothing re-runs this measurement once\n // the dialog actually finishes laying out, so the synchronous call\n // above can catch it mid-layout (still effectively 0×0 at that exact\n // point, even though `hidden` itself is already correctly false) and\n // wrongly conclude nothing overflows. A fresh *first* open happens to\n // dodge this because real image decode work is slow enough that by the\n // time it resolves, layout has already settled — not something a\n // reopen (cached, near-instant) can rely on. Re-checking one frame\n // later, after layout has actually been committed, catches the case the\n // synchronous read above can miss without regressing it (the sync call\n // already got it right whenever it can, so this is a rarely-needed\n // correction, not a routine double-measurement).\n if (this.captionTruncationFrame !== null) cancelAnimationFrame(this.captionTruncationFrame);\n this.captionTruncationFrame = requestAnimationFrame(() => {\n this.captionTruncationFrame = null;\n this.updateCaptionTruncation();\n });\n }\n\n /**\n * DESIGN.md §2.3a — the caption's own default height cap (above) already\n * keeps it clear of the toolbar, but says nothing about the vertically-\n * centered prev/next nav arrows sharing its same left edge; a long\n * enough caption could still grow up over one of those. `--shoji-*`\n * collapses it to roughly one line by default (shoji.css) regardless, so\n * this only ever needs to detect whether that collapse actually clipped\n * something — `scrollHeight > clientHeight` after layout, the same\n * technique already used elsewhere in this codebase (and its own tests)\n * to detect caption overflow. Marks it truncated/interactive only when\n * there's genuinely more to read; a caption that already fits shouldn't\n * look or behave clickable.\n *\n * A real bug, reported from real usage: the first version only capped\n * `max-height` + `overflow: hidden` — a plain pixel clip with no regard\n * for where a line of text actually ends, so the cutoff routinely sliced\n * straight through the middle of the last visible line instead of\n * stopping at a clean line boundary, reading as broken rather than\n * intentionally truncated.\n *\n * Two follow-up attempts, both also wrong, both worth recording so they\n * aren't retried: `-webkit-line-clamp` never cleanly stopped at a line\n * boundary here regardless of the line count fed into it. Replacing it\n * with `lines * lineHeight + paddingY` arithmetic (using\n * `getComputedStyle(el).lineHeight`) still left a sliver of the next\n * line visible in a real browser — confirmed by screenshot, not just\n * this sandbox. Root cause: a browser doesn't necessarily lay out N\n * stacked lines at exactly N times the CSS `line-height` value: text\n * layout does its own sub-pixel rounding per line, so arithmetic\n * multiplication drifts from the real rendered geometry by enough to\n * expose part of an extra line, and no fixed safety margin is correct\n * for every font/zoom/line-count combination.\n *\n * This version doesn't compute line boundaries at all — it reads them\n * straight from the browser's own layout via `Range.getClientRects()`,\n * which returns one rect per actual rendered line fragment (rich\n * captions with inline markup can put more than one rect on the same\n * visual row; each is handled independently below rather than grouped,\n * since only the topmost/bottommost edges per row matter here). The cap\n * is set to the bottom edge of the last line whose bottom still fits\n * inside the current (arbitrary, CSS-calc'd) height budget — a real\n * measured boundary, never an assumed one.\n *\n * A third real bug, caught after switching to this measured approach:\n * padding the box out by a full `padding-bottom` past that last line's\n * *measured* bottom still isn't safe, because line boxes butt up much\n * closer together than the padding value — the next (excluded) line's\n * own top can fall well inside that padding band, so its glyph\n * ascenders paint into what was supposed to be empty breathing room.\n * Fixed by also finding that next line's top and never letting the cap\n * reach it, regardless of how much of `padding-bottom` that leaves.\n */\n private updateCaptionTruncation(): void {\n if (!this.dom) return;\n const el = this.dom.caption;\n el.style.removeProperty('max-height');\n const truncated = !el.hidden && el.scrollHeight > el.clientHeight;\n if (truncated) {\n const paddingBottom = parseFloat(getComputedStyle(el).paddingBottom) || 0;\n const budget = el.clientHeight - paddingBottom;\n const elTop = el.getBoundingClientRect().top;\n const range = document.createRange();\n range.selectNodeContents(el);\n let lastFittingBottom = 0;\n let nextLineTop = Infinity;\n for (const rect of range.getClientRects()) {\n if (rect.height === 0) continue;\n const relTop = rect.top - elTop;\n const relBottom = rect.bottom - elTop;\n if (relBottom <= budget) {\n if (relBottom > lastFittingBottom) lastFittingBottom = relBottom;\n } else if (relTop < nextLineTop) {\n nextLineTop = relTop;\n }\n }\n const cutBottom = lastFittingBottom > 0 ? lastFittingBottom : budget;\n const desired = cutBottom + paddingBottom;\n const cap = Number.isFinite(nextLineTop) ? Math.min(desired, nextLineTop) : desired;\n el.style.maxHeight = `${Math.max(0, Math.floor(cap) - 1)}px`;\n }\n el.classList.toggle('shoji-caption--truncated', truncated);\n if (truncated) {\n el.tabIndex = 0;\n el.setAttribute('role', 'button');\n el.setAttribute('aria-haspopup', 'dialog');\n } else {\n el.removeAttribute('tabindex');\n el.removeAttribute('role');\n el.removeAttribute('aria-haspopup');\n if (this.captionModalOpen) this.closeCaptionModal();\n }\n }\n\n private readonly onCaptionActivate = (event: Event): void => {\n if (!this.dom?.caption.classList.contains('shoji-caption--truncated')) return;\n this.captionModalOpenedViaKeyboard = event instanceof KeyboardEvent;\n if (event instanceof KeyboardEvent) {\n if (event.key !== 'Enter' && event.key !== ' ') return;\n event.preventDefault();\n // A real bug, reported from real usage against Autoplay's\n // onCaptionExpand (§4.1 point 18): a *keyboard* activation here\n // (Tab+Enter/Space, the only path this branch actually runs for)\n // must not also bubble up to onKeydown's shortcut dispatch — this is\n // unambiguously \"activate this control,\" not also a global shortcut\n // key, same reasoning the caption modal's own keydown handler\n // already uses to isolate itself from onKeydown. This alone doesn't\n // cover the click path below reopening on a *later*, unrelated\n // keypress — see `captionModalOpenedViaKeyboard`'s own doc comment\n // for that half of the fix.\n event.stopPropagation();\n } else {\n // A mouse-drag that ended in a real text selection is not a click to\n // open — `.shoji-caption` is deliberately excluded from drag-to-\n // navigate (GestureController's INTERACTIVE_CONTROL_SELECTOR) so the\n // viewer can select/copy caption text; without this check, finishing\n // that selection would also toggle the modal open on every drag.\n const selection = window.getSelection();\n if (selection && !selection.isCollapsed) return;\n // A link/button inside a rich-HTML caption keeps its own click\n // behavior — this only opens the modal for a click on the caption\n // itself, not one that's actually aimed at interactive content\n // inside it.\n if (event.target instanceof Element && event.target.closest('a, button')) return;\n }\n this.openCaptionModal();\n };\n\n /**\n * DESIGN.md §2.3a — shows the full caption (re-rendered via the same\n * `renderCaption()` the truncated one already used, so a rich-HTML\n * caption's own links/formatting are identical in both places) in a\n * small nested dialog, scrollable if it's still taller than the\n * viewport allows. `FocusTrap.retarget()` narrows Tab-cycling to just\n * this modal without touching the outer trap's own focus-restore state\n * (see its own doc comment for why a second `FocusTrap` instance isn't\n * used instead). Requested directly: the modal *replaces* the truncated\n * caption rather than just visually sitting on top of it — hiding\n * `dom.caption` too, not only because the modal's own opaque backdrop\n * already covers it, but so it can't still be reached by a screen\n * reader's browse-mode cursor (unlike Tab, `FocusTrap`/`retarget()`\n * don't affect that) while a completely different dialog is the one\n * actually open.\n */\n private openCaptionModal(): void {\n if (!this.dom || this.captionModalOpen) return;\n const item = this.itemList[this.activeIndex];\n this.renderCaption(this.dom.captionModalContent, item?.caption);\n this.dom.captionModal.hidden = false;\n // Captured before hiding the caption below (a real bug, caught by e2e\n // regression: hiding the currently-focused caption blurs it immediately\n // — a hidden element can't hold focus — so reading `activeElement`\n // *after* that line ended up capturing wherever the browser's own\n // focus-loss fallback landed (the dialog), not the caption itself.\n // Escape/close-button/backdrop then \"restored\" focus to the dialog\n // instead of back to the caption that was actually focused when this\n // opened.\n this.captionModalReturnFocus = document.activeElement as HTMLElement | null;\n this.dom.caption.hidden = true;\n // A real bug, caught testing this: the click that opens the modal\n // typically leaves the pointer sitting right over the caption —\n // hiding it out from under a *stationary* cursor never fires the\n // `pointerleave` `wireControlHover()` needs to drop it from\n // `hoveringElements` (browsers only fire that on actual pointer\n // movement past an element's edge, not on the element disappearing).\n // `reconcileHover()` would eventually self-heal this on the next real\n // pointer move anyway, but nothing meaningful should still read as\n // \"hovered\" once the modal has taken over — clearing it immediately\n // here is more deterministic than waiting for that.\n this.hoveringElements.clear();\n this.captionModalOpen = true;\n this.focusTrap.retarget(this.dom.captionModalPanel);\n document.addEventListener('keydown', this.onCaptionModalKeydown, true);\n this.bus.emit('captionModalChange', { open: true });\n }\n\n /**\n * Requested directly: closing should leave the viewer looking at a fully\n * normal, fully visible gallery — not just the modal gone, but the\n * truncated caption back (via `updateCaptionVisibility()`, which\n * recomputes its real hidden state from scratch rather than blindly\n * flipping a flag back — the more robust \"recompute from source of\n * truth\" this codebase already prefers elsewhere) and any auto-hidden\n * toolbar/nav explicitly re-shown (`onActivity()`, same pairing every\n * other real interaction already uses) rather than left hidden until\n * the viewer happens to move the mouse.\n */\n private closeCaptionModal(): void {\n if (!this.dom || !this.captionModalOpen) return;\n document.removeEventListener('keydown', this.onCaptionModalKeydown, true);\n this.dom.captionModal.hidden = true;\n this.captionModalOpen = false;\n this.updateCaptionVisibility();\n this.focusTrap.retarget(this.dom.dialog);\n if (this.captionModalOpenedViaKeyboard) {\n this.captionModalReturnFocus?.focus({ preventScroll: true });\n }\n this.captionModalReturnFocus = null;\n this.captionModalOpenedViaKeyboard = false;\n this.onActivity();\n this.bus.emit('captionModalChange', { open: false });\n }\n\n private renderCurrentSlide(openPlaceholderSrc?: string): void {\n if (!this.slides || !this.dom) return;\n const dom = this.dom;\n this.slides.render(\n this.itemList,\n this.activeIndex,\n (loadedIndex) => {\n if (loadedIndex === this.activeIndex) {\n this.bus.emit('slideItemLoad', { index: loadedIndex });\n this.setSlideLoading(false);\n // captionFadePending: skip here too, not just the synchronous\n // call below — decode() resolves on a microtask, which can win\n // the race against the mode animation's own onComplete (the one\n // that's actually supposed to update it), especially for an\n // already-preloaded slide.\n if (!this.captionFadePending) this.updateCaptionVisibility();\n }\n },\n openPlaceholderSrc,\n this.loop,\n );\n this.setSlideLoading(!this.slides.isActiveReady());\n\n const item = this.itemList[this.activeIndex];\n const total = this.itemList.length;\n dom.counter.textContent = total > 0 ? `${this.activeIndex + 1} / ${total}` : '';\n dom.counter.hidden = !this.showCounter || total === 0;\n // navigate()'s animated path already faded the outgoing caption out\n // and updates content itself, once that fade (and the slide's own mode\n // transition) actually finishes — updating it here too, immediately,\n // would flash the new caption's text mid-fade instead of the outgoing\n // one.\n if (!this.captionFadePending) this.updateCaptionVisibility();\n const label = `Image ${this.activeIndex + 1} of ${total}${item?.alt ? `: ${item.alt}` : ''}`;\n this.liveRegion.announce(label);\n\n dom.prevButton.hidden = total <= 1;\n dom.nextButton.hidden = total <= 1;\n dom.prevButton.disabled = !this.loop && this.activeIndex <= 0;\n dom.nextButton.disabled = !this.loop && this.activeIndex >= total - 1;\n }\n\n open(index = this.options.index ?? 0): void {\n if (this.destroyed || this.opened) return;\n this.bus.emit('beforeOpen', { index });\n this.ensureLightbox();\n this.opened = true;\n this.activeIndex = index;\n this.captionVisibleOnVideo = !!this.options.showVideoCaption;\n lockBodyScroll();\n const origin = this.getOriginElement(index);\n // Both the open placeholder and the zoom-in animation below need a size\n // to show/grow toward before the real image has loaded — without\n // item.width/height, that size can only ever be guessed (\"probably\n // fills the dialog\"), which is exactly what visibly overshoots for a\n // genuinely small photo (§2.3/§2.3b's own real-bug history). Rather\n // than guess, skip both entirely when naturalSize is unknown: the\n // ordinary spinner shows, and the real image just appears once it's\n // ready — no animation of its own, deliberately (same \"no second,\n // disconnected transition\" reasoning the placeholder-to-real-image\n // handoff already uses elsewhere).\n const naturalSize = this.resolveNaturalSize(index);\n // The first slide's caption fades in alongside zoomIn() the same way a\n // navigated-to one fades in alongside the mode transition (§2.5) —\n // only when there's actually a zoom to sync with, same \"no second,\n // disconnected transition\" reasoning as the comment above: nothing\n // grows in from the thumbnail without known dimensions, so nothing\n // should fade in disconnected from that either.\n const fadeInOnOpen =\n !!(origin && naturalSize) &&\n !(\n typeof window.matchMedia === 'function' &&\n window.matchMedia('(prefers-reduced-motion: reduce)').matches\n );\n if (fadeInOnOpen && this.dom) {\n this.captionFadePending = true;\n this.dom.caption.style.opacity = '0';\n }\n // A real bug/inconsistency: a video item *does* have a known naturalSize\n // (its aspect ratio), but the low-res placeholder this unlocks is a\n // dead end — it's a static photo, immediately replaced by something\n // entirely different (an error state, or the real embed), not the same\n // image sharpening the way a photo's placeholder-to-full-res handoff\n // is. That made a fresh open() inconsistent with navigating back to an\n // already-cached video slide (no placeholder shown there at all, since\n // navigate() never passes one) — same \"no second, disconnected\n // transition\" reasoning as the naturalSize-unknown case above, just\n // triggered by content type instead of missing dimensions.\n this.renderCurrentSlide(\n naturalSize && !this.itemList[index]?.video\n ? this.resolveOpenPlaceholderSrc(this.itemList[index], origin)\n : undefined,\n );\n this.dom!.outer.classList.add('shoji-open');\n document.addEventListener('keydown', this.onKeydown);\n this.focusTrap.activate(this.dom!.dialog);\n this.scheduleAutoHide();\n\n const media = this.slides?.getActiveMedia();\n if (media && origin && naturalSize) {\n zoomIn({\n origin,\n target: media,\n aspectRatio: this.resolveAspectRatio(index, origin),\n naturalSize,\n });\n }\n if (fadeInOnOpen && this.dom) {\n // No half-duration split here, unlike transitionCaption() — there's\n // no outgoing caption to fade out first, just this one fading in\n // from nothing, over the same full --shoji-duration zoomIn() itself\n // runs on.\n this.captionFadePending = false;\n this.updateCaptionVisibility();\n this.dom.caption.style.opacity = '';\n }\n\n this.bus.emit('open', { index });\n this.bus.emit('afterOpen', { index });\n }\n\n /** DESIGN.md §2.3 — low-res open() placeholder source, checked in order: item.thumb, a live data-shoji-thumb on origin, else origin's own rendered <img>. */\n private resolveOpenPlaceholderSrc(\n item: GalleryItem | undefined,\n origin: HTMLElement | null,\n ): string | undefined {\n if (item?.thumb) return item.thumb;\n const dataThumb = origin?.getAttribute('data-shoji-thumb');\n if (dataThumb) return dataThumb;\n const img = origin?.querySelector('img');\n return img?.currentSrc || img?.src || undefined;\n }\n\n /**\n * DESIGN.md §2.2/§2.6 — navigates while open; always clamps an explicit\n * out-of-range index (this is a directed jump, not a step — `loop` only\n * affects `next()`/`prev()`, see below). No-op if not open. `animate`\n * (default `true`) runs the configured §2.5 transition; hosts that want\n * an instant jump — e.g. syncing to an external index without a visual\n * flourish — can pass `{ animate: false }`.\n */\n goTo(index: number, options?: { animate?: boolean }): void {\n if (this.destroyed || !this.opened || this.itemList.length === 0) return;\n const target = this.clampToRange(index);\n if (target === this.activeIndex) return;\n const direction: 1 | -1 = target > this.activeIndex ? 1 : -1;\n this.navigate(target, direction, options?.animate ?? true);\n }\n\n next(): void {\n this.navigate(this.nextIndex(), 1, true);\n }\n\n prev(): void {\n this.navigate(this.prevIndex(), -1, true);\n }\n\n private nextIndex(): number {\n const atEnd = this.activeIndex >= this.itemList.length - 1;\n return atEnd && this.loop ? 0 : this.activeIndex + 1;\n }\n\n private prevIndex(): number {\n const atStart = this.activeIndex <= 0;\n return atStart && this.loop ? this.itemList.length - 1 : this.activeIndex - 1;\n }\n\n /**\n * DESIGN.md §2.5 — the shared path behind `goTo()`/`next()`/`prev()`.\n * `animate` is `false` only for `GestureController`'s own host callbacks\n * (see `ensureLightbox()`): a gesture-completed swipe already played its\n * own live-drag/settle animation, so running a *second*, `mode`-based\n * transition on top of it would visibly double-animate. Every other\n * caller — buttons, keyboard, autoplay, a plugin calling `goTo()` — gets\n * the real, configured transition.\n */\n private navigate(target: number, direction: 1 | -1, animate: boolean): void {\n if (this.destroyed || !this.opened || this.itemList.length === 0) return;\n const clamped = this.clampToRange(target);\n if (clamped === this.activeIndex) return;\n // DESIGN.md §2.3a — the modal shows a specific slide's caption; leaving\n // it open across a navigation (arrow keys reach it even while the\n // gesture gate above suspends drag-navigate) would keep showing the\n // outgoing slide's text over the new slide.\n if (this.captionModalOpen) this.closeCaptionModal();\n // DESIGN.md §3.1a — the popover's own position depends on the current\n // toolbar height, which can change slide-to-slide (e.g. a video slide's\n // caption-toggle button appearing/disappearing); simplest to just close\n // it rather than reposition it mid-navigation.\n if (this.toolbarOverflowOpen) this.closeToolbarOverflow();\n const from = this.activeIndex;\n // beforeSlide fires first so a listener (e.g. Autoplay) can detach its\n // own 'pause' listener from the outgoing video before pauseMedia() below\n // triggers a native 'pause' event on it — otherwise that listener is\n // still attached and misreads this programmatic pause as the viewer\n // manually pausing the video, a real regression this caused once\n // (advancing past a stuck video called next(), which paused it here,\n // which the still-attached listener treated as a manual pause and\n // stopped the whole slideshow before the new slide's afterSlide handler\n // ever got a chance to run). activeIndex hasn't changed yet at this\n // point, so getActiveMedia() still resolves to the outgoing slide either\n // way — only the listener-detach ordering relative to pauseMedia matters.\n this.bus.emit('beforeSlide', { from, to: clamped });\n // A video started by the viewer keeps playing otherwise — this slide\n // stays cached (still within `preload`), and only pausing (not\n // releasing) lets it resume right where it left off if revisited.\n pauseMedia(this.slides?.getActiveMedia() ?? null);\n this.activeIndex = clamped;\n\n if (animate && this.transition) {\n // Not layered onto a gesture-completed swipe (the `else` branch) —\n // same scoping as the mode transition itself (§2.4/§2.5).\n this.transitionCaption();\n const swap = (): void => this.renderCurrentSlide();\n const modeName = this.resolveTransitionMode();\n const builtin = TRANSITION_PRESETS[modeName];\n if (builtin) {\n this.transition.animate(builtin, direction, swap);\n } else {\n this.transition.animateCustom(modeName, direction, swap);\n }\n } else {\n const swap = (): void => this.renderCurrentSlide();\n swap();\n }\n\n this.bus.emit('afterSlide', { from, to: clamped });\n }\n\n private resolveTransitionMode(): string {\n return this.options.mode ?? 'slide';\n }\n\n close(): void {\n this.beginClose();\n }\n\n /**\n * DESIGN.md §2.4/§2.6a — same effect as `close()`, from a completed\n * vertical swipe. `frozenDrag` is threaded through to `zoomOut()`'s own\n * `dragStart`, so the whole close continues as one motion from exactly\n * where the drag left off (see `GestureController.\n * takeFrozenDragTransform`).\n */\n private closeFromSwipe(frozenDrag: FrozenDragTransform): void {\n this.beginClose(frozenDrag);\n }\n\n /**\n * `frozenDrag` — see `closeFromSwipe()`'s doc comment; absent for a\n * button-close, which has no drag to continue from. Controls fade and the\n * zoom-out run concurrently, both starting the instant `forceHideControls()`\n * runs — not sequenced one after the other. (A previous version waited for\n * the controls' own fade to fully finish before starting the zoom-out, for\n * a button-close specifically — requested directly, to avoid stationary\n * chrome hovering over an already-shrinking photo. Reversed on later,\n * explicit feedback: waiting read as two distinct steps rather than one\n * motion; starting both together still avoids stationary chrome, since\n * the chrome is disappearing too, just without the pause.)\n */\n private beginClose(frozenDrag?: FrozenDragTransform): void {\n if (this.destroyed || !this.opened || this.isClosing) return;\n // DESIGN.md §2.3a — every close path (button, swipe, Escape, backdrop\n // click, destroy-while-open) funnels through here; the modal must not\n // linger past the lightbox itself closing, and its own document-level\n // keydown listener needs removing before that.\n if (this.captionModalOpen) this.closeCaptionModal();\n if (this.toolbarOverflowOpen) this.closeToolbarOverflow();\n // Read before beforeClose fires — the Zoom plugin's own beforeClose\n // handler resets its scale/pan back to neutral there (so zoomOut()'s\n // measurement below stays correct, same reasoning dragStart's own doc\n // comment explains), which would otherwise erase the very state this\n // is capturing.\n const zoomStart = this.zoomStartProvider?.() ?? undefined;\n this.bus.emit('beforeClose', {});\n\n // .shoji-outer must stay display:block for the zoom-out to be visible,\n // so the actual state flip (isOpen, focus restore, etc.) is deferred\n // until the animation finishes — see finishClose(). isClosing guards\n // against a second close() (e.g. a repeated Escape) re-triggering the\n // animation mid-flight while isOpen is still (deliberately) true.\n this.isClosing = true;\n\n const media = this.slides?.getActiveMedia();\n // close() never tears down the slide pool (reopening should be\n // instant), so nothing else stops a viewer-started video just because\n // the lightbox is now hidden.\n pauseMedia(media ?? null);\n\n const origin = this.getOriginElement(this.activeIndex);\n const naturalSize = this.resolveNaturalSize(this.activeIndex);\n // Real content already rendered doesn't need naturalSize at all —\n // effectiveTargetBox() (zoomTransition.ts) measures it directly, no\n // guessing involved. Only closing before it ever finished loading (no\n // real content, no naturalSize to fall back on either) would have\n // nothing but a guessed target to shrink toward — same \"don't guess\"\n // reasoning open() above already applies to its own animation.\n if (media && origin && (this.slides?.isActiveReady() || naturalSize)) {\n const aspectRatio = this.resolveAspectRatio(this.activeIndex, origin);\n this.forceHideControls();\n // Starts at the same instant as the zoom-out and the controls fade —\n // see the comment on .shoji-backdrop's own transition (shoji.css) for\n // why this can't just be left to the instant display:none cut at the\n // end of finishClose().\n if (this.dom) this.dom.backdrop.style.opacity = '0';\n zoomOut(\n { origin, target: media, aspectRatio, naturalSize, dragStart: frozenDrag, zoomStart },\n () => this.finishClose(),\n );\n } else {\n this.finishClose();\n }\n }\n\n /**\n * Forces the same fade §2.8's idle timer would eventually trigger, bypassing `hideControls()`'s own `isControlActive()` hover guard — the most common close path (clicking close) is hovering a control at this exact instant, and a deliberate close should hide regardless. No-op if already hidden.\n *\n * Also marks `.shoji-controls-hidden-for-close` alongside the ordinary\n * class — a real bug, reported from real usage: a plugin's own overlay\n * (Autoplay's progress bar, `autoplay.css`) was wired to fade on plain\n * `.shoji-controls-hidden`, the same class *ordinary idle auto-hide* also\n * applies — so it faded out and stayed gone through every idle period\n * too, not just the close animation the original request was actually\n * about. That went unnoticed for a while because until recently, tapping\n * the image toggled play/pause *and* revealed controls on the same tap,\n * papering over how often it was actually gone; once that tap-to-toggle\n * was removed (§4.1 point 15) as a separate, unrelated fix, an idle\n * slideshow now visibly loses its own progress indicator and never gets\n * it back without an unrelated interaction — surfacing this as a real\n * regression in practice, even though no code touching this class had\n * changed. This second class is the hook a plugin's CSS can key off\n * specifically for \"closing,\" without it ever matching ordinary idle-hide.\n */\n private forceHideControls(): void {\n if (!this.dom || this.autoHidden) return;\n this.autoHidden = true;\n this.dom.dialog.classList.add('shoji-controls-hidden', 'shoji-controls-hidden-for-close');\n this.bus.emit('controls:hide', {});\n }\n\n /**\n * DESIGN.md §2.4/§2.8 — `GestureController`'s live vertical-drag cue: hide\n * past the same distance a release would close, reveal again on retreat.\n * `hidden: false` only reveals if visible when *this* gesture started\n * (`controlsHiddenAtGestureStart`) — shouldn't resurrect controls already\n * separately hidden (idle, or `autoHideDelay: 0`) before the drag began.\n * `dragCloseThreshold` (bus event) fires on every crossing regardless of\n * that guard — Autoplay (§4-autoplay) needs to know about the crossing\n * itself, not just whether controls visibly moved.\n */\n private setControlsHiddenForDrag(hidden: boolean): void {\n this.controlsHiddenByDrag = hidden;\n if (hidden) this.forceHideControls();\n else if (!this.controlsHiddenAtGestureStart) this.showControls();\n // A real bug, reported from real usage: `.shoji-controls-hidden` also\n // hides the cursor (shoji.css) — fine for an idle/inactive hide, but\n // this hide happens *during* an active drag, so the cursor vanished\n // right when the viewer most needs to see it. This marker class scopes\n // a cursor override to specifically this case (see shoji.css).\n this.dom?.dialog.classList.toggle('shoji-controls-hidden-for-drag', hidden);\n this.bus.emit('dragCloseThreshold', { hidden });\n }\n\n /** `item.width`/`height`, else origin's `naturalWidth`/`naturalHeight` (accurate when `item.thumb` is unset). Feeds `computeTransform`'s letterbox-aware sizing. */\n private resolveAspectRatio(index: number, origin: HTMLElement | null): number | undefined {\n const item = this.itemList[index];\n if (item?.width && item.height) return item.width / item.height;\n const thumbImg = origin?.querySelector('img');\n if (thumbImg?.naturalWidth && thumbImg.naturalHeight) {\n return thumbImg.naturalWidth / thumbImg.naturalHeight;\n }\n return undefined;\n }\n\n /**\n * `item.width`/`height` only — deliberately never a thumbnail's own\n * `naturalWidth`/`naturalHeight` the way `resolveAspectRatio` above will:\n * that's a fine stand-in for *shape*, but using it as the real photo's\n * true pixel size would under-cap a genuinely large photo down to\n * thumbnail resolution. `undefined` here just means \"genuinely unknown,\"\n * not \"assume small\" — `zoomTransition.ts`'s `containedBox` already\n * treats it that way (no cap applied at all).\n */\n private resolveNaturalSize(index: number): { width: number; height: number } | undefined {\n const item = this.itemList[index];\n return item?.width && item.height ? { width: item.width, height: item.height } : undefined;\n }\n\n /**\n * Idempotent on purpose: a pending zoom-out's `transitionend`/fallback\n * timeout can still fire after `destroy()` has already force-finished the\n * close (§ destroy() below) — the `!this.opened` guard makes that a no-op\n * instead of a second `close`/`afterClose` emission on a torn-down gallery.\n */\n private finishClose(): void {\n if (!this.opened) return;\n this.isClosing = false;\n this.opened = false;\n unlockBodyScroll();\n document.removeEventListener('keydown', this.onKeydown);\n this.focusTrap.deactivate();\n this.dom?.outer.classList.remove('shoji-open');\n // Cleared, not left at '0' — a fresh open() must start fully opaque\n // again, not still faded out from the last close.\n if (this.dom) this.dom.backdrop.style.opacity = '';\n if (this.autoHideTimer !== null) {\n clearTimeout(this.autoHideTimer);\n this.autoHideTimer = null;\n }\n this.autoHidden = false;\n this.hoveringElements.clear();\n this.controlsHiddenByDrag = false;\n this.dom?.dialog.classList.remove('shoji-controls-hidden', 'shoji-controls-hidden-for-close');\n // A completed drag-close never calls setControlsHiddenForDrag(false)\n // (nothing left to un-hide for) — clean up the marker here instead, so\n // it can't linger into the next open().\n this.dom?.dialog.classList.remove('shoji-controls-hidden-for-drag');\n this.bus.emit('close', {});\n this.bus.emit('afterClose', {});\n }\n\n /** DESIGN.md §2.1 — diffs by id (fallback src), preserving the active slide. */\n updateSlides(items: GalleryItemInput[], currentIndex?: number): void {\n if (this.destroyed) return;\n\n const activeItem = this.itemList[this.activeIndex];\n const activeKey = activeItem ? itemKey(activeItem) : undefined;\n this.itemList = resolveDynamicVideoItems(items);\n\n let nextIndex = currentIndex;\n if (nextIndex === undefined && activeKey !== undefined) {\n const preserved = items.findIndex((item) => itemKey(item) === activeKey);\n if (preserved !== -1) nextIndex = preserved;\n }\n this.activeIndex = Math.min(\n Math.max(nextIndex ?? this.activeIndex, 0),\n Math.max(items.length - 1, 0),\n );\n\n if (this.opened) this.renderCurrentSlide();\n this.bus.emit('itemsUpdated', { items: this.itemList });\n }\n\n /**\n * Sugar over `updateSlides()` for the common \"insert some items\" case —\n * splices `items` into a copy of the current list at `atIndex` (default:\n * append at the end) and hands the result to `updateSlides()`, which\n * still does all the real work (active-item preservation, live re-render\n * if open, `itemsUpdated`). `atIndex` follows `Array.prototype.splice`'s\n * own semantics (negative counts from the end, out-of-range clamps) —\n * no extra validation on top of that.\n */\n addSlides(items: GalleryItemInput[], atIndex?: number): void {\n if (this.destroyed) return;\n const next: GalleryItemInput[] = [...this.itemList];\n next.splice(atIndex ?? next.length, 0, ...items);\n this.updateSlides(next);\n }\n\n /**\n * Sugar over `updateSlides()` for the common \"remove some items\" case.\n * Accepts a single id/index or an array mixing both: a `string` matches\n * the same `id ?? src` key `updateSlides()` already uses for active-item\n * preservation, a `number` matches that position in the *current*\n * `items` list (resolved against the list as it is now, before any\n * removal — passing `[0, 1]` removes the first two items, not \"index 0,\n * then whatever became index 1 after that\").\n */\n removeSlides(match: string | number | Array<string | number>): void {\n if (this.destroyed) return;\n const matches = Array.isArray(match) ? match : [match];\n const indices = new Set(matches.filter((m): m is number => typeof m === 'number'));\n const keys = new Set(matches.filter((m): m is string => typeof m === 'string'));\n const next = this.itemList.filter((item, i) => !indices.has(i) && !keys.has(itemKey(item)));\n this.updateSlides(next);\n }\n\n /** DESIGN.md §2.7 — selector-mode only; sugar over updateSlides() sourced from a DOM rescan. */\n refresh(): void {\n if (this.destroyed) return;\n if (this.isDynamicMode) {\n console.warn('Shoji: refresh() is a no-op in dynamic mode — call updateSlides() instead.');\n return;\n }\n const scanned = scanContainer(this.element, this.selector);\n this.scannedElements = scanned.map((s) => s.element);\n this.updateSlides(scanned.map((s) => s.item));\n }\n\n /** Shared by `destroy()`/`reinit()` (§2.7): force-close, run plugin cleanups, drop gesture/transition/slide/dialog. */\n private teardown(): void {\n if (this.opened) {\n // A real bug, caught by cross-test interference in the unit suite —\n // never reproduced as a simple direct assertion because the existing\n // \"destroy() while the modal is open\" test's `removeEventListener`\n // spy assertion was too loose to actually catch it (FocusTrap's own\n // unrelated document keydown listener removal satisfied the same\n // `expect.any(Function)` matcher). `beginClose()` closes the caption\n // modal first (DESIGN.md §2.3a) — `teardown()` calls `finishClose()`\n // directly instead, skipping `beginClose()` and its own close-path\n // entirely, so destroying a gallery while its caption modal was open\n // never removed that modal's document-level keydown listener. Left\n // unfixed, it leaks forever: a capture-phase listener bound to the\n // destroyed instance, `stopPropagation()`-ing every future keydown on\n // the page regardless of what it's for.\n if (this.captionModalOpen) this.closeCaptionModal();\n // Same gap, same fix, for the toolbar overflow popover (§3.1a) —\n // teardown() skips beginClose() entirely, so a destroy()/reinit()\n // while it happened to be open would leak its own document-level\n // keydown listener the same way.\n if (this.toolbarOverflowOpen) this.closeToolbarOverflow();\n if (!this.isClosing) this.bus.emit('beforeClose', {});\n this.finishClose();\n }\n for (const cleanup of this.pluginCleanups) cleanup();\n this.pluginCleanups = [];\n this.shortcuts.clear();\n this.pluginStorage.clear();\n this.activePluginNames.clear();\n this.videoProviders.clear();\n if (!this.isDynamicMode) {\n this.element.removeEventListener('click', this.onContainerClick);\n }\n this.gesture?.destroy();\n this.gesture = null;\n this.transition = null;\n this.toolbarHeightObserver?.disconnect();\n this.toolbarHeightObserver = null;\n if (this.toolbarHeightFrame !== null) cancelAnimationFrame(this.toolbarHeightFrame);\n this.toolbarHeightFrame = null;\n if (this.captionTruncationFrame !== null) cancelAnimationFrame(this.captionTruncationFrame);\n this.captionTruncationFrame = null;\n this.captionFadePending = false;\n this.slides?.destroy();\n this.dom?.outer.remove();\n this.slides = null;\n this.dom = null;\n // A real leak, caught by inspection: core's own `wireControlHover()`\n // calls in `ensureLightbox()` (closeButton, toolbar slots, caption,\n // counter, ...) never store/call the unsubscribe they return, since\n // there was never anywhere else that needed it before — those elements\n // were expected to just live and die with the rest of `dom` above.\n // `reinit()` routes through here too, though, and rebuilds a whole new\n // `dom` afterward (`ensureLightbox()` runs again) — without this,\n // `hoverableElements` would keep every previous reinit cycle's now-\n // detached elements forever, growing unbounded across repeated calls.\n this.hoverableElements.clear();\n this.hoveringElements.clear();\n // Same reasoning: a plugin's own `ctx.ui.toolbar()` unsubscribe not\n // being called for some reason must not leave stale, now-detached\n // elements in here across a `reinit()` cycle either.\n this.pluginToolbarButtons.length = 0;\n }\n\n destroy(): void {\n if (this.destroyed) return;\n // Guarded, not a plain delete(): if a second Gallery was constructed on\n // this same element without the first ever being destroyed, the\n // registry entry already points at that newer instance — an old,\n // stale `this` being destroyed later must not evict it.\n if (instanceRegistry.get(this.element) === this) instanceRegistry.delete(this.element);\n allInstances.delete(this);\n this.teardown();\n this.bus.emit('destroy', {});\n this.bus.clear();\n this.destroyed = true;\n }\n\n /**\n * DESIGN.md §2.7 — full teardown + reconstruction on `this`, not a new\n * object: `Shoji.getInstance(el)`/any held reference keeps working across\n * the call, unlike `destroy()` + `new Shoji(...)`. Omit `options` to\n * rebuild unchanged (a hard reset); pass options to reconfigure\n * structurally. `gallery.on(...)` listeners do NOT survive — the event\n * bus is fully cleared, same as `destroy()` — re-`on()` anything needed.\n */\n reinit(options: GalleryOptions = this.options): void {\n if (this.destroyed) return;\n this.teardown();\n this.bus.clear();\n this.applyOptions(options);\n }\n}\n"],"names":["prefersReducedMotion","parseCssTime","clearInlineTransform","preset","markIntentionalScrollInternal"],"mappings":"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"}
|