@michaelyagi/shoji 0.1.0-alpha.10
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/LICENSE +21 -0
- package/README.md +83 -0
- package/dist/esm/core/EventBus.d.ts +16 -0
- package/dist/esm/core/FocusTrap.d.ts +9 -0
- package/dist/esm/core/Gallery.d.ts +221 -0
- package/dist/esm/core/GestureController.d.ts +57 -0
- package/dist/esm/core/LiveRegion.d.ts +6 -0
- package/dist/esm/core/SlideManager.d.ts +85 -0
- package/dist/esm/core/bodyScrollLock.d.ts +2 -0
- package/dist/esm/core/dom.d.ts +27 -0
- package/dist/esm/core/icons.d.ts +6 -0
- package/dist/esm/core/index.d.ts +5 -0
- package/dist/esm/core/index.js +2105 -0
- package/dist/esm/core/index.js.map +1 -0
- package/dist/esm/core/plugin.d.ts +54 -0
- package/dist/esm/core/rotateFlipNormalize.d.ts +19 -0
- package/dist/esm/core/scan.d.ts +19 -0
- package/dist/esm/core/types.d.ts +290 -0
- package/dist/esm/core/zoomTransition.d.ts +33 -0
- package/dist/esm/gestures/GestureEngine.d.ts +78 -0
- package/dist/esm/index.css +540 -0
- package/dist/esm/index.d.ts +32 -0
- package/dist/esm/index.js +24 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/index2.css +22 -0
- package/dist/esm/index3.css +170 -0
- package/dist/esm/index4.css +27 -0
- package/dist/esm/plugins/activeThumbnail/index.d.ts +26 -0
- package/dist/esm/plugins/activeThumbnail/index.js +63 -0
- package/dist/esm/plugins/activeThumbnail/index.js.map +1 -0
- package/dist/esm/plugins/autoplay/icons.d.ts +3 -0
- package/dist/esm/plugins/autoplay/index.d.ts +19 -0
- package/dist/esm/plugins/autoplay/index.js +207 -0
- package/dist/esm/plugins/autoplay/index.js.map +1 -0
- package/dist/esm/plugins/fullscreen/icons.d.ts +3 -0
- package/dist/esm/plugins/fullscreen/index.d.ts +17 -0
- package/dist/esm/plugins/fullscreen/index.js +74 -0
- package/dist/esm/plugins/fullscreen/index.js.map +1 -0
- package/dist/esm/plugins/layout/index.d.ts +191 -0
- package/dist/esm/plugins/layout/index.js +752 -0
- package/dist/esm/plugins/layout/index.js.map +1 -0
- package/dist/esm/plugins/layout/justified.d.ts +68 -0
- package/dist/esm/plugins/layout/masonry.d.ts +92 -0
- package/dist/esm/plugins/rotateFlip/icons.d.ts +5 -0
- package/dist/esm/plugins/rotateFlip/index.d.ts +17 -0
- package/dist/esm/plugins/rotateFlip/index.js +138 -0
- package/dist/esm/plugins/rotateFlip/index.js.map +1 -0
- package/dist/esm/plugins/video/index.d.ts +13 -0
- package/dist/esm/plugins/video/index.js +207 -0
- package/dist/esm/plugins/video/index.js.map +1 -0
- package/dist/esm/plugins/video/vimeo.d.ts +43 -0
- package/dist/esm/plugins/video/youtube.d.ts +61 -0
- package/dist/esm/plugins/zoom/icons.d.ts +4 -0
- package/dist/esm/plugins/zoom/index.d.ts +30 -0
- package/dist/esm/plugins/zoom/index.js +274 -0
- package/dist/esm/plugins/zoom/index.js.map +1 -0
- package/dist/esm/plugins/zoom/zoomMath.d.ts +24 -0
- package/dist/esm/transitions/SlideTransition.d.ts +25 -0
- package/dist/esm/transitions/presets.d.ts +21 -0
- package/dist/esm/zoomTransition-bbKHpVpA.js +110 -0
- package/dist/esm/zoomTransition-bbKHpVpA.js.map +1 -0
- package/dist/shoji.css +759 -0
- package/dist/shoji.js +3907 -0
- package/dist/shoji.js.map +1 -0
- package/dist/shoji.min.css +1 -0
- package/dist/shoji.min.js +2 -0
- package/dist/shoji.min.js.map +1 -0
- package/package.json +77 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shoji.min.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","../src/plugins/activeThumbnail/index.ts","../src/plugins/autoplay/icons.ts","../src/plugins/autoplay/index.ts","../src/plugins/fullscreen/icons.ts","../src/plugins/fullscreen/index.ts","../src/plugins/layout/masonry.ts","../src/plugins/layout/justified.ts","../src/plugins/layout/index.ts","../src/core/rotateFlipNormalize.ts","../src/plugins/rotateFlip/icons.ts","../src/plugins/rotateFlip/index.ts","../src/plugins/video/vimeo.ts","../src/plugins/video/youtube.ts","../src/plugins/video/index.ts","../src/plugins/zoom/icons.ts","../src/plugins/zoom/zoomMath.ts","../src/plugins/zoom/index.ts","../src/index.ts"],"sourcesContent":["/** DESIGN.md §2.6a — reference-counted page scroll lock. */\nlet lockCount = 0;\nlet savedOverflow = '';\nlet savedHtmlOverflow = '';\n\nexport function lockBodyScroll(): void {\n if (lockCount === 0) {\n savedOverflow = document.body.style.overflow;\n document.body.style.overflow = 'hidden';\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 lockCount++;\n}\n\nexport function unlockBodyScroll(): void {\n lockCount = Math.max(0, lockCount - 1);\n if (lockCount === 0) {\n document.body.style.overflow = savedOverflow;\n document.documentElement.style.overflow = savedHtmlOverflow;\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 PLAY_ICON =\n '<svg viewBox=\"0 0 24 24\" width=\"28\" height=\"28\" fill=\"currentColor\"><path d=\"M8 5v14l11-7z\"/></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","import { CAPTION_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}\n\nexport interface LightboxDom {\n outer: 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 §3 — `ctx.ui.toolbar(slot, ...)` inserts into these; close lives in `right`. */\n toolbarLeft: HTMLElement;\n toolbarCenter: HTMLElement;\n toolbarRight: 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 toolbarRight.append(captionToggleButton, closeButton);\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\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 outer.appendChild(dialog);\n\n return {\n outer,\n dialog,\n counter,\n caption,\n closeButton,\n prevButton,\n nextButton,\n captionToggleButton,\n toolbarLeft,\n toolbarCenter,\n toolbarRight,\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 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 // -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 // 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 — a real bug: capturing eagerly retargets the\n // browser's synthesized mousedown/click to the capturing element\n // (Chromium computes their target using whatever capture state is\n // already in effect once pointerdown's own synchronous handlers have\n // run), so a plain tap/click on the content was landing on `target`\n // itself instead of whatever the pointer was actually over — breaking\n // any click-target-based logic downstream (Gallery's click-outside-\n // to-close, in particular: it saw every click, even one dead-center\n // on the photo, as if it had landed on nothing recognizable, and\n // closed the gallery). A confirmed drag never fires a native \"click\"\n // anyway (mousedown/mouseup target different elements once the\n // pointer has actually moved), so deferring capture to here loses\n // nothing for real drags while leaving taps/clicks alone entirely.\n // Still not enough on its own for a gesture whose *effect* a caller\n // suppresses but whose direction still locks here regardless (the\n // zoom plugin's pan: core's own drag-to-navigate/close is gated off\n // while zoomed, but this engine has no idea — it still locks and\n // still captured, so the release still retargeted its click the same\n // way). `shouldCapture()` lets the caller skip capture for the whole\n // gesture in that case, not just its effect.\n if (this.callbacks.shouldCapture?.() ?? true) this.target.setPointerCapture(event.pointerId);\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 }\n\n this.primaryPointerId = null;\n this.direction = null;\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","export 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 — `target` is always its full flex-box size, never the (usually smaller, letterboxed) rendered photo inside it; see `effectiveTargetBox`. */\n aspectRatio?: number;\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\ninterface 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/attached yet. */\nfunction containedBox(container: Box, aspectRatio: number): Box {\n const containerRatio = container.width / container.height;\n const width = aspectRatio > containerRatio ? container.width : container.height * aspectRatio;\n const height = aspectRatio > containerRatio ? container.width / aspectRatio : container.height;\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(target: HTMLElement, aspectRatio?: number): 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);\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 */\nfunction computeTransform(\n origin: HTMLElement,\n target: HTMLElement,\n aspectRatio?: number,\n): string | null {\n const originRect = origin.getBoundingClientRect();\n const targetRect = effectiveTargetBox(target, aspectRatio);\n if (\n targetRect.width === 0 ||\n targetRect.height === 0 ||\n originRect.width === 0 ||\n originRect.height === 0\n ) {\n return null;\n }\n const scale = Math.min(\n originRect.width / targetRect.width,\n originRect.height / targetRect.height,\n );\n const translateX =\n originRect.left + originRect.width / 2 - (targetRect.left + targetRect.width / 2);\n const translateY =\n originRect.top + originRect.height / 2 - (targetRect.top + targetRect.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 * Waits for `target`'s own transform transition to end (with a safety-net\n * timeout in case transitionend never fires — an interrupted/removed\n * element, or a browser quirk) 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 */\nexport function waitForTransitionEnd(target: HTMLElement, cb: () => void): 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 === 'transform')\n 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 */\nfunction clearInlineTransform(target: HTMLElement, expectedTransform: string): void {\n target.style.transition = '';\n target.style.transformOrigin = '';\n target.style.willChange = '';\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 }: ZoomTransitionTarget): void {\n if (prefersReducedMotion()) return;\n const transform = computeTransform(origin, target, aspectRatio);\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 }: ZoomTransitionTarget,\n onComplete: () => void,\n): void {\n if (prefersReducedMotion()) {\n onComplete();\n return;\n }\n const transform = computeTransform(origin, target, aspectRatio);\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 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 } 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. Also used by `Gallery.ts`'s `isBackdropClick`,\n * which used to have its own narrower list (missing select/input/textarea/\n * a[href]) — a plugin-mounted non-button control got misread as a backdrop\n * click and closed the gallery. One shared selector so that can't recur.\n */\nexport const INTERACTIVE_CONTROL_SELECTOR =\n 'button, video, input, select, textarea, a[href], [data-shoji-no-drag]';\n\n/** A click/drag starting on a real control shouldn't also be captured as a gesture — see `INTERACTIVE_CONTROL_SELECTOR`. */\nfunction shouldIgnoreGesture(event: PointerEvent): boolean {\n return event\n .composedPath()\n .some((node) => node instanceof Element && node.matches(INTERACTIVE_CONTROL_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 slides: SlideManager;\n canGoNext(): boolean;\n canGoPrev(): boolean;\n next(): void;\n prev(): void;\n close(): 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\n constructor(\n private readonly host: GestureControllerHost,\n relay: GestureRelayCallbacks,\n options?: Partial<GestureEngineOptions>,\n ) {\n this.engine = new GestureEngine(\n host.dialog,\n {\n ignore: shouldIgnoreGesture,\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 /** Purely presentational drag feedback — scales/fades the dialog toward `close()`'s target state as the viewer drags, without closing until release decides the outcome. */\n private applyVerticalDragFeedback(delta: number): void {\n const progress = Math.min(Math.abs(delta) / VERTICAL_FEEDBACK_DISTANCE, 1);\n const dialog = this.host.dialog;\n dialog.style.transition = '';\n dialog.style.transform = `translateY(${delta}px) scale(${1 - progress * 0.15})`;\n dialog.style.opacity = String(1 - progress * 0.6);\n }\n\n private clearVerticalDragFeedback(animate: boolean): void {\n const dialog = this.host.dialog;\n dialog.style.transition = animate ? DRAG_FEEDBACK_TRANSITION : '';\n dialog.style.transform = '';\n dialog.style.opacity = '';\n }\n\n private finishVerticalDrag(completed: boolean): void {\n if (completed && this.host.canClose()) {\n // No settle animation of its own — clear the feedback instantly and\n // let close()'s own zoom-out transition (zoomTransition.ts) take over\n // as the single visible closing animation, rather than stacking two.\n this.clearVerticalDragFeedback(false);\n this.host.close();\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 }\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 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 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 { PLAY_ICON } from './icons';\nimport type { VideoProviderRenderer } from './plugin';\nimport type { GalleryItem } from './types';\n\n/** Native scrub-bar dragging genuinely pauses `<video>` for its duration — showing the play-overlay immediately on every `pause` would flash it on every seek. Long enough to swallow that, short enough a real pause doesn't feel delayed. */\nconst PAUSE_OVERLAY_DELAY_MS = 200;\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 /** Video play-overlay button, if any — travels with `node` across cache reuse. */\n extra?: HTMLElement;\n}\n\nexport interface SlideManagerOptions {\n preload: number;\n /** Play-overlay button's label. */\n playVideoLabel: string;\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 playVideoLabel: string;\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.playVideoLabel = options.playVideoLabel;\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. */\n render(\n items: readonly GalleryItem[],\n centerIndex: number,\n onLoad: (index: number) => void,\n openPlaceholderSrc?: string,\n ): void {\n // Trim the cache to the same centerIndex ± preload window the slots\n // cover — releases a dropped video here rather than just letting the\n // reference disappear (which would leave it paused-in-place, never\n // actually released).\n for (const [index, entry] of this.cache) {\n if (index < centerIndex - this.preload || index > centerIndex + this.preload) {\n releaseVideoNode(entry.node);\n this.cache.delete(index);\n }\n }\n\n const targets = this.slots.map((_, i) => {\n const offset = i - this.preload;\n const index = centerIndex + offset;\n const item = index >= 0 && index < items.length ? items[index] : undefined;\n return { offset, index, item };\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 if (index === centerIndex && openPlaceholderSrc) {\n this.revealOpenPlaceholder(openPlaceholderSrc, slot, index);\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 }\n\n /** Releases the slot's old content and swaps the new node in, caching it. `extra` rides as a second child. `ensureImageDecoding` goes through `moveIn` instead. */\n private swapIn(\n slot: Slot,\n node: HTMLElement,\n item: GalleryItem,\n index: number,\n extra?: HTMLElement,\n ): void {\n releaseVideo(slot.media);\n this.applyAspect(slot, item);\n if (extra) slot.media.replaceChildren(node, extra);\n else slot.media.replaceChildren(node);\n slot.ready = true;\n this.cache.set(index, { node, item, extra });\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 if (entry.extra) slot.media.replaceChildren(entry.node, entry.extra);\n else 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 /** DESIGN.md §2.3 — swaps the spinner for the placeholder once *it* decodes, not immediately: `item.thumb` is often just `item.src` again, so an undecoded placeholder can leave as long a blank gap as the spinner it replaces. */\n private revealOpenPlaceholder(src: string, slot: Slot, index: number): 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 the spinner, not slot.media wholesale — a provider video may\n // have already appended its own (still-hidden) container alongside it.\n slot.media.querySelector('.shoji-slide-spinner')?.remove();\n slot.media.appendChild(img);\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 if (item.poster) video.poster = item.poster;\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, this.createVideoPlayOverlay(video));\n const reveal = (): void => onLoad(index);\n video.addEventListener('loadedmetadata', reveal, { once: true });\n video.addEventListener('error', reveal, { once: true });\n }\n\n /** Tracks the video's own play/pause/ended state. Hide `.shoji-video-play-overlay` in CSS to opt out. */\n private createVideoPlayOverlay(video: HTMLVideoElement): HTMLElement {\n const overlay = document.createElement('button');\n overlay.type = 'button';\n overlay.className = 'shoji-video-play-overlay';\n overlay.innerHTML = PLAY_ICON;\n overlay.ariaLabel = overlay.title = this.playVideoLabel;\n overlay.hidden = !video.paused;\n\n let pauseTimer: ReturnType<typeof setTimeout> | null = null;\n const clearPauseTimer = (): void => {\n if (pauseTimer === null) return;\n clearTimeout(pauseTimer);\n pauseTimer = null;\n };\n\n overlay.addEventListener('click', (event) => {\n event.stopPropagation();\n // A broken/missing source (bad path, unsupported format) rejects with\n // NotSupportedError — a real, if unusual, host-data problem, not a\n // reason to crash with an unhandled rejection; the native controls\n // already show their own error state once the 'error' event fires.\n video.play().catch(() => {});\n });\n video.addEventListener('play', () => {\n clearPauseTimer();\n overlay.hidden = true;\n });\n video.addEventListener('pause', () => {\n clearPauseTimer();\n pauseTimer = setTimeout(() => {\n pauseTimer = null;\n if (video.paused) overlay.hidden = false;\n }, PAUSE_OVERLAY_DELAY_MS);\n });\n video.addEventListener('ended', () => {\n clearPauseTimer();\n overlay.hidden = false;\n });\n return overlay;\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 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, instead of deferring 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 (Phase 2's spinner, or an open placeholder) is\n // still sitting behind the now-visible embed — never the embed itself.\n slot.media.querySelector('.shoji-slide-spinner, .shoji-slide-open-placeholder')?.remove();\n this.applyAspect(slot, item);\n slot.ready = true;\n this.cache.set(index, { node: container, item });\n onLoad(index);\n };\n renderFn(container, item, onReady, controller.signal);\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 §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`/`mobileSettings.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(private readonly slides: SlideManager) {}\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 ghost.appendChild(outgoing.cloneNode(true) as HTMLElement);\n this.slides.element.appendChild(ghost);\n void ghost.offsetHeight; // commit the ghost's natural (untransformed) 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 waitForEnd(ghost, () => ghost.remove());\n waitForEnd(incoming, () => cleanupIncoming(incoming));\n }\n}\n","import { lockBodyScroll, unlockBodyScroll } 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, 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 } 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 playVideo: 'Play video',\n showCaption: 'Show caption',\n hideCaption: 'Hide caption',\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 * `.shoji-slide-provider-video` (a real bug, reported from real usage,\n * mobile-specific): a provider embed (YouTube) isn't a `<video>` element —\n * it's a plain `<div>` wrapping a cross-origin `<iframe>` — so it never\n * matched `INTERACTIVE_CONTROL_SELECTOR`'s `video` term the way a native\n * HTML5 video slide already did. `.shoji-toolbar`'s own empty space is\n * `pointer-events: none` (so it doesn't block the content underneath), so a\n * tap there falls through to this container and — without this exclusion —\n * read as \"clicked outside,\" closing the gallery on the exact same tap\n * that was meant to reveal auto-hidden controls again. Desktop rarely hit\n * this (hovering reveals controls without ever generating a `click`), but\n * on mobile every reveal attempt is a tap, and a tap is a click.\n */\nfunction isBackdropClick(event: Event): boolean {\n return !event\n .composedPath()\n .some(\n (node) =>\n node instanceof Element &&\n node.matches(\n `.shoji-slide-img, .shoji-slide-provider-video, .shoji-counter, .shoji-caption, ${INTERACTIVE_CONTROL_SELECTOR}`,\n ),\n );\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 private loop = true;\n private closable = true;\n private autoHideDelay = 5000;\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 private hoveredControlCount = 0;\n private isClosing = 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 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 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.close();\n };\n\n /** DESIGN.md §2.8 — any interaction re-shows controls, restarts the idle clock. `autoHideDelay: 0` = \"never show controls\" — a no-op here. */\n private readonly onActivity = (): void => {\n if (this.autoHideDelay === 0) return;\n this.showControls();\n this.scheduleAutoHide();\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 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 // 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 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 playVideoLabel: this.locale.playVideo,\n videoProviders: this.videoProviders,\n });\n this.transition = new SlideTransition(this.slides);\n const dom = buildLightboxDom(this.slides.element, this.locale);\n this.dom = dom;\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.outer.addEventListener('click', this.onOuterClick);\n dom.outer.addEventListener('pointermove', this.onActivity, { 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.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.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 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 canClose: () => this.closable,\n onActivity: () => this.onActivity(),\n isZoomed: () => this.zoomGate?.() ?? false,\n },\n {\n onTap: (x, y) => this.bus.emit('tap', { x, y }),\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 /** DESIGN.md §3 — plugins init here, not the constructor. `requires` checks names loaded earlier; an unmet one is skipped (logged), not thrown. Cleared up front so a `reinit()` doesn't inherit names from before. */\n private initPlugins(): void {\n this.activePluginNames.clear();\n this.videoProviders.clear();\n for (const plugin of this.options.plugins ?? []) {\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 const missing = (plugin.requires ?? []).filter((name) => !this.activePluginNames.has(name));\n if (missing.length > 0) {\n console.error(\n `Shoji: plugin \"${plugin.name}\" requires [${missing.join(', ')}] to be registered first — 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 close button\n * (never after), so close stays fixed rightmost and plugins cluster to\n * its left in registration order: `plugins: [A, B, C]` reads A, B, C,\n * close. 'left'/'center' are independent zones for a plugin that doesn't\n * 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.closeButton);\n } else {\n (slot === 'left' ? dom.toolbarLeft : dom.toolbarCenter).appendChild(node);\n }\n return () => {\n unhover?.();\n node.remove();\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 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 count 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 let hovering = false;\n const onEnter = (): void => {\n hovering = true;\n this.hoveredControlCount++;\n this.onActivity();\n };\n const onLeave = (): void => {\n hovering = false;\n this.hoveredControlCount--;\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 if (hovering) {\n hovering = false;\n this.hoveredControlCount--;\n this.scheduleAutoHide();\n }\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.hoveredControlCount > 0;\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 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 === 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 private hideControls(): void {\n if (!this.dom || this.autoHidden || this.isControlActive()) 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');\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 * 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 }\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 this.updateCaptionVisibility();\n }\n },\n openPlaceholderSrc,\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 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 this.renderCurrentSlide(this.resolveOpenPlaceholderSrc(this.itemList[index], origin));\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 this.applyMobileControlsSetting();\n\n const media = this.slides?.getActiveMedia();\n if (media && origin) {\n zoomIn({ origin, target: media, aspectRatio: this.resolveAspectRatio(index, origin) });\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 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 const swap = (): void => this.renderCurrentSlide();\n if (animate && this.transition) {\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 swap();\n }\n\n this.bus.emit('afterSlide', { from, to: clamped });\n }\n\n /** DESIGN.md §2.5 — `mobileSettings.mode` overrides `mode` on a coarse-pointer device, evaluated fresh each navigation. */\n private resolveTransitionMode(): string {\n if (this.isMobileQuery() && this.options.mobileSettings?.mode) {\n return this.options.mobileSettings.mode;\n }\n return this.options.mode ?? 'slide';\n }\n\n private isMobileQuery(): boolean {\n return (\n typeof window.matchMedia === 'function' && window.matchMedia('(pointer: coarse)').matches\n );\n }\n\n /**\n * DESIGN.md §2.5 — `mobileSettings.controls: false` starts controls\n * hidden on a coarse-pointer device, reusing the existing §2.8 auto-hide\n * mechanism (`hideControls()`) rather than a separate permanent-hide\n * state: auto-hide is opacity-only, never removed from the tab order,\n * and already reveals on any activity — a genuinely *permanent* hide\n * would strand a touch user with no way to ever reach Close.\n */\n private applyMobileControlsSetting(): void {\n if (this.isMobileQuery() && this.options.mobileSettings?.controls === false) {\n this.hideControls();\n }\n }\n\n close(): void {\n if (this.destroyed || !this.opened || this.isClosing) return;\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 const origin = this.getOriginElement(this.activeIndex);\n if (media && origin) {\n const aspectRatio = this.resolveAspectRatio(this.activeIndex, origin);\n zoomOut({ origin, target: media, aspectRatio }, () => this.finishClose());\n } else {\n this.finishClose();\n }\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 * 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 if (this.autoHideTimer !== null) {\n clearTimeout(this.autoHideTimer);\n this.autoHideTimer = null;\n }\n this.autoHidden = false;\n this.hoveredControlCount = 0;\n this.dom?.dialog.classList.remove('shoji-controls-hidden');\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 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.slides?.destroy();\n this.dom?.outer.remove();\n this.slides = null;\n this.dom = null;\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","import type { PluginContext, ShojiPlugin } from '../../core/plugin';\n\nexport interface ActiveThumbnailOptions {\n /** CSS class applied to the origin thumbnail for whichever slide is currently active. Default `'shoji-thumb-active'` — Shoji ships no default styling for it (the host's own thumbnail markup, or the layout plugin's tiles, define what \"active\" looks like), only the class toggling. */\n activeClass?: string;\n /** Scrolls the active thumbnail into view (`block: 'nearest'`) whenever it changes. Default `true`. */\n scrollIntoView?: boolean;\n}\n\nfunction prefersReducedMotion(): boolean {\n return (\n typeof window.matchMedia === 'function' &&\n window.matchMedia('(prefers-reduced-motion: reduce)').matches\n );\n}\n\n/**\n * Opt-in: keeps the host's thumbnail grid in sync with whichever slide is\n * currently open — highlighting it and, by default, scrolling it into view —\n * as the viewer moves through the lightbox by any means (arrow keys,\n * prev/next buttons, a completed swipe, autoplay ticking forward, a plugin\n * calling `goTo()`). Not core: CLAUDE.md's \"everything that can be a plugin\n * is a plugin\" — a host whose thumbnail grid is short enough to always be\n * fully visible, or who doesn't want their page scrolling out from under\n * them mid-slideshow, should be able to leave this off entirely.\n *\n * Reuses `Gallery.getOriginElement()` (the same index → thumbnail lookup the\n * zoom transition already relies on, §2.3b) rather than re-deriving it, so\n * this plugin's coverage exactly matches whatever the zoom transition\n * already zooms to/from: `scannedElements[index]` in selector mode,\n * `data-shoji-id=\"<item.id>\"` markers elsewhere (including the layout\n * plugin's own tiles, which set that attribute automatically when\n * `item.id` is present).\n */\nexport const ActiveThumbnail: ShojiPlugin = {\n name: 'activeThumbnail',\n defaults: {\n activeClass: 'shoji-thumb-active',\n scrollIntoView: true,\n } satisfies ActiveThumbnailOptions,\n\n init(ctx: PluginContext): () => void {\n const { gallery } = ctx;\n const activeClass = String(ctx.options.activeClass ?? 'shoji-thumb-active');\n const scrollIntoView = ctx.options.scrollIntoView !== false;\n\n let current: HTMLElement | null = null;\n let scrollTimer: ReturnType<typeof setTimeout> | null = null;\n\n function cancelPendingScroll(): void {\n if (scrollTimer !== null) {\n clearTimeout(scrollTimer);\n scrollTimer = null;\n }\n }\n\n function apply(index: number): void {\n const el = gallery.getOriginElement(index);\n if (current && current !== el) current.classList.remove(activeClass);\n cancelPendingScroll();\n if (el) {\n el.classList.add(activeClass);\n if (scrollIntoView) {\n // Debounced, not fired on every single navigation directly: a\n // real bug, reported from real usage and confirmed via direct\n // testing — navigating faster than a single smooth scroll can\n // finish (autoplay ticking, or just clicking quickly) leaves the\n // browser with an interrupted, not-yet-settled scroll animation,\n // which can visibly resolve later, at some unrelated later point\n // (e.g. exactly when the gallery closes) rather than simply being\n // superseded. Waiting a short beat before actually scrolling — and\n // restarting that wait on every subsequent navigation — means a\n // rapid burst only ever issues one real scrollIntoView call, for\n // wherever the viewer actually lands, instead of one *per step*\n // that can never keep up and pile up interrupted animations.\n // classList.add() above is unaffected either way — the highlight\n // itself is never debounced, only the scroll.\n scrollTimer = setTimeout(() => {\n scrollTimer = null;\n // Scrolling is best-effort — whatever goes wrong with it must\n // never take the highlight down with it (already applied above).\n try {\n el.scrollIntoView({\n block: 'nearest',\n inline: 'nearest',\n behavior: prefersReducedMotion() ? 'auto' : 'smooth',\n });\n } catch {\n // no-op — see comment above\n }\n }, 80);\n }\n }\n current = el;\n }\n\n // Cleared on close (not left highlighting a thumbnail the viewer isn't\n // looking at anymore) — reopening re-applies it from whatever index\n // open() lands on, via the 'afterOpen' listener below. Also cancels any\n // still-pending debounced scroll — nothing left to usefully scroll\n // toward once the highlight itself is about to be cleared, and this is\n // exactly what stops a debounced-but-not-yet-fired scroll from\n // surfacing later as an unexplained shift after the gallery is closed.\n function clear(): void {\n cancelPendingScroll();\n current?.classList.remove(activeClass);\n current = null;\n }\n\n const offOpen = ctx.on('afterOpen', ({ index }) => apply(index));\n const offSlide = ctx.on('afterSlide', ({ to }) => apply(to));\n const offClose = ctx.on('close', clear);\n\n return () => {\n offOpen();\n offSlide();\n offClose();\n clear();\n };\n },\n};\n","/** DESIGN.md §9 — inline SVG, stroke/fill = currentColor, matches src/core/icons.ts's convention. */\n\nexport const PLAY_ICON =\n '<svg viewBox=\"0 0 24 24\" width=\"24\" height=\"24\" fill=\"currentColor\"><path d=\"M8 5v14l11-7z\"/></svg>';\n\nexport const PAUSE_ICON =\n '<svg viewBox=\"0 0 24 24\" width=\"24\" height=\"24\" fill=\"currentColor\"><path d=\"M7 5h4v14H7zM13 5h4v14h-4z\"/></svg>';\n","import type { PluginContext, ShojiPlugin } from '../../core/plugin';\nimport { PAUSE_ICON, PLAY_ICON } from './icons';\nimport './autoplay.css';\n\n/**\n * A native `<video>` or a `.shoji-slide-provider-video` container a video\n * provider plugin (§4-video) has augmented with the same shape — Autoplay\n * never needs to know which. `play()`'s return type is technically a lie\n * for the provider case (that `.play` is a synchronous fire-and-forget, no\n * `HTMLVideoElement.play()`-style promise) — harmless, `enterSlide()`'s own\n * `typeof playResult.catch === 'function'` guard already treats a\n * non-promise `play()` result as \"nothing to await,\" same as `undefined`.\n */\ntype PlayableMedia = Pick<\n HTMLVideoElement,\n 'play' | 'pause' | 'paused' | 'ended' | 'muted' | 'addEventListener' | 'removeEventListener'\n>;\n\n/** `.shoji-slide-provider-video` only counts if it's actually been wired up as playable (§4-video's `wirePlayableContract`) — a provider still mid-async-setup, or one that never opted into Autoplay sync at all, isn't. */\nfunction findPlayable(media: HTMLElement | null): PlayableMedia | null {\n const video = media?.querySelector('video');\n if (video) return video;\n const provider = media?.querySelector<HTMLElement & Partial<PlayableMedia>>(\n '.shoji-slide-provider-video',\n );\n if (provider && typeof provider.play === 'function') return provider as PlayableMedia;\n return null;\n}\n\n/** A provider container is attached to the DOM immediately (§4-video), well before its async setup (loading an SDK, constructing a player) finishes wiring `.play` — this is what tells `enterSlide()` \"there's a video here, it's just not playable yet\" apart from \"no video at all,\" so it doesn't mistreat a still-loading video as an ordinary timed slide. */\nfunction isPendingProviderVideo(media: HTMLElement | null): boolean {\n const provider = media?.querySelector<HTMLElement & Partial<PlayableMedia>>(\n '.shoji-slide-provider-video',\n );\n return !!provider && typeof provider.play !== 'function';\n}\n\n// A provider embed's postMessage bridge (DESIGN.md §4.3) can need more real\n// time after \"ready\" before it reliably processes its first command — a\n// play() issued too soon can silently no-op with nothing to catch. Retries\n// a few times with a short delay instead of a single best-effort attempt.\nconst PROVIDER_PLAY_RETRY_MS = 400;\nconst MAX_PROVIDER_PLAY_ATTEMPTS = 8; // + the initial attempt = 9 total, ~3.6s before giving up\n\nexport interface AutoplayOptions {\n /** Milliseconds between advances for timed (photo) slides. Default `5000`. */\n interval?: number;\n /** Shows the thin progress bar (`--shoji-progress`) tracking time-to-next-advance along the dialog's bottom edge, for timed slides only — never shown during video slides, whose own runtime drives advancement instead. Default `true`. Purely presentational: turning it off doesn't change any timing, only whether it's drawn. */\n showProgress?: boolean;\n /** Starts the slideshow automatically as soon as the gallery opens — every `open()`, not just the first — instead of waiting for the toolbar button/`Space`. Default `false`. */\n autoStart?: boolean;\n}\n\n/**\n * DESIGN.md §4-autoplay. Advances on a fixed `interval` (default 5000ms) for\n * ordinary slides; for a video slide, plays it and waits for `ended` instead\n * — the interval never applies to video. A manual pause on that video pauses\n * the *slideshow* too (not just the video); manually resuming the video does\n * NOT resume the slideshow — that requires pressing the slideshow's own\n * play control again. See `enterSlide()`/`onVideoPause()` below for exactly\n * where each half of that rule lives.\n */\nexport const Autoplay: ShojiPlugin = {\n name: 'autoplay',\n defaults: { interval: 5000, showProgress: true } satisfies AutoplayOptions,\n\n init(ctx: PluginContext): () => void {\n const { gallery } = ctx;\n const interval = Number(ctx.options.interval ?? 5000);\n const showProgress = ctx.options.showProgress !== false;\n const autoStart = ctx.options.autoStart === true;\n const locale = ctx.options.locale as Partial<Record<'play' | 'pause', string>> | undefined;\n const playLabel = locale?.play ?? 'Play slideshow';\n const pauseLabel = locale?.pause ?? 'Pause slideshow';\n\n let playing = false;\n let timer: ReturnType<typeof setTimeout> | null = null;\n let currentVideo: PlayableMedia | null = null;\n let awaitingProviderVideo = false;\n\n // A real bug, regression: this used to capture gallery.getActiveMedia()\n // once here and listen on that node directly, back when a pool slot's\n // own offset never changed after construction (only its *content*\n // moved between slots) — so \"whichever slot has offset 0\" was always\n // the same physical node, safe to capture once. SlideManager's pool now\n // relabels a slot's offset in place instead (§2.3) — the node that\n // happened to be offset 0 when this plugin initialized can become a\n // neighbor after even one navigation, while a *different* node becomes\n // the active one, silently going unheard by this listener. Listening on\n // `ctx.ui.outer()` instead (the whole lightbox, never relabeled) still\n // catches a provider's own error event (§4-video's `onError`, dispatched\n // with `bubbles: true`) regardless of which slide it came from — the\n // `contains()` check below is what actually scopes it to the currently\n // active slide, re-resolved fresh on every error rather than trusting a\n // stale reference.\n const outer = ctx.ui.outer();\n function onVideoError(event: Event): void {\n if (!playing) return;\n const active = gallery.getActiveMedia();\n if (active && event.target instanceof Node && active.contains(event.target)) advance();\n }\n outer.addEventListener('error', onVideoError);\n\n const button = document.createElement('button');\n button.type = 'button';\n // shoji-autoplay-toggle: a stable hook for host code that needs to find\n // this button from outside (e.g. to pause the slideshow before opening\n // its own UI on top) — `title`/`aria-label` swap with `locale`, so\n // matching on those breaks silently the moment a host customizes it.\n button.className = 'shoji-toolbar-button shoji-autoplay-toggle';\n button.setAttribute('aria-label', playLabel);\n button.title = playLabel;\n button.innerHTML = PLAY_ICON;\n\n const progress = document.createElement('div');\n progress.className = 'shoji-autoplay-progress';\n progress.hidden = true;\n const progressBar = document.createElement('div');\n progressBar.className = 'shoji-autoplay-progress-bar';\n progress.appendChild(progressBar);\n\n function clearTimer(): void {\n if (timer !== null) {\n clearTimeout(timer);\n timer = null;\n }\n }\n\n function resetProgressBar(): void {\n if (!showProgress) return;\n progress.hidden = true;\n progressBar.style.transition = 'none';\n progressBar.style.width = '0%';\n }\n\n function runProgressBar(ms: number): void {\n if (!showProgress) return;\n progress.hidden = false;\n progressBar.style.transition = 'none';\n progressBar.style.width = '0%';\n void progressBar.offsetWidth; // commit the reset before transitioning\n progressBar.style.transition = `width ${ms}ms linear`;\n progressBar.style.width = '100%';\n }\n\n function setButtonState(isPlaying: boolean): void {\n playing = isPlaying;\n button.innerHTML = isPlaying ? PAUSE_ICON : PLAY_ICON;\n button.setAttribute('aria-label', isPlaying ? pauseLabel : playLabel);\n button.title = isPlaying ? pauseLabel : playLabel;\n }\n\n function onVideoEnded(): void {\n if (playing) advance();\n }\n\n // A real user pause, as opposed to the pause some browsers fire\n // immediately alongside 'ended' — `.ended` disambiguates the two.\n // Provider videos (§4.3's renderers) are responsible for holding their\n // own 'pause' dispatch back if it might just be a natural end arriving\n // slightly early — vimeo.ts's `PAUSE_DISPATCH_DELAY_MS` is why.\n function onVideoPause(): void {\n if (currentVideo?.ended) return;\n stop();\n }\n\n function detachVideo(): void {\n if (!currentVideo) return;\n currentVideo.removeEventListener('ended', onVideoEnded);\n currentVideo.removeEventListener('pause', onVideoPause);\n currentVideo = null;\n }\n\n // A provider embed (e.g. YouTube) is cross-origin — unlike native\n // <video>, its own autoplay policy requires a *direct* user gesture on\n // the embed itself, which an automatic play() arriving via this\n // timer/'ended'/slideItemLoad chain never has; it silently no-ops\n // rather than rejecting, so there's nothing to catch. Muting first is\n // what actually gets it to play — the viewer can still unmute via the\n // embed's own controls.\n //\n // A real bug, reported from real usage, found only once a second\n // promise-returning provider (Vimeo) existed to compare against:\n // re-issuing `play()`/`muted = true` on every retry — needed for\n // YouTube, whose fire-and-forget `playVideo()` can silently drop a\n // command issued before its postMessage bridge is fully settled — does\n // the opposite for a provider whose `play()` genuinely returns a\n // promise. Confirmed directly, isolated from Shoji entirely: a single,\n // uninterrupted `play()` call reliably started Vimeo playback in\n // ~1.6s, while calling it again every 400ms (this loop's original,\n // unconditional behavior) kept it stuck indefinitely — each new call\n // resets the progress the previous one had already made, so it never\n // gets an uninterrupted run long enough to actually start. (This is\n // also what point 12 below's retry-exhaustion was actually skipping\n // past: not a broken video, but this loop's own repeated calls\n // preventing it from ever finishing what the first one started.)\n // `video.play()`'s return type only *claims* to be a promise for every\n // provider (see the `PlayableMedia` doc comment above) — branching on\n // whether it genuinely is one is what lets each provider get the\n // retry behavior it actually needs from this one shared function.\n function ensureProviderPlaying(video: PlayableMedia, attemptsLeft: number): void {\n video.muted = true;\n const playResult = video.play();\n if (playResult && typeof playResult.then === 'function') {\n pollWithoutReissuing(video, attemptsLeft, playResult);\n } else {\n reissueOnEachRetry(video, attemptsLeft);\n }\n }\n\n /** Fire-and-forget `play()` (YouTube) — re-issues the command itself on every retry, per this function's own doc comment above. */\n function reissueOnEachRetry(video: PlayableMedia, attemptsLeft: number): void {\n setTimeout(() => {\n if (currentVideo !== video || !playing) return; // stale — slide changed, or already stopped\n if (!video.paused) return; // took effect\n if (attemptsLeft > 0) ensureProviderPlaying(video, attemptsLeft - 1);\n // Exhausted every retry after already muting first — muted\n // autoplay is essentially never blocked by policy, so this means\n // something is genuinely wrong with the embed, not that it's\n // waiting on a gesture. advance() (not stop()) so a slow/late\n // error report (real usage: YouTube's own error can arrive slower\n // than this retry window, e.g. Error 153) can't leave the\n // slideshow stuck if this fires first.\n else advance();\n }, PROVIDER_PLAY_RETRY_MS);\n }\n\n /** A genuine `play()` promise (e.g. Vimeo) — the command itself is only ever issued once (by the caller); this only re-checks `.paused` on the same schedule, over the same total budget, without ever calling `play()` again. */\n function pollWithoutReissuing(\n video: PlayableMedia,\n attemptsLeft: number,\n playResult: Promise<void>,\n ): void {\n // Swallowed deliberately: a rejection here isn't distinguished from\n // \"still pending\" — the .paused poll below is what actually decides\n // whether this took effect, on the same schedule regardless of how\n // the promise itself settles. An unhandled-rejection console warning\n // is the only cost, same tradeoff as the native <video> path below\n // taking a real .catch() instead when it needs to branch on *why*.\n playResult.catch(() => {});\n setTimeout(() => {\n if (currentVideo !== video || !playing) return;\n if (!video.paused) return;\n if (attemptsLeft > 0) pollWithoutReissuing(video, attemptsLeft - 1, playResult);\n else advance();\n }, PROVIDER_PLAY_RETRY_MS);\n }\n\n function enterSlide(): void {\n clearTimer();\n detachVideo();\n resetProgressBar();\n awaitingProviderVideo = false;\n if (!playing) return;\n\n const media = gallery.getActiveMedia();\n const video = findPlayable(media);\n if (video) {\n currentVideo = video;\n video.addEventListener('ended', onVideoEnded);\n video.addEventListener('pause', onVideoPause);\n if (video instanceof HTMLVideoElement) {\n const playResult = video.play();\n // A rejected play() has two different causes that look identical\n // here: the browser blocking an unmuted play() that isn't a direct\n // continuation of a user gesture (NotAllowedError — the video is\n // fine, it just needs the viewer's own click) or the video\n // genuinely being unplayable (NotSupportedError — a broken/missing\n // source, an unsupported format). Only the first pauses the\n // slideshow for the viewer to resolve by hand; anything else means\n // there's nothing to wait for, so it skips ahead instead.\n if (playResult && typeof playResult.catch === 'function') {\n playResult.catch((error: unknown) => {\n if (currentVideo !== video || !playing) return; // stale\n if (error instanceof DOMException && error.name === 'NotAllowedError') stop();\n else advance();\n });\n }\n } else {\n ensureProviderPlaying(video, MAX_PROVIDER_PLAY_ATTEMPTS);\n }\n return;\n }\n\n // A provider video (e.g. YouTube) still mid-setup isn't an ordinary\n // slide either — the slideItemLoad listener below re-enters once it's\n // actually playable. This timer is a fallback in case that never\n // happens (network failure, blocked, ...), so the slideshow can't\n // stall on it forever.\n awaitingProviderVideo = isPendingProviderVideo(media);\n runProgressBar(interval);\n timer = setTimeout(advance, interval);\n }\n\n function advance(): void {\n const before = gallery.currentIndex;\n gallery.next();\n // loop:false and next() was already at the last item — nothing left\n // to advance to; the 'afterSlide' handler below won't fire for a\n // no-op goTo(), so this is the only place that can catch it.\n if (gallery.currentIndex === before) stop();\n }\n\n function start(): void {\n if (playing) return;\n setButtonState(true);\n ctx.emit('autoplayStart', {});\n enterSlide();\n }\n\n function stop(): void {\n if (!playing) return;\n setButtonState(false);\n clearTimer();\n resetProgressBar();\n if (currentVideo && !currentVideo.paused) currentVideo.pause();\n detachVideo();\n ctx.emit('autoplayStop', {});\n }\n\n function toggle(): void {\n if (playing) stop();\n else start();\n }\n\n button.addEventListener('click', toggle);\n\n // 'right' — clusters immediately before the close button, per DESIGN.md §3.1.\n const removeButton = ctx.ui.toolbar('right', button);\n const removeProgress = showProgress ? ctx.ui.overlay(progress) : null;\n const removeShortcut = ctx.ui.registerShortcut(' ', toggle);\n // Detaches this plugin's own 'pause'/'ended' listeners from the outgoing\n // video before Gallery.navigate() pauses it (see Gallery.ts's comment on\n // this same event) — otherwise that pause is misread as the viewer\n // manually pausing, which stops the slideshow before the new slide's\n // afterSlide handler below ever runs. enterSlide() below also calls\n // detachVideo(), but by then it's too late for *this* transition; that\n // call is what handles the slide *after* this one instead.\n const offBeforeSlide = ctx.on('beforeSlide', () => detachVideo());\n // Any slide change — autoplay's own next(), or the viewer manually\n // navigating mid-slideshow via arrows/buttons/goTo() — re-enters here,\n // tearing down the previous slide's timer/video listeners and setting\n // up fresh ones for whatever is active now. advance() deliberately does\n // NOT call enterSlide() itself: next() already triggers this listener\n // synchronously, so calling it twice would double up the timer/video wiring.\n const offSlide = ctx.on('afterSlide', () => {\n if (playing) enterSlide();\n });\n // A provider video (§4-video) that was still mid-setup when enterSlide()\n // last ran — see awaitingProviderVideo there — becomes playable some\n // time after afterSlide already fired and gave up on it for this pass.\n // Re-enter once it's genuinely ready, scoped to the still-active index\n // so a slide the viewer has already moved past doesn't retroactively\n // hijack the timer.\n const offSlideItemLoad = ctx.on('slideItemLoad', ({ index }) => {\n if (playing && awaitingProviderVideo && index === gallery.currentIndex) enterSlide();\n });\n const offClose = ctx.on('close', () => stop());\n const offOpen = ctx.on('afterOpen', () => {\n if (autoStart) start();\n });\n\n return () => {\n stop();\n outer.removeEventListener('error', onVideoError);\n removeButton();\n removeProgress?.();\n removeShortcut();\n offBeforeSlide();\n offSlide();\n offSlideItemLoad();\n offOpen();\n offClose();\n };\n },\n};\n","/** DESIGN.md §9 — inline SVG, stroke = currentColor, matches src/core/icons.ts's convention. Four corner-arrow glyphs, a generic \"expand\"/\"collapse\" pair, not tied to any particular icon set. */\nexport const EXPAND_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=\"M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5\"/></svg>';\n\nexport const COMPRESS_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 4v5H4M15 4v5h5M9 20v-5H4M15 20v-5h5\"/></svg>';\n","import type { PluginContext, ShojiPlugin } from '../../core/plugin';\nimport { COMPRESS_ICON, EXPAND_ICON } from './icons';\n\n/**\n * Vendor-prefixed Fullscreen API surface (older Safari) — not in standard\n * `lib.dom.d.ts`, so this is a documented interop boundary (CLAUDE.md: `any`\n * only there). Everything else in this file uses the standard,\n * already-typed `Element.requestFullscreen`/`Document.exitFullscreen`/\n * `Document.fullscreenElement`/`Document.fullscreenEnabled`.\n */\ninterface WebkitFullscreenElement extends HTMLElement {\n webkitRequestFullscreen?: () => void;\n}\ninterface WebkitFullscreenDocument extends Document {\n webkitExitFullscreen?: () => void;\n webkitFullscreenElement?: Element | null;\n webkitFullscreenEnabled?: boolean;\n}\n\nfunction isSupported(): boolean {\n return !!(\n document.fullscreenEnabled || (document as WebkitFullscreenDocument).webkitFullscreenEnabled\n );\n}\n\nfunction currentFullscreenElement(): Element | null {\n return (\n document.fullscreenElement ??\n (document as WebkitFullscreenDocument).webkitFullscreenElement ??\n null\n );\n}\n\nfunction requestFullscreen(el: HTMLElement): void {\n const target = el as WebkitFullscreenElement;\n if (target.requestFullscreen) target.requestFullscreen().catch(() => {});\n else target.webkitRequestFullscreen?.();\n}\n\nfunction exitFullscreen(): void {\n const doc = document as WebkitFullscreenDocument;\n if (document.exitFullscreen) document.exitFullscreen().catch(() => {});\n else doc.webkitExitFullscreen?.();\n}\n\n/**\n * DESIGN.md §4 — native Fullscreen API toggle, `.shoji-outer` (`ctx.ui.outer()`,\n * the whole lightbox — backdrop, dialog, controls) is what goes fullscreen,\n * not just the slide media. No toolbar button at all — not a disabled one —\n * on a browser with no Fullscreen API support (checked once at `init()`;\n * historically Safari on iOS), rather than shipping a dead control.\n *\n * `fullscreenchange` (plus the `webkitfullscreenchange` vendor variant) is\n * the single source of truth for button state, not the click handler\n * directly — a real Fullscreen API `request`/`exit` call can be rejected\n * (permission, another element already fullscreen, etc.), and the browser's\n * own Escape-key fullscreen exit doesn't go through this plugin's click\n * handler at all. Reacting to the event instead of the request keeps the\n * button correct regardless of *why* fullscreen state changed.\n */\nexport const Fullscreen: ShojiPlugin = {\n name: 'fullscreen',\n\n init(ctx: PluginContext): (() => void) | void {\n if (!isSupported()) return;\n\n const outer = ctx.ui.outer();\n const locale = (ctx.gallery.options.locale ?? {}) as Record<string, string>;\n const enterLabel = locale.enterFullscreen ?? 'Enter fullscreen';\n const exitLabel = locale.exitFullscreen ?? 'Exit fullscreen';\n\n const button = document.createElement('button');\n button.type = 'button';\n button.className = 'shoji-toolbar-button';\n button.innerHTML = EXPAND_ICON;\n button.setAttribute('aria-label', enterLabel);\n button.title = enterLabel;\n button.setAttribute('aria-pressed', 'false');\n\n function isActive(): boolean {\n return currentFullscreenElement() === outer;\n }\n\n function setState(active: boolean): void {\n button.innerHTML = active ? COMPRESS_ICON : EXPAND_ICON;\n button.setAttribute('aria-label', active ? exitLabel : enterLabel);\n button.title = active ? exitLabel : enterLabel;\n button.setAttribute('aria-pressed', String(active));\n }\n\n button.addEventListener('click', () => {\n if (isActive()) exitFullscreen();\n else requestFullscreen(outer);\n });\n\n const onChange = (): void => {\n const active = isActive();\n setState(active);\n ctx.emit('fullscreenChange', { fullscreen: active });\n };\n document.addEventListener('fullscreenchange', onChange);\n document.addEventListener('webkitfullscreenchange', onChange);\n\n // 'right' — clusters immediately before the close button (DESIGN.md §3.1).\n const removeButton = ctx.ui.toolbar('right', button);\n\n // Browser-level Escape-to-exit-fullscreen behavior is inconsistent\n // across browsers (some exit fullscreen and still deliver the page its\n // own Escape keydown, some don't) — closing the gallery explicitly\n // exits fullscreen too, rather than risking the page getting stuck\n // fullscreen-on-nothing after the lightbox itself is gone.\n const offClose = ctx.on('close', () => {\n if (isActive()) exitFullscreen();\n });\n\n return () => {\n document.removeEventListener('fullscreenchange', onChange);\n document.removeEventListener('webkitfullscreenchange', onChange);\n removeButton();\n offClose();\n if (isActive()) exitFullscreen();\n };\n },\n};\n","/**\n * DESIGN.md §5.2 — deterministic, measure-free layout: positions come purely\n * from each tile's aspect ratio, never from measuring a loaded image (that's\n * exactly what causes the layout-jump problem most masonry libraries have).\n * Pure functions, no DOM — testable in isolation from rendering.\n */\n\nexport interface MasonryTile {\n /** Natural aspect ratio only — any unit works as long as width/height share one. */\n width: number;\n height: number;\n}\n\nexport interface MasonryPosition {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\nexport interface MasonryOptions {\n containerWidth: number;\n /** Gap *between* columns (horizontal). */\n gutterX: number;\n /** Gap *between* tiles stacked within a column (vertical). */\n gutterY: number;\n columns: number | 'auto';\n columnWidth: number;\n minColumnWidth: number;\n maxColumnWidth: number;\n fill: 'shortest' | 'ordered';\n}\n\nexport interface MasonryResult {\n positions: MasonryPosition[];\n columnCount: number;\n columnWidth: number;\n /** Per-column running height, incl. trailing gutterY — feed back in as `startHeights` to continue packing (e.g. an appended page) without relaying out earlier tiles. */\n columnHeights: number[];\n containerHeight: number;\n}\n\n/**\n * `'auto'` column count is `containerWidth / columnWidth`, floored — at odd\n * container widths that division can drift the *effective* per-column width\n * well past `columnWidth` before the count itself changes (e.g. 601px at a\n * 200px target floors to 3 columns of ~200px, but 999px floors to 4 columns\n * of ~250px). `minColumnWidth`/`maxColumnWidth` cap how far that drift goes\n * before the column count itself adjusts, so columns never go stringy-narrow\n * or stretch too wide at awkward container sizes. Only `gutterX` (the axis\n * columns are laid out along) factors into this — `gutterY` never affects\n * column count/width.\n */\nexport function computeColumnCount(options: MasonryOptions): number {\n const { containerWidth, gutterX, columns } = options;\n if (typeof columns === 'number') return Math.max(1, Math.floor(columns));\n\n const target = Math.max(\n 1,\n Math.floor((containerWidth + gutterX) / (options.columnWidth + gutterX)),\n );\n const maxAllowedByMin = Math.max(\n 1,\n Math.floor((containerWidth + gutterX) / (options.minColumnWidth + gutterX)),\n );\n const minRequiredByMax = Math.max(\n 1,\n Math.ceil((containerWidth + gutterX) / (options.maxColumnWidth + gutterX)),\n );\n // minRequiredByMax can exceed maxAllowedByMin at a pathological config\n // (minColumnWidth > maxColumnWidth) — minRequiredByMax wins rather than\n // silently violating maxColumnWidth in that case.\n return Math.max(minRequiredByMax, Math.min(target, maxAllowedByMin));\n}\n\nexport function computeColumnWidth(\n containerWidth: number,\n gutterX: number,\n columnCount: number,\n): number {\n return (containerWidth - gutterX * (columnCount - 1)) / columnCount;\n}\n\n/**\n * Packs `tiles` into columns. `startHeights` (one entry per column, from a\n * previous `MasonryResult.columnHeights`) continues packing after already-\n * placed tiles instead of relaying out from scratch — this is what makes an\n * infinite-scroll append O(new items): earlier tiles' positions never change.\n */\nexport function layoutMasonry(\n tiles: readonly MasonryTile[],\n options: MasonryOptions,\n startHeights?: readonly number[],\n): MasonryResult {\n const columnCount = computeColumnCount(options);\n const colWidth = computeColumnWidth(options.containerWidth, options.gutterX, columnCount);\n const columnHeights =\n startHeights && startHeights.length === columnCount\n ? [...startHeights]\n : new Array<number>(columnCount).fill(0);\n\n const positions: MasonryPosition[] = tiles.map((tile, i) => {\n const colIndex = options.fill === 'ordered' ? i % columnCount : indexOfShortest(columnHeights);\n const tileHeight = tile.width > 0 ? colWidth * (tile.height / tile.width) : colWidth;\n const x = colIndex * (colWidth + options.gutterX);\n const y = columnHeights[colIndex]!;\n columnHeights[colIndex] = y + tileHeight + options.gutterY;\n return { x, y, width: colWidth, height: tileHeight };\n });\n\n const containerHeight = tiles.length === 0 ? 0 : Math.max(...columnHeights) - options.gutterY;\n return { positions, columnCount, columnWidth: colWidth, columnHeights, containerHeight };\n}\n\nfunction indexOfShortest(heights: readonly number[]): number {\n let shortest = 0;\n for (let i = 1; i < heights.length; i++) {\n if (heights[i]! < heights[shortest]!) shortest = i;\n }\n return shortest;\n}\n\nexport interface MasonryHorizontalOptions {\n containerWidth: number;\n /** Gap between tiles packed rightward within a row. */\n gutterX: number;\n /** Gap *between* rows (vertical). */\n gutterY: number;\n /** Every row's exact, fixed height — never scaled/solved-for (that's what distinguishes this from `justified`, whose row heights are the free variable it adjusts to make each row's width land exactly on `containerWidth`; here, height is the fixed input and width is left wherever a row's tiles naturally end). */\n rowHeight: number;\n}\n\nexport interface MasonryHorizontalResult {\n positions: MasonryPosition[];\n rowCount: number;\n containerHeight: number;\n}\n\n/**\n * DESIGN.md §5.1 — packs tiles left-to-right into rows of `rowHeight` (each\n * tile scaled to that exact height at its own true aspect ratio, never\n * stretched/cropped), wrapping to a new row the moment the next tile would\n * push the current one past `containerWidth` — never stretching the\n * finished row to close the remaining gap. The result: every row is\n * *exactly* `rowHeight` tall (the fixed constraint), and every row's right\n * edge lands wherever its tiles' natural widths summed to (the free,\n * \"ragged\" one) — the direct masonry-family transpose of vertical masonry,\n * whose *columns* share a fixed width and whose ragged edge is the bottom.\n * `justified` (`justified.ts`) is the inverse tradeoff: width is the fixed\n * constraint (every row forced flush to `containerWidth`) and height is the\n * free variable solved per row to make that work.\n *\n * A single tile whose natural width at `rowHeight` alone already exceeds\n * `containerWidth` (a wide panorama) is still placed on its own row rather\n * than shrunk — nothing narrower to fall back to without violating the\n * fixed-height constraint, the same tradeoff `justified`'s `minRowHeight`\n * makes in its own analogous single-tile edge case.\n */\nexport function layoutMasonryHorizontal(\n tiles: readonly MasonryTile[],\n options: MasonryHorizontalOptions,\n): MasonryHorizontalResult {\n const { containerWidth, gutterX, gutterY, rowHeight } = options;\n const positions: MasonryPosition[] = new Array(tiles.length);\n let currentY = 0;\n let rowStart = 0;\n let rowWidth = 0; // cumulative width of tiles committed to the current row so far, gutters included\n let rowCount = 0;\n\n function tileWidth(tile: MasonryTile): number {\n return tile.height > 0 ? rowHeight * (tile.width / tile.height) : rowHeight;\n }\n\n function placeRow(start: number, endExclusive: number): void {\n let x = 0;\n for (let i = start; i < endExclusive; i++) {\n const w = tileWidth(tiles[i]!);\n positions[i] = { x, y: currentY, width: w, height: rowHeight };\n x += w + gutterX;\n }\n rowCount++;\n currentY += rowHeight + gutterY;\n }\n\n for (let i = 0; i < tiles.length; i++) {\n const w = tileWidth(tiles[i]!);\n const countInRow = i - rowStart;\n const widthIfIncluded = rowWidth + (countInRow > 0 ? gutterX : 0) + w;\n if (countInRow > 0 && widthIfIncluded > containerWidth) {\n placeRow(rowStart, i); // finalize the row without tile i — it starts the next one instead\n rowStart = i;\n rowWidth = w;\n } else {\n rowWidth = widthIfIncluded;\n }\n }\n if (rowStart < tiles.length) placeRow(rowStart, tiles.length);\n\n const containerHeight = tiles.length === 0 ? 0 : currentY - gutterY;\n return { positions, rowCount, containerHeight };\n}\n","/**\n * DESIGN.md §5.2 — Flickr/Google-Photos-style row filler: tiles are grouped\n * into rows, each row scaled as a whole (every tile in it keeps its own\n * aspect ratio, only the shared row height changes) until the row's total\n * width exactly matches the container — the opposite visual signature from\n * masonry: row *right edges* line up flush, row *heights* vary instead of\n * column bottoms varying. Pure, no DOM — same shape/testing pattern as\n * masonry.ts.\n */\n\nexport interface JustifiedTile {\n width: number;\n height: number;\n}\n\nexport interface JustifiedPosition {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\nexport interface JustifiedOptions {\n containerWidth: number;\n /** Gap between tiles within a row (horizontal). */\n gutterX: number;\n /** Gap *between* rows (vertical). */\n gutterY: number;\n /** Target row height — rows are greedily filled until adding the next tile would exceed containerWidth at this height, then the whole row is rescaled to fit exactly (so actual row heights end up *near*, not always exactly at, this value). */\n rowHeight: number;\n /**\n * Bounds on a row's *scaled* (fill-the-container) height — an unusual mix\n * of aspect ratios (very few tiles, or very narrow/wide ones, e.g. a lone\n * portrait tile stretched to fill the container width under\n * `lastRow: 'justify'`) can otherwise scale a row to an extreme height.\n * `maxRowHeight` clamping means that row no longer exactly fills\n * `containerWidth` at its capped height — it falls short of the right\n * edge instead, the same tradeoff masonry's `maxColumnWidth` already\n * makes for the analogous reason. `minRowHeight` never causes that kind\n * of overflow, though, unlike a naive clamp would: a row that would need\n * to go below `minRowHeight` to fit exactly instead sheds tiles off its\n * end (rolled into the next row) until what's left *does* fit at\n * `minRowHeight` — rows never extend past `containerWidth`. The one\n * unavoidable exception is a single tile so wide/panoramic that even\n * alone it can't satisfy both bounds at once (`minRowHeight` at its own\n * aspect ratio is inherently wider than the container) — there,\n * `minRowHeight` itself yields rather than the row overflowing. Neither\n * bound applies to `lastRow: 'left'`, which already uses `rowHeight` as\n * a fixed, unscaled height rather than deriving one (and, being the\n * left-over *under*-full case by definition, never overflows either).\n */\n minRowHeight: number;\n maxRowHeight: number;\n /** How to handle the trailing row when there aren't enough tiles left to naturally reach containerWidth: 'justify' stretches it like any other row (Flickr-style — the default); 'left' keeps it at natural rowHeight-scaled size, left-aligned, empty space on the right; 'hide' excludes it from `positions` (returned as `null`) entirely. */\n lastRow: 'justify' | 'left' | 'hide';\n}\n\nexport interface JustifiedRow {\n /** Tile index range finalized into this row, `[start, end)`. */\n start: number;\n end: number;\n y: number;\n height: number;\n}\n\nexport interface JustifiedResult {\n /** One entry per input tile, same order — `null` only for a `lastRow: 'hide'`-excluded trailing tile. */\n positions: Array<JustifiedPosition | null>;\n containerHeight: number;\n /** One entry per finalized row, in order — lets a caller find which tiles landed on the same row (e.g. to decide where a compact section label belongs) without re-deriving it from `positions[i].y` equality. */\n rows: JustifiedRow[];\n}\n\nfunction aspectRatioOf(tile: JustifiedTile): number {\n return tile.width > 0 && tile.height > 0 ? tile.width / tile.height : 1;\n}\n\nexport function layoutJustified(\n tiles: readonly JustifiedTile[],\n options: JustifiedOptions,\n): JustifiedResult {\n const { containerWidth, gutterX, gutterY, rowHeight, minRowHeight, maxRowHeight, lastRow } =\n options;\n const positions: Array<JustifiedPosition | null> = new Array(tiles.length).fill(null);\n const rows: JustifiedRow[] = [];\n let currentY = 0;\n let rowStart = 0;\n let aspectSum = 0;\n\n function naturalHeightOf(startIndex: number, endExclusive: number, sum: number): number {\n const availableWidth = containerWidth - gutterX * (endExclusive - startIndex - 1);\n return availableWidth / sum;\n }\n\n function placeRow(startIndex: number, endExclusive: number, height: number): void {\n let x = 0;\n for (let i = startIndex; i < endExclusive; i++) {\n const width = height * aspectRatioOf(tiles[i]!);\n positions[i] = { x, y: currentY, width, height };\n x += width + gutterX;\n }\n rows.push({ start: startIndex, end: endExclusive, y: currentY, height });\n currentY += height + gutterY;\n }\n\n /**\n * Resolves `[startIndex, endExclusive)` (whose combined aspect ratio is\n * `sum`) into its final row, shedding tiles off the end — rolled into\n * whatever comes after — if honoring `minRowHeight` would otherwise push\n * this row's width past `containerWidth`. Returns the `endExclusive`\n * actually used, so the caller knows how many tiles (if any) got rolled\n * over and need to start the next row instead.\n */\n function resolveScaledRow(startIndex: number, endExclusive: number, sum: number): number {\n let end = endExclusive;\n let rowSum = sum;\n while (end - startIndex > 1 && naturalHeightOf(startIndex, end, rowSum) < minRowHeight) {\n end--;\n rowSum -= aspectRatioOf(tiles[end]!);\n }\n const natural = naturalHeightOf(startIndex, end, rowSum);\n // Down to one tile and *still* under minRowHeight: that single tile's\n // own aspect ratio makes minRowHeight-at-full-width inherently wider\n // than the container — nothing left to shed, so minRowHeight yields\n // (the natural height keeps width exactly at containerWidth) rather\n // than ever letting the row overflow.\n const height =\n end - startIndex === 1 && natural < minRowHeight\n ? natural\n : Math.min(Math.max(natural, minRowHeight), maxRowHeight);\n placeRow(startIndex, end, height);\n return end;\n }\n\n for (let i = 0; i < tiles.length; i++) {\n aspectSum += aspectRatioOf(tiles[i]!);\n const count = i - rowStart + 1;\n const widthAtTargetHeight = rowHeight * aspectSum + gutterX * (count - 1);\n if (widthAtTargetHeight >= containerWidth) {\n const end = resolveScaledRow(rowStart, i + 1, aspectSum);\n rowStart = end;\n aspectSum = 0;\n for (let j = end; j <= i; j++) aspectSum += aspectRatioOf(tiles[j]!); // rolled-over tiles seed the next row\n }\n }\n\n if (rowStart < tiles.length) {\n if (lastRow === 'justify') {\n // resolveScaledRow can itself shed tiles (the same overflow-avoidance\n // as the main loop above) — unlike there, there's no \"next row\" this\n // far down for the outer loop to naturally roll them into, so keep\n // resolving whatever's left until every trailing tile lands\n // somewhere. Always makes forward progress (resolveScaledRow places\n // at least one tile per call), so this always terminates.\n let start = rowStart;\n while (start < tiles.length) {\n let sum = 0;\n for (let j = start; j < tiles.length; j++) sum += aspectRatioOf(tiles[j]!);\n start = resolveScaledRow(start, tiles.length, sum);\n }\n } else if (lastRow === 'left') {\n placeRow(rowStart, tiles.length, rowHeight);\n }\n // 'hide': positions stay null for this trailing range, currentY doesn't advance\n }\n\n const containerHeight = tiles.length === 0 ? 0 : Math.max(0, currentY - gutterY);\n return { positions, containerHeight, rows };\n}\n","import type { PluginContext, ShojiPlugin } from '../../core/plugin';\nimport type { GalleryItem } from '../../core/types';\nimport { layoutMasonry, layoutMasonryHorizontal, type MasonryTile } from './masonry';\nimport { layoutJustified, type JustifiedOptions, type JustifiedResult } from './justified';\nimport './layout.css';\n\n/**\n * DESIGN.md §5 — `type: 'grid' | 'masonry' | 'justified'`; masonry supports\n * both `orientation`s, `breakpoints` (§5.4), and `groupBy`/headings (§5.2;\n * see that option's own doc comment for the v1 scope cuts vs. the full\n * spec, including why `groupBy` itself is excluded from `orientation:\n * 'horizontal'`). Deliberately still\n * deferred: jump-free *prepend* coordination (§5.2's scroll-anchoring\n * handoff needs the not-yet-built scroll plugin on the other end —\n * prepending today triggers a full relayout, which can shift scroll\n * position if content above the fold grew).\n */\nexport interface LayoutOptions {\n /** Default `'justified'`. */\n type?: 'grid' | 'masonry' | 'justified';\n gutter?: number | { x: number; y: number };\n /**\n * `type: 'grid'` only — every cell's shape, cropped to fit via\n * `object-fit: cover`; default 1 (square). Deliberately *not* each\n * photo's own aspect ratio: plain CSS grid sizes a row's height to its\n * tallest cell, so per-item ratios leave a visible blank gap under every\n * shorter tile sharing a row with a taller one. Masonry/justified are\n * the two types that preserve true per-photo aspect ratios instead (via\n * their own JS packing, which doesn't have this row-height problem).\n */\n aspectRatio?: number;\n /**\n * `type: 'masonry'` only. `'vertical'` (default): fixed-width columns,\n * tiles stack top-to-bottom, ragged *bottom* edge. `'horizontal'`: fixed-\n * height rows, tiles pack left-to-right, ragged *right* edge — the direct\n * transpose. Both wrap/grow within the container and scroll vertically\n * only, same as every other layout type; neither ever needs horizontal\n * scroll. See `rowHeight` for `'horizontal'`'s one fixed input, and\n * `justified` (a different `type` entirely) for the *opposite* tradeoff —\n * width forced flush, height left to solve for.\n */\n orientation?: 'vertical' | 'horizontal';\n columns?: number | 'auto';\n columnWidth?: number;\n minColumnWidth?: number;\n maxColumnWidth?: number;\n fill?: 'shortest' | 'ordered';\n /** `type: 'justified'`, or `type: 'masonry', orientation: 'horizontal'`. For horizontal masonry this is the row's exact, fixed height (see `orientation`'s doc comment) — `minRowHeight`/`maxRowHeight` don't apply there, since nothing is solved/clamped, only for justified, where it's a *target* a solved-for height converges near. */\n rowHeight?: number;\n minRowHeight?: number;\n maxRowHeight?: number;\n lastRow?: 'justify' | 'left' | 'hide';\n animate?: boolean;\n /**\n * Container-width → partial overrides, applied on top of the base options\n * above whenever the container's current width is <= that key (the\n * *narrowest* matching key wins if more than one applies). Re-evaluated on\n * every relayout, so crossing a threshold while resizing takes effect\n * live. Cannot override `type` or `orientation` — swapping the DOM\n * structure/positioning strategy live is a bigger feature than a sizing\n * tweak; use a media query + two `Shoji` instances if that's genuinely\n * needed.\n */\n breakpoints?: Record<number, Omit<LayoutOptions, 'type' | 'orientation' | 'breakpoints'>>;\n /**\n * Derives a section key per item (e.g. a formatted date) — a heading is\n * inserted whenever the key changes between consecutive items, real\n * \"Today / Yesterday / March 2024\" (Google Photos-style) sectioning.\n * Items must already be grouped consecutively; a key reappearing later\n * after a different key started a new section anyway.\n *\n * Section boundaries behave differently per `type`: `grid`'s heading is a\n * full-span (`grid-column: 1 / -1`) element, native CSS grid gives it its\n * own row for free. `masonry`'s heading is a full-width blocking element\n * too — each section is its own fresh column-packing pass, no two\n * sections ever share visual space. `justified` is the odd one out, by\n * design (this is the actual Google Photos behavior, not a simplified\n * approximation of it): sections do **not** force a row break. Row\n * *composition* ignores section boundaries entirely — one continuous\n * `layoutJustified` pass packs every item regardless of section, so a\n * section with too few items to reach a full row's target width (a\n * single lone photo, say) shares that row with whatever comes right\n * after it, instead of getting stretched into its own awkward\n * almost-empty row. A heading only marks *where* a new section starts:\n * a compact, inline label sized to its own content (not full width),\n * positioned at the x-coordinate of the tile that starts its section,\n * in a slim label-band above whichever row that tile landed on — more\n * than one label can share the same band if more than one section\n * happens to start within the same row (see `renderHeading`'s\n * `{title, subtitle}` form for exactly this — that's what the reference\n * behavior actually looks like).\n *\n * v1 scope cuts vs. DESIGN.md §5.2's full spec: (1) any items change\n * while `groupBy` is set does a full relayout, same simplification\n * justified's non-grouped path already makes for appends (§5.4) — the\n * \"merge across load boundaries\" incremental optimization is deferred;\n * (2) headings are always `<h2>`, not a configurable level; (3) not\n * supported with `orientation: 'horizontal'` (warns, ignored) —\n * DESIGN.md itself calls that pairing rare.\n */\n groupBy?: (item: GalleryItem) => string;\n /**\n * Customizes a heading's content; default renders the key as plain text.\n * Three return shapes:\n * - `string` — plain text (default behavior, just with custom content).\n * - `HTMLElement` — full control, own tag/children; the plugin adds its\n * own class/positioning on top of whatever's returned, nothing else.\n * - `{ title: string; subtitle?: string }` — the built-in structured\n * form (bold title + a smaller, muted `subtitle`, e.g. a date + a\n * location) — the only form the plugin can apply automatic overflow\n * handling to (see `headingOverflow`), since it owns the DOM structure\n * (a raw `HTMLElement` is opaque to it, no safe way to guess what's\n * droppable).\n */\n renderHeading?: (\n key: string,\n items: GalleryItem[],\n ) => string | HTMLElement | { title: string; subtitle?: string };\n /**\n * `type: 'justified'` + the `{ title, subtitle }` structured heading\n * form only — controls what happens when a compact label doesn't fit\n * its available width (the gap until the next section's label sharing\n * its row, or the container edge). Ignored otherwise: `grid`/`masonry`\n * headings are full-width and this kind of squeeze can't happen there;\n * a raw `renderHeading`-returned `HTMLElement`/`string` is left exactly\n * as returned regardless, since the plugin has no safe way to guess\n * what's droppable in an opaque element.\n * - `'show'` (default) — always render the full title + subtitle, one\n * line, however wide; a label can visually run past the next\n * section's label or the container edge if the content is long\n * enough. The simple, predictable default — nothing gets silently\n * dropped or reflowed.\n * - `'fit'` — never wraps a label's text. First the subtitle is dropped\n * if title+subtitle together don't fit; if the title *alone* still\n * doesn't fit next to whatever's sharing its row, that section is\n * pushed to start a fresh row instead, giving its label the room it\n * needs — row *composition* responds to label width, not just label\n * content. Real Google Photos behavior; opt in when labels regularly\n * need to share tight row space. The one unavoidable exception is a\n * single label wider than the entire container even alone on its own\n * row — there, nothing left to break onto, so the label is\n * ellipsis-truncated (still one line, never wrapped, never\n * overflowing).\n * - `'wrap'` — the in-between: same continuous row-packing as `'show'`\n * (row *composition* never reacts to label width, unlike `'fit'` — no\n * section is ever pushed to a fresh row), but a label that doesn't fit\n * its available width (same gap `'fit'` measures) wraps its own text\n * onto additional lines instead of running past it. Height is capped\n * at `maxRowHeight` (or its default, 360) — a label tall enough to hit\n * that cap ellipsis-truncates its last visible line rather than\n * growing the row further. Real Google Photos behavior for its date\n * headers; pick this over `'fit'` when you'd rather a label take an\n * extra line than shove a whole section onto its own row.\n */\n headingOverflow?: 'show' | 'fit' | 'wrap';\n /**\n * Pins the active section's heading via `position: sticky`. Only\n * implemented for `type: 'grid'`, where headings sit in normal document\n * flow — masonry/justified headings are absolutely positioned by JS (like\n * their tiles), and `position: sticky` doesn't do anything meaningful\n * layered onto an already explicitly-positioned element. Warns and is\n * ignored for those two types.\n */\n stickyHeadings?: boolean;\n /**\n * When an item is missing `width`/`height`, measure its tile's own `<img>`\n * once it loads (`naturalWidth`/`naturalHeight`) and correct that tile's\n * aspect ratio in place, rather than leaving it at the permanent 4:3\n * placeholder fallback. Default `true`.\n *\n * The *first* layout pass never waits on this — it always uses the 4:3\n * placeholder immediately, same as before this option existed, so opening\n * the gallery is never blocked on image loads (DESIGN.md §5.2's\n * never-measure-mid-layout-pass rule still holds for that first pass).\n * This only ever *corrects* a placeholder after the fact, once a real\n * measurement lands — the same \"async correction after an initial\n * synchronous pass\" pattern already used for a `containerWidth <= 0`\n * container (§5.4) and the zoom transition's origin-rect race (§2.3b),\n * extended to per-item dimensions. Multiple corrections landing close\n * together (e.g. several cached images resolving in the same tick) are\n * coalesced into one relayout via `requestAnimationFrame`, not one\n * relayout per image.\n *\n * Effectively does automatically, for `thumb`/`poster`/`src`, what a host\n * would otherwise have to do by hand before constructing the gallery\n * (`new Image(); img.onload = () => ...`) — set `false` to keep the old\n * behavior (permanent 4:3 fallback, no self-correction) if you'd rather\n * guarantee zero extra relayouts under any circumstance.\n */\n autoMeasure?: boolean;\n}\n\ninterface ResolvedConfig {\n gutter: { x: number; y: number };\n columns: number | 'auto';\n columnWidth: number;\n minColumnWidth: number;\n maxColumnWidth: number;\n fill: 'shortest' | 'ordered';\n rowHeight: number;\n minRowHeight: number;\n maxRowHeight: number;\n lastRow: 'justify' | 'left' | 'hide';\n}\n\nconst DEFAULT_ASPECT: MasonryTile = { width: 4, height: 3 };\n\ninterface Tile {\n index: number;\n root: HTMLAnchorElement;\n /** The tile's own thumbnail `<img>` — reused by `autoMeasure` to read `naturalWidth`/`naturalHeight` once it loads, rather than fetching the same image a second time with a separate `Image()`. */\n img: HTMLImageElement;\n}\n\ninterface HeadingEntry {\n root: HTMLElement;\n /** index into `tiles[]` (== item index) where this section's tiles start. */\n tileStart: number;\n /** Present only for the built-in `{ title, subtitle }` form — the two elements `applyJustified`'s overflow handling is allowed to hide/wrap, since the plugin itself built them (a raw `renderHeading`-returned `HTMLElement` is opaque; nothing is touched there). */\n structured: { titleEl: HTMLElement; subtitleEl: HTMLElement | null } | null;\n}\n\n/** `gutter: 8` means `{x: 8, y: 8}` — a single number applies to both axes; `{x, y}` sets them independently. */\nfunction resolveGutter(raw: unknown): { x: number; y: number } {\n if (raw && typeof raw === 'object') {\n const obj = raw as { x?: number; y?: number };\n return { x: Number(obj.x ?? 8), y: Number(obj.y ?? 8) };\n }\n const n = Number(raw ?? 8);\n return { x: n, y: n };\n}\n\nfunction keyOf(item: GalleryItem): string {\n return item.id ?? item.src;\n}\n\n/** `newItems` is a superset of `oldItems` sharing an identical prefix — i.e. purely appended, nothing reordered/removed/prepended. */\nfunction isPureAppend(oldItems: readonly GalleryItem[], newItems: readonly GalleryItem[]): boolean {\n if (newItems.length <= oldItems.length) return false;\n for (let i = 0; i < oldItems.length; i++) {\n if (keyOf(oldItems[i]!) !== keyOf(newItems[i]!)) return false;\n }\n return true;\n}\n\nexport const Layout: ShojiPlugin = {\n name: 'layout',\n defaults: {\n type: 'justified',\n gutter: 8,\n columns: 'auto',\n columnWidth: 240,\n minColumnWidth: 160,\n maxColumnWidth: 480,\n fill: 'shortest',\n animate: true,\n autoMeasure: true,\n },\n\n init(ctx: PluginContext): () => void {\n const { gallery } = ctx;\n const type = (ctx.options.type as 'grid' | 'masonry' | 'justified' | undefined) ?? 'justified';\n const gutter = resolveGutter(ctx.options.gutter);\n const aspectRatio = Number(ctx.options.aspectRatio ?? 1);\n const orientation =\n (ctx.options.orientation as 'vertical' | 'horizontal' | undefined) ?? 'vertical';\n const columns = (ctx.options.columns as number | 'auto' | undefined) ?? 'auto';\n const columnWidth = Number(ctx.options.columnWidth ?? 240);\n const minColumnWidth = Number(ctx.options.minColumnWidth ?? 160);\n const maxColumnWidth = Number(ctx.options.maxColumnWidth ?? 480);\n const fill = (ctx.options.fill as 'shortest' | 'ordered' | undefined) ?? 'shortest';\n const rowHeight = Number(ctx.options.rowHeight ?? 220);\n const minRowHeight = Number(ctx.options.minRowHeight ?? 120);\n const maxRowHeight = Number(ctx.options.maxRowHeight ?? 360);\n const lastRow = (ctx.options.lastRow as 'justify' | 'left' | 'hide' | undefined) ?? 'justify';\n const animate = ctx.options.animate !== false;\n const autoMeasureEnabled = ctx.options.autoMeasure !== false;\n const horizontal = type === 'masonry' && orientation === 'horizontal';\n const groupByFn = ctx.options.groupBy as ((item: GalleryItem) => string) | undefined;\n const renderHeadingFn = ctx.options.renderHeading as\n | ((\n key: string,\n items: GalleryItem[],\n ) => string | HTMLElement | { title: string; subtitle?: string })\n | undefined;\n const headingOverflow =\n (ctx.options.headingOverflow as 'show' | 'fit' | 'wrap' | undefined) ?? 'show';\n const stickyHeadingsRaw = ctx.options.stickyHeadings === true;\n const stickyHeadings = stickyHeadingsRaw && type === 'grid';\n if (stickyHeadingsRaw && type !== 'grid') {\n console.warn(\n \"Shoji: layout plugin — stickyHeadings is only implemented for type: 'grid'; ignored (see the option's doc comment).\",\n );\n }\n if (groupByFn && horizontal) {\n console.warn(\n \"Shoji: layout plugin — groupBy isn't supported with orientation: 'horizontal'; ignored.\",\n );\n }\n const groupingEnabled = !!groupByFn && !horizontal;\n\n const baseConfig: ResolvedConfig = {\n gutter,\n columns,\n columnWidth,\n minColumnWidth,\n maxColumnWidth,\n fill,\n rowHeight,\n minRowHeight,\n maxRowHeight,\n lastRow,\n };\n\n const breakpointsRaw = ctx.options.breakpoints as\n Record<number, Partial<LayoutOptions>> | undefined;\n const sortedBreakpoints = breakpointsRaw\n ? Object.entries(breakpointsRaw)\n .map(([key, overrides]) => [Number(key), overrides] as const)\n .sort((a, b) => a[0] - b[0])\n : [];\n\n /** Re-run on every relayout — the narrowest breakpoint whose key is still >= the current width wins; falls back to `baseConfig` if none match or no breakpoints were given. */\n function resolveConfig(width: number): ResolvedConfig {\n const match = sortedBreakpoints.find(([maxWidth]) => width <= maxWidth);\n if (!match) return baseConfig;\n const overrides = match[1];\n return {\n gutter:\n overrides.gutter !== undefined ? resolveGutter(overrides.gutter) : baseConfig.gutter,\n columns: overrides.columns ?? baseConfig.columns,\n columnWidth: overrides.columnWidth ?? baseConfig.columnWidth,\n minColumnWidth: overrides.minColumnWidth ?? baseConfig.minColumnWidth,\n maxColumnWidth: overrides.maxColumnWidth ?? baseConfig.maxColumnWidth,\n fill: overrides.fill ?? baseConfig.fill,\n rowHeight: overrides.rowHeight ?? baseConfig.rowHeight,\n minRowHeight: overrides.minRowHeight ?? baseConfig.minRowHeight,\n maxRowHeight: overrides.maxRowHeight ?? baseConfig.maxRowHeight,\n lastRow: overrides.lastRow ?? baseConfig.lastRow,\n };\n }\n\n const container = gallery.element;\n container.classList.add('shoji-layout', `shoji-layout--${type}`);\n if (animate) container.classList.add('shoji-layout--animate');\n\n let tiles: Tile[] = [];\n let headings: HeadingEntry[] = [];\n let columnHeights: number[] | undefined;\n let previousItems: readonly GalleryItem[] = [];\n let warnedMissingDims = false;\n /**\n * A real bug, reported from real usage: `autoMeasureTiles()` used to\n * write its measurement straight onto `item.width`/`item.height` —\n * convenient, since `aspectOf()` already reads those first, but wrong.\n * That measurement comes from the *tile's* own `<img>`\n * (`measurableSrc()`: `item.thumb` first, then `poster`, then `src`) —\n * for layout purposes a thumbnail's aspect ratio is a fine stand-in for\n * the full photo's, close enough almost always. But `item.width`/\n * `height` aren't Layout-private state — RotateFlip's own fit-scale\n * (§4.5) and `SlideManager`'s aspect-ratio loading placeholder both\n * read the exact same fields, under the contract that they mean the\n * full photo's *true* pixel dimensions, not a thumbnail's. A thumbnail\n * genuinely smaller than the lightbox in both orientations always\n * satisfies `fitScaleFor`'s `Math.min(1, ...)` cap on its own, so\n * RotateFlip silently computed a no-op 1x scale for every such\n * item — confirmed directly against a real photo whose thumbnail\n * happened to be a separate, smaller file (an item whose thumbnail\n * happens to equal its full `src` never showed this, since then the\n * \"wrong\" measurement and the real one are simply the same numbers).\n * This cache keeps the auto-measurement entirely Layout-private —\n * `aspectOf()` below still benefits from it, but nothing outside this\n * plugin ever sees a thumbnail's dimensions mistaken for the photo's.\n */\n const measuredAspect = new WeakMap<GalleryItem, MasonryTile>();\n\n /** [startIndex, endExclusive) into `items`/`tiles` per section, split wherever `groupByFn`'s key changes. */\n function computeSections(\n items: readonly GalleryItem[],\n ): Array<{ key: string; startIndex: number; endIndex: number }> {\n const sections: Array<{ key: string; startIndex: number; endIndex: number }> = [];\n for (let i = 0; i < items.length; i++) {\n const key = groupByFn!(items[i]!);\n const current = sections[sections.length - 1];\n if (!current || current.key !== key) {\n sections.push({ key, startIndex: i, endIndex: i + 1 });\n } else {\n current.endIndex = i + 1;\n }\n }\n return sections;\n }\n\n function createHeadingRoot(\n key: string,\n sectionItems: GalleryItem[],\n ): { root: HTMLElement; structured: HeadingEntry['structured'] } {\n const rendered = renderHeadingFn ? renderHeadingFn(key, sectionItems) : key;\n let root: HTMLElement;\n let structured: HeadingEntry['structured'] = null;\n if (typeof rendered === 'string') {\n root = document.createElement('h2');\n root.textContent = rendered;\n } else if (rendered instanceof HTMLElement) {\n root = rendered;\n } else {\n root = document.createElement('h2');\n const titleEl = document.createElement('span');\n titleEl.className = 'shoji-layout-heading-title';\n titleEl.textContent = rendered.title;\n root.appendChild(titleEl);\n let subtitleEl: HTMLElement | null = null;\n if (rendered.subtitle) {\n subtitleEl = document.createElement('span');\n subtitleEl.className = 'shoji-layout-heading-subtitle';\n subtitleEl.textContent = rendered.subtitle;\n root.appendChild(subtitleEl);\n }\n structured = { titleEl, subtitleEl };\n }\n root.classList.add('shoji-layout-heading');\n if (stickyHeadings) root.classList.add('shoji-layout-heading--sticky');\n return { root, structured };\n }\n\n /** Segment boundaries derived from `headings` — `end` is the next heading's `tileStart`, or `tiles.length` for the last one. */\n function headingSegments(): Array<{ heading: HeadingEntry; start: number; end: number }> {\n return headings.map((heading, i) => ({\n heading,\n start: heading.tileStart,\n end: i + 1 < headings.length ? headings[i + 1]!.tileStart : tiles.length,\n }));\n }\n\n /** `undefined` for a video item with neither `thumb` nor `poster` — its tile `<img>` ends up pointed at the unplayable video file itself, which can never load as an image, so there's nothing `autoMeasure` could learn from it either. */\n function measurableSrc(item: GalleryItem): string | undefined {\n return item.thumb ?? item.poster ?? (item.video ? undefined : item.src);\n }\n\n function aspectOf(item: GalleryItem | undefined): MasonryTile {\n if (item?.width && item.height) return { width: item.width, height: item.height };\n const measured = item && measuredAspect.get(item);\n if (measured) return measured;\n const selfCorrecting = autoMeasureEnabled && !!item && !!measurableSrc(item);\n if (!warnedMissingDims && !selfCorrecting) {\n warnedMissingDims = true;\n console.warn(\n autoMeasureEnabled\n ? 'Shoji: layout plugin — item(s) missing width/height with no measurable thumb/poster/src to auto-measure from either; falling back to a 4:3 placeholder ratio permanently for those.'\n : 'Shoji: layout plugin — item(s) missing width/height and autoMeasure is disabled. Masonry/justified positions are computed from aspect ratios up front, never by measuring loaded images (DESIGN.md §5.2 — that causes layout-jump), so items without dimensions fall back to a 4:3 placeholder ratio instead.',\n );\n }\n return DEFAULT_ASPECT;\n }\n\n function createTile(item: GalleryItem, index: number): Tile {\n const root = document.createElement('a');\n root.className = 'shoji-layout-tile';\n if (item.id) root.dataset.shojiId = item.id;\n // Always set, unlike shojiId above (item.id is optional) — the\n // reliable element→item lookup for a `layoutRender` listener that\n // only has the element itself later (e.g. a click handler on content\n // it injected into the tile), not this render pass's own event\n // payload. `gallery.items[Number(el.dataset.shojiIndex)]` always works.\n root.dataset.shojiIndex = String(index);\n\n // item.src is the video *file* for a video item (unplayable as a tile\n // thumbnail) — item.poster is the actual image to show there, checked\n // before falling back to item.src, which is only ever correct for a\n // plain image item.\n const img = document.createElement('img');\n img.src = item.thumb ?? item.poster ?? item.src;\n img.loading = 'lazy';\n img.alt = item.alt ?? '';\n root.appendChild(img);\n\n const tile: Tile = { index, root, img };\n root.addEventListener('click', (event) => {\n event.preventDefault();\n gallery.open(tile.index);\n });\n return tile;\n }\n\n function applyGrid(target: readonly Tile[]): void {\n // Grid otherwise never reads its own size (native CSS grid reflows\n // itself) — only breakpoint resolution needs it read here.\n const containerWidth = container.getBoundingClientRect().width;\n const config = resolveConfig(containerWidth);\n container.style.setProperty('--shoji-layout-column-width', `${config.columnWidth}px`);\n // CSS gap shorthand is \"row-gap column-gap\" — y before x.\n container.style.setProperty(\n '--shoji-layout-gutter',\n `${config.gutter.y}px ${config.gutter.x}px`,\n );\n for (const tile of target) {\n tile.root.style.setProperty('--shoji-layout-tile-aspect', `${aspectRatio}`);\n }\n }\n\n /** `appendOnly`: reposition just these (new) tiles, continuing from the saved `columnHeights`; omit to relayout every tile from scratch (resize, or a non-append items change). Never called with `appendOnly` set when grouping is active — grouped changes always route through `fullRender` (see the `groupBy` option's doc comment). */\n function applyMasonry(appendOnly: readonly Tile[] | null): void {\n const containerWidth = container.getBoundingClientRect().width;\n if (containerWidth <= 0) return; // not painted yet (e.g. display:none) — the resize observer's initial callback will retry once it is\n\n const config = resolveConfig(containerWidth);\n const masonryOptions = {\n containerWidth,\n gutterX: config.gutter.x,\n gutterY: config.gutter.y,\n columns: config.columns,\n columnWidth: config.columnWidth,\n minColumnWidth: config.minColumnWidth,\n maxColumnWidth: config.maxColumnWidth,\n fill: config.fill,\n };\n\n if (groupingEnabled && headings.length > 0) {\n const segments = headingSegments();\n // Batch-read every heading's height up front, before writing any\n // tile styles below — interleaving reads (which force a layout\n // flush) with the transform writes in the same pass measurably\n // delayed the very first paint under animate: true (each flush\n // gives the browser a \"before\" style to transition away from that\n // it otherwise wouldn't have for freshly-inserted elements).\n const headingHeights = segments.map((s) => s.heading.root.getBoundingClientRect().height);\n\n let y = 0;\n segments.forEach(({ heading, start, end }, i) => {\n const segTiles = tiles.slice(start, end);\n heading.root.style.transform = `translate(0px, ${y}px)`;\n y += headingHeights[i]! + config.gutter.y;\n const segResult = layoutMasonry(\n segTiles.map((t) => aspectOf(gallery.items[t.index])),\n masonryOptions,\n );\n segTiles.forEach((tile, j) => {\n const pos = segResult.positions[j]!;\n tile.root.style.width = `${pos.width}px`;\n tile.root.style.height = `${pos.height}px`;\n tile.root.style.visibility = 'visible';\n tile.root.style.transform = `translate(${pos.x}px, ${y + pos.y}px)`;\n });\n y += segResult.containerHeight + config.gutter.y;\n });\n columnHeights = undefined; // no cross-segment continuation once grouped\n container.style.height = `${Math.max(0, y - config.gutter.y)}px`;\n return;\n }\n\n const target = appendOnly ?? tiles;\n const masonryTiles = target.map((t) => aspectOf(gallery.items[t.index]));\n const startHeights = appendOnly ? columnHeights : undefined;\n const result = layoutMasonry(masonryTiles, masonryOptions, startHeights);\n\n target.forEach((tile, i) => {\n const pos = result.positions[i]!;\n tile.root.style.width = `${pos.width}px`;\n tile.root.style.height = `${pos.height}px`;\n tile.root.style.visibility = 'visible';\n tile.root.style.transform = `translate(${pos.x}px, ${pos.y}px)`;\n });\n\n columnHeights = result.columnHeights;\n container.style.height = `${result.containerHeight}px`;\n }\n\n /**\n * The masonry-family transpose of applyMasonry: fixed-height rows pack\n * tiles left-to-right, wrapping to a new row rather than stretching —\n * see layoutMasonryHorizontal's doc comment (masonry.ts) for the full\n * algorithm. Like applyJustified, this always relays out every tile\n * from scratch (no `appendOnly`/incremental-continuation param) — a\n * row isn't \"committed\" until enough tiles arrive to overflow it, so an\n * appended tile can extend what was the previous render's trailing row,\n * exactly the same reason applyJustified can't do incremental append.\n */\n function applyMasonryHorizontal(): void {\n const containerWidth = container.getBoundingClientRect().width;\n if (containerWidth <= 0) return; // not painted yet (e.g. display:none) — the resize observer's initial callback will retry once it is\n\n const config = resolveConfig(containerWidth);\n const masonryTiles = tiles.map((t) => aspectOf(gallery.items[t.index]));\n const result = layoutMasonryHorizontal(masonryTiles, {\n containerWidth,\n gutterX: config.gutter.x,\n gutterY: config.gutter.y,\n rowHeight: config.rowHeight,\n });\n\n tiles.forEach((tile, i) => {\n const pos = result.positions[i]!;\n tile.root.style.width = `${pos.width}px`;\n tile.root.style.height = `${pos.height}px`;\n tile.root.style.visibility = 'visible';\n tile.root.style.transform = `translate(${pos.x}px, ${pos.y}px)`;\n });\n\n container.style.height = `${result.containerHeight}px`;\n }\n\n /**\n * The `groupBy` + `justified` combination — see that option's doc\n * comment for the full behavioral spec (sections don't force a row\n * break by default; a heading is a compact inline label, not a\n * full-width blocking element).\n *\n * `headingOverflow: 'show'` (default) and `'wrap'`: one continuous\n * `layoutJustified` pass packs every tile regardless of section — the\n * simple case, a single \"segment\" covering everything (computeSegments()\n * below only ever splits into multiple segments for `'fit'`). `'wrap'`\n * additionally constrains each label's own box afterward (see\n * applyWrapDecisions) — that's a content-level decision within a fixed\n * layout, not a row-composition one, so it doesn't need its own segment\n * handling here.\n *\n * `headingOverflow: 'fit'`: still prefers continuous packing, but a\n * section whose label (title+subtitle, or title alone once the\n * subtitle's dropped) wouldn't fit the gap to the next label sharing\n * its row gets pushed to start a fresh row instead — the label is\n * never wrapped. This means row *composition itself* has to respond to\n * label width, not just label content — computeSegments() below finds\n * the largest run of sections that can share rows without any label\n * needing to break, by growing a trial `layoutJustified` call one\n * section at a time and backing off the moment a fit check fails; each\n * final segment becomes its own independent `layoutJustified` call\n * (same mechanism as masonry's hard per-section blocks, except the\n * unit here is a dynamically-sized run of sections, not always\n * exactly one).\n */\n function applyJustifiedGrouped(\n justifiedOptions: JustifiedOptions,\n config: ResolvedConfig,\n containerWidth: number,\n ): void {\n // Batch-measure every structured heading's natural (unconstrained,\n // single-line) width up front — both with and without its subtitle —\n // before any segmentation trial runs. Needed by computeSegments()'s\n // fit checks below, and again by the final per-segment content\n // decision pass; measuring once here avoids re-measuring per trial.\n const fullWidths = new Map<number, number>();\n const titleOnlyWidths = new Map<number, number>();\n if (headingOverflow === 'fit') {\n for (const heading of headings) {\n if (!heading.structured) continue;\n const { subtitleEl } = heading.structured;\n heading.root.style.maxWidth = 'none';\n heading.root.style.whiteSpace = 'nowrap';\n if (subtitleEl) subtitleEl.hidden = false;\n fullWidths.set(heading.tileStart, heading.root.getBoundingClientRect().width);\n if (subtitleEl) {\n subtitleEl.hidden = true;\n titleOnlyWidths.set(heading.tileStart, heading.root.getBoundingClientRect().width);\n subtitleEl.hidden = false; // restored — the real decision happens per final segment, below\n } else {\n titleOnlyWidths.set(heading.tileStart, fullWidths.get(heading.tileStart)!);\n }\n }\n }\n\n /** [startSection, endSection) into `headings`, sized `tiles[]` range, and that range's own layoutJustified result. */\n type Segment = {\n startSection: number;\n endSection: number;\n tileStart: number;\n tileEnd: number;\n result: JustifiedResult;\n };\n\n function tileAspects(tileStart: number, tileEnd: number) {\n return tiles.slice(tileStart, tileEnd).map((t) => aspectOf(gallery.items[t.index]));\n }\n\n /** Runs layoutJustified on sections [startSection, endSection) and checks every label in it fits (full or title-only) its row's available width — null if any doesn't. */\n function tryFit(startSection: number, endSection: number): JustifiedResult | null {\n const tileStart = headings[startSection]!.tileStart;\n const tileEnd =\n endSection < headings.length ? headings[endSection]!.tileStart : tiles.length;\n const result = layoutJustified(tileAspects(tileStart, tileEnd), justifiedOptions);\n for (let k = startSection; k < endSection; k++) {\n const heading = headings[k]!;\n if (!heading.structured) continue; // raw renderHeading content never triggers a break\n const pos = result.positions[heading.tileStart - tileStart];\n if (!pos) continue; // lastRow: 'hide' trailing range within this trial\n const next = k + 1 < endSection ? headings[k + 1] : undefined;\n const nextPos = next ? result.positions[next.tileStart - tileStart] : null;\n const sameRow = !!nextPos && Math.abs(nextPos.y - pos.y) < 0.5;\n const available = (sameRow ? nextPos!.x : containerWidth) - pos.x - config.gutter.x;\n const bestWidth = Math.min(\n fullWidths.get(heading.tileStart) ?? Infinity,\n titleOnlyWidths.get(heading.tileStart) ?? Infinity,\n );\n if (bestWidth > available) return null;\n }\n return result;\n }\n\n function computeSegments(): Segment[] {\n if (headingOverflow !== 'fit') {\n return [\n {\n startSection: 0,\n endSection: headings.length,\n tileStart: 0,\n tileEnd: tiles.length,\n result: layoutJustified(tileAspects(0, tiles.length), justifiedOptions),\n },\n ];\n }\n const segments: Segment[] = [];\n let segStart = 0;\n while (segStart < headings.length) {\n const base = tryFit(segStart, segStart + 1);\n const tileStart = headings[segStart]!.tileStart;\n if (base === null) {\n // Doesn't fit even alone in its own row — the unavoidable case\n // (a single label wider than the container itself). Nothing\n // narrower to fall back to; take it as its own 1-section\n // segment anyway and let the content-decision pass below\n // ellipsis-truncate it rather than ever wrapping or\n // overflowing.\n const tileEnd =\n segStart + 1 < headings.length ? headings[segStart + 1]!.tileStart : tiles.length;\n segments.push({\n startSection: segStart,\n endSection: segStart + 1,\n tileStart,\n tileEnd,\n result: layoutJustified(tileAspects(tileStart, tileEnd), justifiedOptions),\n });\n segStart += 1;\n continue;\n }\n let end = segStart + 1;\n let best = base;\n while (end < headings.length) {\n const trial = tryFit(segStart, end + 1);\n if (trial === null) break;\n end += 1;\n best = trial;\n }\n const tileEnd = end < headings.length ? headings[end]!.tileStart : tiles.length;\n segments.push({\n startSection: segStart,\n endSection: end,\n tileStart,\n tileEnd,\n result: best,\n });\n segStart = end;\n }\n return segments;\n }\n\n /** Applies the real drop-subtitle/ellipsis decision for every structured heading in one finalized segment, using its actual computed positions (not a trial). */\n function applyContentDecisions(segment: Segment): void {\n for (let k = segment.startSection; k < segment.endSection; k++) {\n const heading = headings[k]!;\n if (!heading.structured) continue;\n const pos = segment.result.positions[heading.tileStart - segment.tileStart];\n if (!pos) continue;\n const next = k + 1 < segment.endSection ? headings[k + 1] : undefined;\n const nextPos = next\n ? segment.result.positions[next.tileStart - segment.tileStart]\n : null;\n const sameRow = !!nextPos && Math.abs(nextPos.y - pos.y) < 0.5;\n const available = (sameRow ? nextPos!.x : containerWidth) - pos.x - config.gutter.x;\n\n const { subtitleEl } = heading.structured;\n heading.root.style.whiteSpace = 'nowrap';\n heading.root.style.maxWidth = 'none';\n heading.root.style.overflow = '';\n heading.root.style.textOverflow = '';\n if (subtitleEl) subtitleEl.hidden = false;\n\n const full = fullWidths.get(heading.tileStart) ?? 0;\n if (full <= available) continue; // fits with subtitle, as-is\n\n if (subtitleEl) {\n subtitleEl.hidden = true;\n const titleOnly = titleOnlyWidths.get(heading.tileStart) ?? 0;\n if (titleOnly <= available) continue; // fits with subtitle dropped\n }\n\n // Still too wide title-only — only reachable for a 1-section\n // segment that couldn't be narrowed any further (computeSegments\n // already gave every other case its own row's worth of room).\n // Never wrap: ellipsis-truncate instead, still one line.\n heading.root.style.maxWidth = `${Math.max(0, available)}px`;\n heading.root.style.overflow = 'hidden';\n heading.root.style.textOverflow = 'ellipsis';\n }\n }\n\n /**\n * `headingOverflow: 'wrap'`'s content decision — unlike\n * applyContentDecisions above, this never drops the subtitle or\n * ellipsis-truncates a single line; it lets the label wrap onto\n * additional lines within its available width, then caps *how many*\n * lines via `-webkit-line-clamp` sized from `maxRowHeight` — a label\n * short enough to fit within that many lines is unaffected (nothing\n * for the clamp to do), a longer one ellipsis-truncates its last\n * visible line rather than growing the row past the configured cap.\n * `-webkit-line-clamp` is broadly supported cross-browser despite the\n * prefix (Chrome/Safari/Firefox all implement it) — there's no\n * unprefixed equivalent with comparable support yet.\n */\n function applyWrapDecisions(segment: Segment): void {\n for (let k = segment.startSection; k < segment.endSection; k++) {\n const heading = headings[k]!;\n if (!heading.structured) continue;\n const pos = segment.result.positions[heading.tileStart - segment.tileStart];\n if (!pos) continue;\n const next = k + 1 < segment.endSection ? headings[k + 1] : undefined;\n const nextPos = next\n ? segment.result.positions[next.tileStart - segment.tileStart]\n : null;\n const sameRow = !!nextPos && Math.abs(nextPos.y - pos.y) < 0.5;\n const available = (sameRow ? nextPos!.x : containerWidth) - pos.x - config.gutter.x;\n\n heading.root.style.whiteSpace = 'normal';\n heading.root.style.maxWidth = `${Math.max(0, available)}px`;\n\n const style = getComputedStyle(heading.root);\n const parsedLineHeight = parseFloat(style.lineHeight);\n const lineHeight = Number.isFinite(parsedLineHeight)\n ? parsedLineHeight\n : parseFloat(style.fontSize) * 1.2;\n const maxLines = Math.max(1, Math.floor(config.maxRowHeight / lineHeight));\n\n heading.root.style.display = '-webkit-box';\n heading.root.style.overflow = 'hidden';\n heading.root.style.setProperty('-webkit-box-orient', 'vertical');\n heading.root.style.setProperty('-webkit-line-clamp', `${maxLines}`);\n }\n }\n\n const segments = computeSegments();\n let cumulativeExtra = 0;\n let headingPtr = 0;\n\n for (const segment of segments) {\n if (headingOverflow === 'fit') applyContentDecisions(segment);\n else if (headingOverflow === 'wrap') applyWrapDecisions(segment);\n\n // Batch-read this segment's heading heights up front, after content\n // decisions are final — see applyMasonry's identical pattern/comment\n // for why reads and writes shouldn't interleave in the same pass.\n const headingHeights = new Map<number, number>();\n for (let k = segment.startSection; k < segment.endSection; k++) {\n const heading = headings[k]!;\n headingHeights.set(heading.tileStart, heading.root.getBoundingClientRect().height);\n }\n\n for (const row of segment.result.rows) {\n const globalStart = row.start + segment.tileStart;\n const globalEnd = row.end + segment.tileStart;\n\n let bandHeight = 0;\n while (headingPtr < segment.endSection && headings[headingPtr]!.tileStart < globalEnd) {\n const heading = headings[headingPtr]!;\n const pos = segment.result.positions[heading.tileStart - segment.tileStart];\n if (pos) {\n heading.root.hidden = false;\n heading.root.style.transform = `translate(${pos.x}px, ${row.y + cumulativeExtra}px)`;\n bandHeight = Math.max(bandHeight, headingHeights.get(heading.tileStart) ?? 0);\n }\n headingPtr++;\n }\n if (bandHeight > 0) cumulativeExtra += bandHeight + config.gutter.y;\n\n for (let i = globalStart; i < globalEnd; i++) {\n const pos = segment.result.positions[i - segment.tileStart];\n const tile = tiles[i]!;\n tile.root.hidden = pos === null;\n if (!pos) continue;\n tile.root.style.width = `${pos.width}px`;\n tile.root.style.height = `${pos.height}px`;\n tile.root.style.visibility = 'visible';\n tile.root.style.transform = `translate(${pos.x}px, ${pos.y + cumulativeExtra}px)`;\n }\n }\n\n // Tiles beyond this segment's last finalized row (a lastRow: 'hide'\n // trailing range) never got touched above — hide them explicitly.\n const segLastCovered =\n segment.result.rows.length > 0\n ? segment.result.rows[segment.result.rows.length - 1]!.end + segment.tileStart\n : segment.tileStart;\n for (let i = segLastCovered; i < segment.tileEnd; i++) {\n tiles[i]!.root.hidden = true;\n }\n\n // Advance past this whole segment's own content height, so the\n // next segment's rows (a fresh layoutJustified call, local y\n // starting back at 0) stack directly beneath it.\n cumulativeExtra += segment.result.containerHeight + config.gutter.y;\n }\n\n // Any heading whose section starts in the very last segment's hidden\n // trailing range never got positioned above — hide it rather than\n // leaving it at its CSS default top:0/left:0.\n while (headingPtr < headings.length) {\n headings[headingPtr]!.root.hidden = true;\n headingPtr++;\n }\n\n container.style.height = `${Math.max(0, cumulativeExtra - config.gutter.y)}px`;\n }\n\n /**\n * Unlike masonry's `columnHeights`, a justified row isn't \"locked in\"\n * until enough tiles arrive to fill it — an appended tile can extend or\n * complete what was the *previous* render's trailing row. Rather than\n * build a second continuation scheme for that, justified always relays\n * out every tile on any items change (append included); see §5.4.\n */\n function applyJustified(): void {\n const containerWidth = container.getBoundingClientRect().width;\n if (containerWidth <= 0) return; // not painted yet — resize observer's initial callback retries once it is\n\n const config = resolveConfig(containerWidth);\n const justifiedOptions = {\n containerWidth,\n gutterX: config.gutter.x,\n gutterY: config.gutter.y,\n rowHeight: config.rowHeight,\n minRowHeight: config.minRowHeight,\n maxRowHeight: config.maxRowHeight,\n lastRow: config.lastRow,\n };\n\n if (groupingEnabled && headings.length > 0) {\n applyJustifiedGrouped(justifiedOptions, config, containerWidth);\n return;\n }\n\n const justifiedTiles = tiles.map((t) => aspectOf(gallery.items[t.index]));\n const result = layoutJustified(justifiedTiles, justifiedOptions);\n\n tiles.forEach((tile, i) => {\n const pos = result.positions[i];\n tile.root.hidden = pos === null; // lastRow: 'hide' on this pass's trailing tiles\n if (!pos) return;\n tile.root.style.width = `${pos.width}px`;\n tile.root.style.height = `${pos.height}px`;\n tile.root.style.visibility = 'visible';\n tile.root.style.transform = `translate(${pos.x}px, ${pos.y}px)`;\n });\n\n container.style.height = `${result.containerHeight}px`;\n }\n\n function relayoutAll(): void {\n if (horizontal) applyMasonryHorizontal();\n else if (type === 'masonry') applyMasonry(null);\n else if (type === 'justified') applyJustified();\n else applyGrid(tiles);\n }\n\n let destroyed = false;\n let measureFrame: number | null = null;\n\n /**\n * Coalesces every `autoMeasure` correction landing in the same tick (or\n * the same animation frame — several cached images resolving\n * synchronously back-to-back is the common case) into one relayout,\n * never one per image. `groupingEnabled` needs a real `fullRender()`,\n * not just a reposition pass: `groupBy` was evaluated once already\n * during the first render, using whatever `item.width`/etc. was known\n * *then* — if that's exactly the field a correction just filled in,\n * the section boundaries themselves were computed wrong and only a\n * fresh `computeSections()` call (inside `fullRender`) fixes them, not\n * `relayoutAll()`, which only ever repositions the existing tiles/headings.\n */\n function scheduleMeasureRelayout(): void {\n if (measureFrame !== null || destroyed) return;\n measureFrame = requestAnimationFrame(() => {\n measureFrame = null;\n if (destroyed) return;\n if (groupingEnabled) fullRender(gallery.items);\n else relayoutAll();\n });\n }\n\n /**\n * DESIGN.md §5's `autoMeasure` option — never blocks or delays the\n * *first* layout pass (already painted with the 4:3 placeholder by the\n * time this runs); only ever corrects it afterward, once a real\n * measurement lands. Reuses each tile's own `<img>` (already fetching\n * for display) rather than a second `new Image()`, so nothing is\n * fetched twice. A `loading=\"lazy\"` tile far off-screen may not\n * resolve for a long time (or ever, if never scrolled to) — harmless,\n * since nothing visible is waiting on it; it simply keeps its 4:3\n * placeholder until/unless it does.\n */\n function autoMeasureTiles(newTiles: readonly Tile[]): void {\n if (!autoMeasureEnabled) return;\n for (const tile of newTiles) {\n const item = gallery.items[tile.index];\n // measuredAspect.has(item): skip re-measuring (and re-attaching a\n // 'load' listener) an item this cache already has an answer for —\n // item.width/height no longer get set by this function itself\n // (see measuredAspect's own doc comment above), so that alone\n // can't short-circuit repeat work across tile rebuilds the way it\n // used to.\n if (\n !item ||\n (item.width && item.height) ||\n measuredAspect.has(item) ||\n !measurableSrc(item)\n )\n continue;\n\n const onLoad = (): void => {\n if (destroyed) return;\n if (tile.img.naturalWidth > 0 && tile.img.naturalHeight > 0) {\n measuredAspect.set(item, {\n width: tile.img.naturalWidth,\n height: tile.img.naturalHeight,\n });\n scheduleMeasureRelayout();\n }\n };\n if (tile.img.complete) onLoad();\n else tile.img.addEventListener('load', onLoad, { once: true });\n // No 'error' listener needed — aspectOf()'s 4:3 fallback already\n // covers an image that never loads; there's nothing to update.\n }\n }\n\n /** DESIGN.md §5 — fires once a render pass's tile DOM is built and appended, so a host can inject its own content into `element` (a badge, a selection checkbox, anything not itself part of layout) without re-deriving which tiles are newly built this pass. */\n function emitLayoutRender(rendered: readonly Tile[]): void {\n ctx.emit('layoutRender', {\n tiles: rendered.map((tile) => ({ index: tile.index, element: tile.root })),\n });\n }\n\n function fullRender(items: readonly GalleryItem[]): void {\n container.replaceChildren();\n tiles = items.map((item, i) => createTile(item, i));\n headings = [];\n if (groupingEnabled) {\n for (const section of computeSections(items)) {\n const sectionItems = items.slice(section.startIndex, section.endIndex);\n const { root: headingRoot, structured } = createHeadingRoot(section.key, sectionItems);\n container.appendChild(headingRoot);\n headings.push({ root: headingRoot, tileStart: section.startIndex, structured });\n for (let i = section.startIndex; i < section.endIndex; i++) {\n container.appendChild(tiles[i]!.root);\n }\n }\n } else {\n for (const tile of tiles) container.appendChild(tile.root);\n }\n columnHeights = undefined;\n relayoutAll();\n autoMeasureTiles(tiles);\n emitLayoutRender(tiles);\n }\n\n function appendRender(newItems: readonly GalleryItem[], startIndex: number): void {\n const newTiles = newItems.map((item, i) => createTile(item, startIndex + i));\n for (const tile of newTiles) container.appendChild(tile.root);\n tiles = [...tiles, ...newTiles];\n if (horizontal)\n applyMasonryHorizontal(); // full relayout — see applyMasonryHorizontal's doc comment\n else if (type === 'masonry') applyMasonry(newTiles);\n else if (type === 'justified')\n applyJustified(); // full relayout — see applyJustified's doc comment\n else applyGrid(newTiles);\n autoMeasureTiles(newTiles);\n emitLayoutRender(newTiles);\n }\n\n fullRender(gallery.items);\n previousItems = gallery.items;\n\n const offItemsUpdated = ctx.on('itemsUpdated', ({ items }) => {\n if (groupingEnabled) {\n // grouped changes always fully re-render — see the `groupBy` option's doc comment.\n fullRender(items);\n } else if (isPureAppend(previousItems, items)) {\n appendRender(items.slice(previousItems.length), previousItems.length);\n } else {\n fullRender(items);\n }\n previousItems = items;\n });\n\n // Native CSS grid already reflows its own tile positions responsively —\n // it only needs a resize-driven recompute at all when breakpoints are\n // configured (to re-check whether the container crossed a threshold).\n // Masonry/justified always need one, since they're JS-computed.\n let resizeFrame: number | null = null;\n let resizeObserver: ResizeObserver | undefined;\n if (type === 'masonry' || type === 'justified' || sortedBreakpoints.length > 0) {\n resizeObserver = new ResizeObserver(() => {\n if (resizeFrame !== null) return;\n resizeFrame = requestAnimationFrame(() => {\n resizeFrame = null;\n relayoutAll();\n });\n });\n resizeObserver.observe(container);\n }\n\n return () => {\n destroyed = true;\n if (measureFrame !== null) cancelAnimationFrame(measureFrame);\n resizeObserver?.disconnect();\n if (resizeFrame !== null) cancelAnimationFrame(resizeFrame);\n offItemsUpdated();\n container.classList.remove('shoji-layout', `shoji-layout--${type}`, 'shoji-layout--animate');\n container.style.removeProperty('height');\n container.style.removeProperty('width');\n container.replaceChildren();\n };\n },\n};\n","export interface RotateFlipState {\n flipH: boolean;\n flipV: boolean;\n /** Degrees; normalized to one of 0/90/180/270 by `normalizeRotateFlip`. */\n rotation: number;\n}\n\n/**\n * DESIGN.md §8.1's flip/rotation canonicalization table, verbatim — CLAUDE.md:\n * \"Flip + rotate compose non-commutatively. Use the normalization table...\n * don't re-derive it.\" `flipH && flipV` is always visually equivalent to a\n * 180°-rotated state with neither flip set; this collapses any\n * `flipH`/`flipV`/`rotation` combination down to that canonical form, so two\n * states reached via different sequences of clicks compare equal and never\n * accumulate redundant flip+flip-again or unbounded rotation values.\n * Shared between the standalone rotate/flip *view* plugin (§4) and the\n * future Editor plugin (§8), which both need the identical composition —\n * not duplicated per-plugin.\n */\nexport function normalizeRotateFlip(state: RotateFlipState): RotateFlipState {\n const rotation = ((state.rotation % 360) + 360) % 360;\n if (state.flipH && state.flipV) {\n return { flipH: false, flipV: false, rotation: (rotation + 180) % 360 };\n }\n return { flipH: state.flipH, flipV: state.flipV, rotation };\n}\n","/** DESIGN.md §9 — inline SVG, stroke = currentColor, matches src/core/icons.ts's convention. */\nexport const ROTATE_LEFT_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 14 4 9l5-5\"/><path d=\"M4 9h10a6 6 0 1 1 0 12H9\"/></svg>';\n\nexport const ROTATE_RIGHT_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 14 20 9l-5-5\"/><path d=\"M20 9H10a6 6 0 1 0 0 12h5\"/></svg>';\n\nexport const FLIP_H_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=\"M12 3v18\"/><path d=\"M17 7l3 5-3 5\"/><path d=\"M7 7l-3 5 3 5\"/></svg>';\n\nexport const FLIP_V_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=\"M3 12h18\"/><path d=\"M7 17l5 3 5-3\"/><path d=\"M7 7l5-3 5 3\"/></svg>';\n","import type { PluginContext, ShojiPlugin } from '../../core/plugin';\nimport { normalizeRotateFlip, type RotateFlipState } from '../../core/rotateFlipNormalize';\nimport { waitForTransitionEnd } from '../../core/zoomTransition';\nimport { FLIP_H_ICON, FLIP_V_ICON, ROTATE_LEFT_ICON, ROTATE_RIGHT_ICON } from './icons';\n\nconst NEUTRAL: RotateFlipState = { flipH: false, flipV: false, rotation: 0 };\n\n/**\n * Flip axes apply to the *currently visible* (already-rotated) orientation,\n * not the original unrotated image — `scaleX`/`scaleY` listed before\n * `rotate()` in the transform string is what makes that true: CSS transform\n * functions apply right-to-left, so `rotate()` (rightmost) affects the\n * content first, and the flip (leftmost) acts on that already-rotated\n * result.\n *\n * `flipH`/`flipV`/`rotationDeg` here are the *raw, uncanonicalized* visual\n * values (see `visualFlipH`/`visualFlipV`/`visualRotation` below) — never\n * `state`'s own normalized ones. Two real bugs came from feeding the\n * normalized state directly into this animated transform instead:\n *\n * 1. `rotationDeg` must be unbounded, not wrapped to `[0, 360)`: animating\n * straight to a wrapped value (e.g. 270° → 0° after a fourth\n * rotate-right click, instead of continuing on to 360°) makes the\n * browser interpolate a 270° *decrease*, spinning backward almost a\n * full turn instead of continuing the same 90° forward step being\n * clicked through.\n * 2. `flipH`/`flipV` must stay two independent, literal booleans, never\n * collapsed the way `normalizeRotateFlip` collapses `flipH && flipV`\n * into `rotation + 180`: flipping horizontal then vertical while\n * already flipped horizontal would otherwise animate `scaleX` back to\n * 1 *and* `rotate` up to 180° simultaneously (both values are in the\n * transform-function list, so the browser interpolates each\n * independently) — a compound squish-and-spin instead of the plain\n * vertical-flip motion \"Flip vertical\" implies. Keeping the raw\n * booleans means only `scaleY` ever changes for that click, since\n * flipping both axes is algebraically identical to a 180° rotation\n * regardless of what rotation is already applied — the *end* look\n * always matches the canonical state either way, only the *animated\n * path* differs.\n *\n * `rotationDeg === 0` is still a safe, unambiguous \"truly neutral\" check\n * for the shortcut below — every rotate click adds ±90, so it only lands\n * back on exactly `0` at genuine reset/initial state, never a masked\n * multiple of 360.\n *\n * `fitScale` (see `fitScaleFor` below) is folded directly into `scaleX`/\n * `scaleY` — a single uniform factor commutes with everything else already\n * in this transform (rotate, and flip's own -1), so multiplying it in here\n * is exactly equivalent to a separate trailing `scale()` function, without\n * adding a fourth transform function for the browser to interpolate.\n */\nfunction transformFor(\n flipH: boolean,\n flipV: boolean,\n rotationDeg: number,\n fitScale: number,\n): string {\n if (!flipH && !flipV && rotationDeg === 0 && fitScale === 1) return 'none';\n const scaleX = (flipH ? -1 : 1) * fitScale;\n const scaleY = (flipV ? -1 : 1) * fitScale;\n return `scaleX(${scaleX}) scaleY(${scaleY}) rotate(${rotationDeg}deg)`;\n}\n\n/**\n * DESIGN.md §4.5 — shrinks the *visible photo* exactly as much as needed to\n * keep it from getting its edges clipped away when rotated, and grows it\n * back up to fill newly-available space on rotation, but never past its\n * own native pixel resolution.\n *\n * **A real bug in the previous version of this fix, caught from real usage\n * on the docs site itself: it assumed `.shoji-slide-img` always scales to\n * *touch* the container on at least one axis (`object-fit: contain`'s\n * usual behavior), which is wrong.** The actual CSS is `max-width: 100%;\n * max-height: 100%` — a *cap*, not a forced fill (documented in\n * `shoji.css`'s own comment: real photos are assumed bigger than the\n * slide area, so never growing past natural size is the correct default).\n * A small placeholder photo, comfortably smaller than the dialog, simply\n * renders at its own native size — untouching every edge, nothing scaled.\n * The previous formula didn't know this: it computed an imagined\n * \"as if `object-fit: contain` always scales to fill\" pre-rotation size\n * (e.g. an 800×600 photo hypothetically stretched to ~1267×950 in a\n * 1920×950 window), then shrank *from that invented size* — a real,\n * visible shrink relative to what was actually on screen a moment\n * earlier, even though nothing should have changed at all. Confirmed\n * directly: measuring the real `<img>` on the real deployed docs page\n * showed it rendered at exactly its 800×600 native size, not the\n * \"contain-fit\" size the old formula assumed.\n *\n * **The fix: compute the photo's real render scale at each orientation\n * the same way the browser's own CSS does, then compare the two —\n * instead of computing an idealized target size and separately capping\n * it.** `scaleAt0`/`scaleAt90` are each `Math.min(1, mediaWidth /\n * relevantNaturalWidth, mediaHeight / relevantNaturalHeight)` — exactly\n * mirroring `max-width/max-height: 100%`'s own \"shrink to fit, never grow\n * past native size\" rule, once for the current (unrotated) orientation\n * and once for the rotated one (natural width/height swapped). The result\n * is simply their ratio: how much *more* (or less) of its own native\n * resolution the rotated orientation can use compared to what's already\n * on screen. This single ratio does everything the old two-step\n * idealFit-then-cap formula tried to do, correctly and for free: neither\n * `scaleAt0` nor `scaleAt90` can ever exceed `1` (native resolution is\n * never exceeded, in *either* orientation, not just relative to a\n * possibly-wrong assumed starting point), and whichever one is more\n * constrained by the container — rather than by native resolution —\n * still shrinks or grows the ratio exactly as far as that constraint\n * requires.\n *\n * `1` (a no-op) for 0°/180° — a rectangle's own bounding box is unchanged\n * by a half-turn, nothing to re-fit — and whenever either media dimension\n * isn't known yet (not yet laid out) or the photo's own natural dimensions\n * aren't known at all (video, or an image that hasn't decoded) — skipped\n * entirely rather than guessing.\n */\nfunction fitScaleFor(\n mediaWidth: number,\n mediaHeight: number,\n rotationDeg: number,\n naturalWidth: number | undefined,\n naturalHeight: number | undefined,\n): number {\n if (!mediaWidth || !mediaHeight || !naturalWidth || !naturalHeight) return 1;\n if ((rotationDeg / 90) % 2 === 0) return 1;\n const scaleAt0 = Math.min(1, mediaWidth / naturalWidth, mediaHeight / naturalHeight);\n const scaleAt90 = Math.min(1, mediaWidth / naturalHeight, mediaHeight / naturalWidth);\n return scaleAt90 / scaleAt0;\n}\n\n/**\n * DESIGN.md §4 — rotate ±90°/flip H/V of the *view*: a CSS transform on the\n * active `.shoji-slide-media`, non-destructive, resets to neutral on every\n * `afterOpen`/`afterSlide` (per-slide, not per-gallery — DESIGN.md's own\n * wording). Emits `rotateFlipChange` on every change so a host that wants\n * this to persist can store it themselves and re-apply later (e.g. by\n * feeding a starting orientation back in some other way) — this plugin\n * itself has no persistence of its own, matching \"resets per slide\".\n *\n * Distinct from the (unbuilt) Editor plugin's own rotate/flip (§8), which\n * is destructive/persisted server-side via `item.edits`. Both share the\n * same composition math (`normalizeRotateFlip`, `src/core/`) rather than\n * each re-deriving flip+rotate's non-commutative composition — a real bug\n * class CLAUDE.md calls out by name.\n */\nexport const RotateFlip: ShojiPlugin = {\n name: 'rotateFlip',\n\n init(ctx: PluginContext): () => void {\n const { gallery } = ctx;\n const locale = (gallery.options.locale ?? {}) as Record<string, string>;\n const rotateLeftLabel = locale.rotateLeft ?? 'Rotate left';\n const rotateRightLabel = locale.rotateRight ?? 'Rotate right';\n const flipHLabel = locale.flipHorizontal ?? 'Flip horizontal';\n const flipVLabel = locale.flipVertical ?? 'Flip vertical';\n\n let state: RotateFlipState = { ...NEUTRAL };\n /** Unbounded — never wrapped like `state.rotation` is. Drives only the CSS transform's rotate() degrees, so every rotate click continues smoothly in the same direction (90, 180, 270, 360, 450, ...) instead of snapping backward whenever the normalized state wraps past 0/360. See `transformFor`'s own doc comment for the full reasoning. */\n let visualRotation = 0;\n /** Raw, independent toggles — never collapsed into a rotation the way `state.flipH`/`state.flipV` are. See `transformFor`'s own doc comment. */\n let visualFlipH = false;\n let visualFlipV = false;\n\n /**\n * `animate` mirrors the Zoom plugin's own \"discrete jumps animate\"\n * pattern (`zoom/index.ts`'s `withTransition`) — a button click is a\n * one-shot state change, not a continuous gesture, so it eases instead\n * of snapping. `reset()` stays unanimated: the viewer never rotated the\n * new slide, so there's nothing to visibly animate *from*. The\n * transition is cleared once it ends so it doesn't linger onto the\n * open/close zoom transition's own later use of this same element's\n * `transform` (`zoomTransition.ts`, `.shoji-slide-media`).\n */\n /** `item.width`/`height` when known, else the active image's own natural dimensions — same fallback order the zoom transition (§2.3b) uses for its own aspect ratio, just sourced from the slide itself rather than the origin thumbnail, and kept as a real width/height pair (not collapsed to a ratio) since `fitScaleFor` needs the actual pixel counts for its resolution ceiling, not just their proportion. `undefined` for anything else (video, an image not yet decoded) — `fitScaleFor` treats that as nothing to fit, not a guess. */\n function resolveNaturalSize(media: HTMLElement): { width: number; height: number } | undefined {\n const item = gallery.items[gallery.currentIndex];\n if (item?.width && item.height) return { width: item.width, height: item.height };\n const img = media.querySelector('img');\n if (img?.naturalWidth && img.naturalHeight) {\n return { width: img.naturalWidth, height: img.naturalHeight };\n }\n return undefined;\n }\n\n function apply(animate: boolean): void {\n const media = gallery.getActiveMedia();\n if (!media) return;\n const natural = resolveNaturalSize(media);\n const fitScale = fitScaleFor(\n media.clientWidth,\n media.clientHeight,\n visualRotation,\n natural?.width,\n natural?.height,\n );\n const transform = transformFor(visualFlipH, visualFlipV, visualRotation, fitScale);\n if (!animate) {\n media.style.transform = transform;\n return;\n }\n media.style.transition = 'transform var(--shoji-duration) var(--shoji-easing)';\n media.style.transform = transform;\n waitForTransitionEnd(media, () => {\n media.style.transition = '';\n });\n }\n\n function buildButton(icon: string, label: string): HTMLButtonElement {\n const button = document.createElement('button');\n button.type = 'button';\n button.className = 'shoji-toolbar-button';\n button.innerHTML = icon;\n button.setAttribute('aria-label', label);\n button.title = label;\n return button;\n }\n\n const rotateLeftBtn = buildButton(ROTATE_LEFT_ICON, rotateLeftLabel);\n const rotateRightBtn = buildButton(ROTATE_RIGHT_ICON, rotateRightLabel);\n const flipHBtn = buildButton(FLIP_H_ICON, flipHLabel);\n const flipVBtn = buildButton(FLIP_V_ICON, flipVLabel);\n flipHBtn.setAttribute('aria-pressed', 'false');\n flipVBtn.setAttribute('aria-pressed', 'false');\n\n /**\n * `visualRotationDelta`, when given, is a rotate click's own known\n * direction (±90) — applied as-is to the unbounded `visualRotation`.\n * Flip clicks omit it entirely: `visualFlipH`/`visualFlipV` are toggled\n * directly at each call site instead, completely independent of\n * `state`'s own canonicalized rotation/flip collapse (`transformFor`'s\n * doc comment has the full reasoning for why the two must stay\n * decoupled).\n */\n function update(patch: Partial<RotateFlipState>, visualRotationDelta?: number): void {\n state = normalizeRotateFlip({ ...state, ...patch });\n if (visualRotationDelta !== undefined) visualRotation += visualRotationDelta;\n apply(true);\n flipHBtn.setAttribute('aria-pressed', String(state.flipH));\n flipVBtn.setAttribute('aria-pressed', String(state.flipV));\n ctx.emit('rotateFlipChange', { index: gallery.currentIndex, ...state });\n }\n\n /**\n * A real bug, reported from real usage: with exactly one flip axis\n * active, \"Rotate right\" visually spun the image counter-clockwise\n * instead — a `+90` raw `rotate()` delta composed with a single\n * `scaleX(-1)`/`scaleY(-1)` mirror reverses the rotation's visual\n * handedness (a mirror is a reflection — determinant -1 — so a\n * clockwise turn *inside* it reads as counter-clockwise once mirrored\n * back onto the screen). Flipping *both* axes doesn't have this problem\n * — two reflections compose back into a rotation (determinant +1, same\n * as no flip at all), which is exactly why `normalizeRotateFlip`\n * already collapses that combination into a plain 180° rotation.\n * Inverting the raw delta whenever `visualFlipH !== visualFlipV` (XOR —\n * \"exactly one axis flipped\") makes the buttons always spin the image\n * the way they're visually labeled, regardless of flip state; `state`\n * (the canonicalized, emitted value) gets the same inverted delta, so\n * it stays an accurate description of what's actually on screen.\n */\n function rotateDelta(clockwise: boolean): number {\n const flippedOnOneAxis = visualFlipH !== visualFlipV;\n return clockwise !== flippedOnOneAxis ? 90 : -90;\n }\n\n rotateLeftBtn.addEventListener('click', () => {\n const delta = rotateDelta(false);\n update({ rotation: state.rotation + delta }, delta);\n });\n rotateRightBtn.addEventListener('click', () => {\n const delta = rotateDelta(true);\n update({ rotation: state.rotation + delta }, delta);\n });\n flipHBtn.addEventListener('click', () => {\n visualFlipH = !visualFlipH;\n update({ flipH: !state.flipH });\n });\n flipVBtn.addEventListener('click', () => {\n visualFlipV = !visualFlipV;\n update({ flipV: !state.flipV });\n });\n\n function reset(): void {\n state = { ...NEUTRAL };\n visualRotation = 0;\n visualFlipH = false;\n visualFlipV = false;\n apply(false);\n flipHBtn.setAttribute('aria-pressed', 'false');\n flipVBtn.setAttribute('aria-pressed', 'false');\n }\n\n // 'right' — registered in this order, so they cluster left-to-right as\n // rotateLeft, rotateRight, flipH, flipV, then whatever later plugin (or\n // the close button) follows (DESIGN.md §3.1).\n const removeButtons = [rotateLeftBtn, rotateRightBtn, flipHBtn, flipVBtn].map((button) =>\n ctx.ui.toolbar('right', button),\n );\n\n const offOpen = ctx.on('afterOpen', reset);\n const offSlide = ctx.on('afterSlide', reset);\n\n return () => {\n for (const remove of removeButtons) remove();\n offOpen();\n offSlide();\n };\n },\n};\n","import type { VideoProviderRenderer } from '../../core/plugin';\n\n/**\n * The bits of Vimeo's Player SDK actually used here — not in `lib.dom.d.ts`\n * (a third-party global loaded on demand), so this is a documented interop\n * boundary (CLAUDE.md: `any` only there). Kept intentionally minimal, not a\n * full mirror of Vimeo's own typings. Unlike YouTube's IFrame API, every\n * method here is genuinely Promise-based — Vimeo has no synchronous\n * \"isMuted()\"-style query at all, which is why `wirePlayableContract` below\n * mirrors `muted` locally instead of reading it live (see its own comment).\n */\ninterface VimeoPlayer {\n play(): Promise<void>;\n pause(): Promise<void>;\n setMuted(muted: boolean): Promise<boolean>;\n ready(): Promise<void>;\n destroy(): Promise<void>;\n on(event: 'play' | 'pause' | 'ended', callback: () => void): void;\n on(event: 'error', callback: (error: VimeoErrorEvent) => void): void;\n}\n\n/** Vimeo's own error event shape (`name`/`message`, plus an undocumented `method` sometimes) — e.g. `name: 'PrivacyError'` (embedding disabled), `'PasswordError'` (password-protected). */\ninterface VimeoErrorEvent {\n name: string;\n message: string;\n}\n\ninterface VimeoNamespace {\n Player: new (\n element: HTMLElement,\n options: { id?: number; url?: string; playsinline?: boolean },\n ) => VimeoPlayer;\n}\n\ndeclare global {\n interface Window {\n Vimeo?: VimeoNamespace;\n }\n}\n\nlet apiPromise: Promise<VimeoNamespace> | null = null;\n\n// See the 'pause'/'ended' race comment where this is used, below.\nconst PAUSE_DISPATCH_DELAY_MS = 250;\n\n/**\n * Loads `https://player.vimeo.com/api/player.js` exactly once however many\n * Vimeo slides/galleries end up on a page — `window.Vimeo` is a single\n * global. Simpler than `youtube.ts`'s `loadYouTubeApi`: Vimeo's script has\n * no global-callback side channel to compose with (nothing else could\n * already be watching for it the way `onYouTubeIframeAPIReady` can be), so\n * a plain `script.onload` is enough.\n */\nfunction loadVimeoApi(): Promise<VimeoNamespace> {\n if (window.Vimeo?.Player) return Promise.resolve(window.Vimeo);\n if (apiPromise) return apiPromise;\n\n apiPromise = new Promise((resolve) => {\n const script = document.createElement('script');\n script.src = 'https://player.vimeo.com/api/player.js';\n script.onload = () => resolve(window.Vimeo!);\n document.head.appendChild(script);\n });\n return apiPromise;\n}\n\n/**\n * DESIGN.md §4-autoplay — Autoplay looks for exactly this shape (`.play`/\n * `.pause`/`.paused`/`.ended`, plus real `play`/`pause`/`ended` DOM events)\n * on whatever's active, native `<video>` or not. Augmenting `container` this\n * way is what makes slideshow sync work without Autoplay ever having to\n * know Vimeo — or any other provider — exists. `play` genuinely returns\n * `player.play()`'s own promise (unlike `youtube.ts`'s fire-and-forget\n * `.play`) — Autoplay's own `ensureProviderPlaying` (§4.1 point 12) branches\n * its retry strategy on whether this is a real thenable at runtime.\n */\ntype PlayableElement = HTMLElement & {\n play: () => Promise<void>;\n pause: () => void;\n paused: boolean;\n ended: boolean;\n muted: boolean;\n};\n\n/**\n * `muted`'s setter genuinely forwards to `player.setMuted()` (fire-and-\n * forget, same as `youtube.ts`'s `mute()`/`unMute()` calls), but the getter\n * mirrors a locally-tracked value instead of reading the player live —\n * unlike YouTube's `isMuted()`, Vimeo's `getMuted()` is Promise-based, so\n * there's no synchronous value to read at all. Harmless here: nothing in\n * Autoplay ever reads `.muted` back, only sets it (DESIGN.md §4.1 point 8's\n * `video.muted = true` before an automatic play).\n */\nfunction wirePlayableContract(container: HTMLElement, player: VimeoPlayer): PlayableElement {\n const playable = container as PlayableElement;\n playable.paused = true;\n playable.ended = false;\n playable.play = () => player.play();\n playable.pause = () => player.pause();\n let mutedState = false;\n Object.defineProperty(playable, 'muted', {\n configurable: true,\n get: () => mutedState,\n set: (value: boolean) => {\n mutedState = value;\n player.setMuted(value);\n },\n });\n return playable;\n}\n\n/**\n * DESIGN.md §4-video — no poster/thumbnail handling at all: the slide shows\n * nothing until the embed itself is ready (same spinner-then-reveal every\n * other slide type gets), never an auto-fetched or guessed preview image.\n */\nexport const renderVimeo: VideoProviderRenderer = (container, item, onReady, signal) => {\n if (item.video?.provider !== 'vimeo') return;\n const { id, url } = item.video;\n if (!id && !url) {\n // A misconfigured item — scan.ts already warned about this at\n // normalization time. Same placeholder SlideManager itself uses for \"no\n // source\"/\"provider not registered\", so the spinner doesn't hang\n // forever waiting for a ready() that would never resolve otherwise.\n const placeholder = document.createElement('div');\n placeholder.className = 'shoji-slide-placeholder';\n placeholder.textContent = 'Video';\n container.appendChild(placeholder);\n onReady();\n return;\n }\n\n // shoji-video-mount (styles/shoji.css) — several real bugs, reported from\n // real usage, chased through in sequence:\n //\n // 1. Left at its own defaults, Vimeo's SDK sizes `mount` to a small fixed\n // box (its own embed default, ~640x360) via inline styles — CSS's own\n // `width/height: 100%` on the iframe only ever resolves relative to\n // that fixed-size `mount`, not the actual slide, so the embed rendered\n // small, in a corner, instead of filling the dialog.\n //\n // 2. `responsive: true` looked like the fix (and does make `mount` fill\n // its parent's *width*) but doesn't actually solve this: it computes\n // `mount`'s *height* from that width times the video's own native\n // aspect ratio via a padding-percentage trick, entirely ignoring how\n // much vertical space the slide actually has — confirmed directly at a\n // wide-but-short viewport, the resulting iframe came out taller than\n // its container, pushing the bottom of the video (and Vimeo's own\n // scrubber/controls bar, always along that same edge) below the\n // visible area.\n //\n // 3. Fully stretching `mount` to the container's own box instead (no\n // aspect-ratio at all) fixed that, but traded it for a third bug:\n // stretching regardless of the video's real 16:9 shape exposed\n // Vimeo's own internal letterbox color (white — not overridable from\n // here, that content is cross-origin) wherever the box wasn't already\n // 16:9 itself, instead of Shoji's own dark backdrop.\n //\n // `shoji-video-mount`'s CSS rules are what actually solve all three at\n // once now: contained to a real 16:9 box via container query units\n // (`!important` still doing the \"discard whatever Vimeo tries to set\n // inline\" job point 1 needed), so any leftover gap is *outside* the\n // iframe, showing this element's own transparent background — Shoji's\n // dark backdrop — rather than the provider's white. YouTube's plain\n // iframe (no such wrapper) gets the identical treatment directly, see\n // `.shoji-slide-provider-video > iframe` in shoji.css.\n const mount = document.createElement('div');\n mount.className = 'shoji-video-mount';\n container.appendChild(mount);\n\n loadVimeoApi().then((Vimeo) => {\n if (signal.aborted) return; // navigated away before the (possibly first-ever, slow) API load resolved\n\n const playable = container as PlayableElement;\n // `url` (the original data-shoji-video/video.url, when present) is\n // preferred over a bare id: it's what carries an unlisted video's\n // privacy hash (vimeo.com/{id}/{hash}) through to the player, which a\n // numeric id alone can't express. Only dynamic-mode items authored\n // without a url ever fall back to id-only.\n const player = new Vimeo.Player(mount, {\n ...(url ? { url } : { id: Number(id) }),\n playsinline: true,\n });\n\n // A real bug, reported from real usage: reaching the end of the video\n // stopped the slideshow instead of advancing it, and — the tell —\n // manually pressing play again restarted the same video from 0 instead\n // of resuming anywhere. Confirmed directly: Vimeo's own 'pause' arrives\n // as a genuinely separate postMessage event shortly *before* 'ended'\n // (~20ms apart in testing) when a video reaches its natural end, unlike\n // native `<video>`, whose `.ended` property the browser sets\n // synchronously before firing either event — Autoplay's own\n // `onVideoPause` (DESIGN.md §4.1 point 3) checks `.ended` to tell a\n // real pause from an end-adjacent one, and that check reliably lost the\n // race, reading `false` and treating a natural end as a manual pause.\n // Fixed by not dispatching 'pause' immediately: it's held for\n // `PAUSE_DISPATCH_DELAY_MS`, comfortably above the observed gap, and\n // dropped entirely if 'ended' arrives first — which is also then the\n // *only* event a natural end ever produces, matching how a single\n // native `<video>` 'pause' alongside 'ended' was already a non-issue\n // via the `.ended` check alone. A genuine manual pause (no 'ended'\n // following) still dispatches, just delayed by that same short window\n // — imperceptible for a real pause, and worth it for a correct one.\n let pendingPauseTimer: ReturnType<typeof setTimeout> | null = null;\n function clearPendingPause(): void {\n if (pendingPauseTimer !== null) {\n clearTimeout(pendingPauseTimer);\n pendingPauseTimer = null;\n }\n }\n player.on('play', () => {\n clearPendingPause();\n playable.paused = false;\n playable.dispatchEvent(new Event('play'));\n });\n player.on('pause', () => {\n playable.paused = true;\n clearPendingPause();\n pendingPauseTimer = setTimeout(() => {\n pendingPauseTimer = null;\n playable.dispatchEvent(new Event('pause'));\n }, PAUSE_DISPATCH_DELAY_MS);\n });\n player.on('ended', () => {\n clearPendingPause();\n playable.paused = true;\n playable.ended = true;\n playable.dispatchEvent(new Event('ended'));\n });\n // bubbles: true — reaches .shoji-slide-media (the pool slot, always\n // present regardless of readiness) without needing wirePlayableContract\n // to have run first; matches youtube.ts's onError — a player can error\n // out (bad id, privacy-restricted, password-protected) without ever\n // becoming ready. Autoplay (DESIGN.md §4.1) listens for this to skip to\n // the next slide instead of stalling.\n player.on('error', (error) =>\n playable.dispatchEvent(\n new CustomEvent('error', { bubbles: true, detail: { name: error.name } }),\n ),\n );\n\n player.ready().then(() => {\n wirePlayableContract(container, player);\n onReady();\n });\n\n signal.addEventListener(\n 'abort',\n () => {\n clearPendingPause();\n void player.destroy();\n },\n { once: true },\n );\n });\n};\n","import type { VideoProviderRenderer } from '../../core/plugin';\n\n/**\n * The bits of the YouTube IFrame Player API actually used here — not in\n * `lib.dom.d.ts` (a third-party global loaded on demand), so this is a\n * documented interop boundary (CLAUDE.md: `any` only there). Kept\n * intentionally minimal, not a full mirror of YouTube's own typings.\n */\ninterface YTPlayer {\n playVideo(): void;\n pauseVideo(): void;\n mute(): void;\n unMute(): void;\n isMuted(): boolean;\n destroy(): void;\n}\n\ninterface YTPlayerStateEvent {\n data: number;\n}\n\n/**\n * Same `{ data: number }` shape as `YTPlayerStateEvent` — `data` holds an\n * error code there (2 invalid parameter, 5 HTML5 player error, 100 video\n * not found/private, 101/150 embedding disabled by the owner; undocumented\n * codes like 153 are also reported in the wild) rather than a player state.\n * Named separately from `YTPlayerStateEvent` for readability at the call\n * site, not because the shape actually differs.\n */\ninterface YTPlayerErrorEvent {\n data: number;\n}\n\ninterface YTNamespace {\n Player: new (\n el: HTMLElement,\n opts: {\n videoId: string;\n playerVars?: { playsinline?: 0 | 1; rel?: 0 | 1 };\n events?: {\n onReady?: () => void;\n onStateChange?: (event: YTPlayerStateEvent) => void;\n onError?: (event: YTPlayerErrorEvent) => void;\n };\n },\n ) => YTPlayer;\n PlayerState: { ENDED: number; PLAYING: number; PAUSED: number };\n}\n\ndeclare global {\n interface Window {\n YT?: YTNamespace;\n onYouTubeIframeAPIReady?: () => void;\n }\n}\n\nlet apiPromise: Promise<YTNamespace> | null = null;\n\n/**\n * Loads `https://www.youtube.com/iframe_api` exactly once however many\n * YouTube slides/galleries end up on a page — the API itself is a single\n * global (`window.YT`), and re-injecting the script tag would be wasteful\n * and risks a second, conflicting `onYouTubeIframeAPIReady` firing. Composes\n * with a host's own `onYouTubeIframeAPIReady`, if one already exists\n * (e.g. the host embeds YouTube elsewhere too), instead of clobbering it.\n */\nfunction loadYouTubeApi(): Promise<YTNamespace> {\n if (window.YT?.Player) return Promise.resolve(window.YT);\n if (apiPromise) return apiPromise;\n\n apiPromise = new Promise((resolve) => {\n const previous = window.onYouTubeIframeAPIReady;\n window.onYouTubeIframeAPIReady = () => {\n previous?.();\n resolve(window.YT!);\n };\n const script = document.createElement('script');\n script.src = 'https://www.youtube.com/iframe_api';\n document.head.appendChild(script);\n });\n return apiPromise;\n}\n\n/**\n * DESIGN.md §4-autoplay — Autoplay looks for exactly this shape (`.play`/\n * `.pause`/`.paused`/`.ended`, plus real `play`/`pause`/`ended` DOM events)\n * on whatever's active, native `<video>` or not. Augmenting `container`\n * this way is what makes slideshow sync work without Autoplay ever having\n * to know YouTube — or any other provider — exists.\n */\ntype PlayableElement = HTMLElement & {\n play: () => void;\n pause: () => void;\n paused: boolean;\n ended: boolean;\n muted: boolean;\n};\n\n/**\n * `muted` is a real accessor (not a plain property) backed by the player's\n * own `mute()`/`unMute()`/`isMuted()` — DESIGN.md §4.1's autoplay-policy fix\n * sets `video.muted = true` right before an *automatic* play, the same way\n * it would for a native `<video>`; a plain property here would just be\n * inert state that never actually silences the embed.\n */\nfunction wirePlayableContract(container: HTMLElement, player: YTPlayer): PlayableElement {\n const playable = container as PlayableElement;\n playable.paused = true;\n playable.ended = false;\n playable.play = () => player.playVideo();\n playable.pause = () => player.pauseVideo();\n Object.defineProperty(playable, 'muted', {\n configurable: true,\n get: () => player.isMuted(),\n set: (value: boolean) => (value ? player.mute() : player.unMute()),\n });\n return playable;\n}\n\nfunction handleStateChange(\n event: YTPlayerStateEvent,\n YT: YTNamespace,\n playable: PlayableElement,\n): void {\n if (event.data === YT.PlayerState.PLAYING) {\n playable.paused = false;\n playable.dispatchEvent(new Event('play'));\n } else if (event.data === YT.PlayerState.PAUSED) {\n playable.paused = true;\n playable.dispatchEvent(new Event('pause'));\n } else if (event.data === YT.PlayerState.ENDED) {\n playable.paused = true;\n playable.ended = true;\n playable.dispatchEvent(new Event('ended'));\n }\n}\n\n/**\n * DESIGN.md §4-video — no poster/thumbnail handling at all: the slide shows\n * nothing until the embed itself is ready (same spinner-then-reveal every\n * other slide type gets), never an auto-fetched or guessed preview image.\n */\nexport const renderYouTube: VideoProviderRenderer = (container, item, onReady, signal) => {\n if (item.video?.provider !== 'youtube') return;\n const videoId = item.video.id;\n if (!videoId) {\n // A misconfigured item — scan.ts (data-shoji-video-id / video: true /\n // video.id auto-fill) already warned about this at normalization time.\n // Same placeholder SlideManager itself uses for \"no source\"/\"provider\n // not registered\", so the spinner doesn't hang forever waiting for an\n // onReady that would never fire otherwise.\n const placeholder = document.createElement('div');\n placeholder.className = 'shoji-slide-placeholder';\n placeholder.textContent = 'Video';\n container.appendChild(placeholder);\n onReady();\n return;\n }\n\n const mount = document.createElement('div');\n container.appendChild(mount);\n\n loadYouTubeApi().then((YT) => {\n if (signal.aborted) return; // navigated away before the (possibly first-ever, slow) API load resolved\n\n const playable = container as PlayableElement;\n const player = new YT.Player(mount, {\n videoId,\n playerVars: { playsinline: 1, rel: 0 },\n events: {\n // `new YT.Player()` returns before the embed's own internal bootstrap\n // (its postMessage handshake with youtube.com) finishes — calling\n // `player.mute()`/`playVideo()`/etc. before that completes can throw\n // (\"player.isMuted is not a function\") or silently no-op, even though\n // the constructor already returned an object. `onReady` is YouTube's\n // own signal that the real API surface is actually callable now, so\n // wiring the contract here — not synchronously after construction —\n // is what makes `findPlayable()`'s \"is `.play` wired?\" check in\n // Autoplay (DESIGN.md §4.1) an accurate proxy for \"is it really\n // ready?\" instead of just \"did we build the wrapper closures?\".\n onReady: () => {\n wirePlayableContract(container, player);\n onReady();\n },\n onStateChange: (event) => handleStateChange(event, YT, playable),\n // bubbles: true — reaches .shoji-slide-media (the pool slot, always\n // present regardless of readiness) without needing wirePlayableContract\n // to have run first; onError can fire instead of onReady entirely\n // (e.g. a removed/private video), so playable isn't a safe assumption\n // here the way it is in onStateChange. Autoplay (DESIGN.md §4.1)\n // listens for this to skip to the next slide instead of stalling.\n onError: (event) =>\n playable.dispatchEvent(\n new CustomEvent('error', { bubbles: true, detail: { code: event.data } }),\n ),\n },\n });\n signal.addEventListener('abort', () => player.destroy(), { once: true });\n });\n};\n","import type { PluginContext, ShojiPlugin } from '../../core/plugin';\nimport { renderVimeo } from './vimeo';\nimport { renderYouTube } from './youtube';\n\n/**\n * DESIGN.md §4-video — renders `{ video: { provider: 'youtube' | 'vimeo', id } }`\n * items (produced by `scan.ts`'s `data-shoji-video` host detection, or\n * authored directly in dynamic mode) as a real embed instead of\n * `SlideManager`'s native-`<video>` fallback, which can't play either URL at\n * all. Purely a renderer: no options, no toolbar button, no poster/\n * thumbnail handling — the slide shows nothing until the embed itself is\n * ready, same as every other slide type. Autoplay (§4-autoplay) picks up\n * real play/pause/ended sync automatically; see `youtube.ts`'s/`vimeo.ts`'s\n * `wirePlayableContract`.\n */\nexport const Video: ShojiPlugin = {\n name: 'video',\n\n init(ctx: PluginContext): () => void {\n const offYouTube = ctx.ui.registerVideoProvider('youtube', renderYouTube);\n const offVimeo = ctx.ui.registerVideoProvider('vimeo', renderVimeo);\n return () => {\n offYouTube();\n offVimeo();\n };\n },\n};\n","/** DESIGN.md §9 — inline SVG, stroke = currentColor, matches src/core/icons.ts's convention. A magnifying glass with a +/− in the lens, and a plain expand-corners glyph for \"actual size\" (distinct from fullscreen's EXPAND_ICON — no diagonal corner arrows, just a frame, so the two aren't visually confusable when both plugins are enabled). */\nexport const ZOOM_IN_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\"><circle cx=\"10\" cy=\"10\" r=\"7\"/><path d=\"M21 21l-5.5-5.5M10 7v6M7 10h6\"/></svg>';\n\nexport const ZOOM_OUT_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\"><circle cx=\"10\" cy=\"10\" r=\"7\"/><path d=\"M21 21l-5.5-5.5M7 10h6\"/></svg>';\n\nexport const ZOOM_ACTUAL_SIZE_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=\"4\" y=\"4\" width=\"16\" height=\"16\" rx=\"1\"/><path d=\"M9 15l6-6M9 9h0M15 15h0\"/></svg>';\n","/** A viewport-relative rect: `{left, top, width, height}`, same shape as `DOMRect` but plain-object so it's trivial to mock in tests. */\nexport interface ZoomBox {\n left: number;\n top: number;\n width: number;\n height: number;\n}\n\nexport interface PanOffset {\n tx: number;\n ty: number;\n}\n\nexport function clampScale(scale: number, min: number, max: number): number {\n return Math.min(max, Math.max(min, scale));\n}\n\n/**\n * One axis of `clampPan` below. `natural*` is the element's own unscaled\n * rendered box (captured once at scale 1, before any transform — `apply()`\n * in `index.ts` renders `translate(tx, ty) scale(s)` with `transform-origin:\n * 0 0`, so the natural box's top-left is exactly where the scaled box's own\n * local origin sits on screen, and `tx`/`ty` are the *only* thing moving it\n * from there). If the scaled content is still smaller than the container on\n * this axis, no pan range can avoid a gap on *both* sides at once — centered\n * is the only sensible answer, not an inverted min>max range.\n */\nfunction clampAxis(\n naturalPos: number,\n naturalSize: number,\n scale: number,\n containerPos: number,\n containerSize: number,\n offset: number,\n): number {\n const scaledSize = naturalSize * scale;\n if (scaledSize <= containerSize) {\n return containerPos + (containerSize - scaledSize) / 2 - naturalPos;\n }\n const minPos = containerPos + containerSize - scaledSize;\n const maxPos = containerPos;\n const proposedPos = naturalPos + offset;\n return Math.min(maxPos, Math.max(minPos, proposedPos)) - naturalPos;\n}\n\n/** Keeps the scaled+panned content's edges from retreating inside `container`'s edges (no empty gap beyond the image), on both axes independently. */\nexport function clampPan(\n natural: ZoomBox,\n container: ZoomBox,\n scale: number,\n pan: PanOffset,\n): PanOffset {\n return {\n tx: clampAxis(natural.left, natural.width, scale, container.left, container.width, pan.tx),\n ty: clampAxis(natural.top, natural.height, scale, container.top, container.height, pan.ty),\n };\n}\n\n/**\n * The standard \"zoom toward a point\" formula: adjusts pan so the viewport\n * point `(anchorX, anchorY)` stays visually fixed as scale changes from\n * `oldScale` to `newScale`, given `transform-origin: 0 0` (see `natural`'s\n * doc comment on `clampAxis` above for why that makes this tractable —\n * scaling never moves the natural box's own top-left on screen, only `pan`\n * does). Callers still need to clamp the result with `clampPan` afterward;\n * this only solves the anchor-fixed part, not boundary containment.\n */\nexport function zoomTowardPoint(\n natural: ZoomBox,\n pan: PanOffset,\n oldScale: number,\n newScale: number,\n anchorX: number,\n anchorY: number,\n): PanOffset {\n const currentLeft = natural.left + pan.tx;\n const currentTop = natural.top + pan.ty;\n const dx = anchorX - currentLeft;\n const dy = anchorY - currentTop;\n const factor = 1 - newScale / oldScale;\n return {\n tx: pan.tx + dx * factor,\n ty: pan.ty + dy * factor,\n };\n}\n","import type { PluginContext, ShojiPlugin } from '../../core/plugin';\nimport { waitForTransitionEnd } from '../../core/zoomTransition';\nimport { ZOOM_ACTUAL_SIZE_ICON, ZOOM_IN_ICON, ZOOM_OUT_ICON } from './icons';\nimport { clampPan, clampScale, zoomTowardPoint, type PanOffset, type ZoomBox } from './zoomMath';\nimport './zoom.css';\n\nexport interface ZoomOptions {\n /** Multiplier cap for pinch/wheel/button zoom. \"Actual size\" can exceed this deliberately — it's an explicit action, not continuous gesture zoom. Default 4. */\n maxScale?: number;\n /** Scale a double-tap/double-click jumps to; a second one while already zoomed resets to 1 instead. Default 2. */\n doubleTapScale?: number;\n /** Multiplier applied per zoom-in/zoom-out toolbar button click. Default 1.5. */\n buttonStep?: number;\n}\n\nconst ZOOM_EPSILON = 1.001; // treat \"just barely above 1\" as unzoomed — avoids float residue pinning isZoomed() true forever\n\n/** A click/drag starting on a real control shouldn't engage pan — same exclusion list GestureController's shouldIgnoreGesture uses, duplicated rather than imported since that function isn't part of core's exported surface. */\nfunction isRealControl(event: PointerEvent): boolean {\n return event\n .composedPath()\n .some(\n (node) =>\n node instanceof Element &&\n node.matches('button, video, input, select, textarea, a[href], [data-shoji-no-drag]'),\n );\n}\n\n/**\n * DESIGN.md §4-zoom — pinch, double-tap/click, wheel+ctrl, and three toolbar\n * buttons (zoom in/out/actual-size) all drive a single `scale`/pan state on\n * the active slide's `<img>` — never `.shoji-slide-media` itself, which the\n * rotateFlip plugin (§4) already transforms; nesting on the inner element\n * instead of fighting over the same transform string means a rotated *and*\n * zoomed photo behaves correctly for free (the outer rotate carries the\n * inner pan/scale along with it as a rigid unit, which is also the visually\n * expected result — see DESIGN.md's note on this plugin for the full\n * reasoning). Pinch/double-tap/wheel are core's own gesture relay (§2.4) —\n * scaffolding that existed specifically for this plugin to consume, not\n * reimplemented here. Pan (single-pointer drag while zoomed) is the one\n * piece core's relay doesn't cover — core's own drag-to-navigate/\n * drag-to-close would otherwise fight over the same drag — so this plugin\n * registers a zoom gate (`Gallery.registerZoomGate`, §4-zoom) that suspends\n * core's drag handling entirely while zoomed, and tracks pan with its own\n * minimal raw pointer listeners instead of reusing `GestureEngine` (whose\n * axis-locked model — pick horizontal *or* vertical per gesture — is the\n * wrong shape for a 2D pan that needs both at once).\n */\nexport const Zoom: ShojiPlugin = {\n name: 'zoom',\n defaults: {\n maxScale: 4,\n doubleTapScale: 2,\n buttonStep: 1.5,\n } satisfies ZoomOptions,\n\n init(ctx: PluginContext): () => void {\n const { gallery } = ctx;\n const maxScale = Number(ctx.options.maxScale ?? 4);\n const doubleTapScale = Number(ctx.options.doubleTapScale ?? 2);\n const buttonStep = Number(ctx.options.buttonStep ?? 1.5);\n const locale = (gallery.options.locale ?? {}) as Record<string, string>;\n const zoomInLabel = locale.zoomIn ?? 'Zoom in';\n const zoomOutLabel = locale.zoomOut ?? 'Zoom out';\n const actualSizeLabel = locale.zoomActualSize ?? 'Actual size';\n\n let scale = 1;\n let pan: PanOffset = { tx: 0, ty: 0 };\n let natural: ZoomBox | null = null;\n let container: ZoomBox | null = null;\n let pinchStartScale = 1;\n\n function getImg(): HTMLImageElement | null {\n const media = gallery.getActiveMedia();\n const child = media?.firstElementChild;\n return child instanceof HTMLImageElement ? child : null;\n }\n\n function boxOf(el: Element): ZoomBox {\n const rect = el.getBoundingClientRect();\n return { left: rect.left, top: rect.top, width: rect.width, height: rect.height };\n }\n\n /** Only valid to measure while scale===1 (untransformed) — the very first zoom action on a slide; every subsequent action within the same slide reuses the cached box, since measuring an already-scaled element would capture the scaled size, not the natural one. */\n function ensureNatural(img: HTMLImageElement): boolean {\n if (natural && container) return true;\n if (scale !== 1) return false; // shouldn't happen — defensive\n natural = boxOf(img);\n container = boxOf(img.parentElement ?? img);\n return true;\n }\n\n /**\n * A real bug: `ensureNatural`'s first measurement of a slide is only\n * trustworthy once the lightbox's own open FLIP transition (`zoomIn`,\n * `core/zoomTransition.ts`) has actually settled — that transition\n * applies its own transform directly to `.shoji-slide-media`, the exact\n * element `ensureNatural` measures as `container`. A zoom action firing\n * before it settles (any interaction within `--shoji-duration` of\n * opening — a fast click, or a test that only waits for the dialog to\n * become visible) captured a wildly wrong, mid-animation rect,\n * permanently poisoning that slide's zoom math (every later action\n * reuses the same cached, wrong box). `zoomIn` is fire-and-forget by\n * design (nothing to await when opening) and clears this exact inline\n * style once its own transition ends — the one signal available for\n * \"is it still running.\" Runs `action` immediately once settled, which\n * is right away in the overwhelming common case (any real interaction\n * more than ~300ms after open).\n */\n function whenSettled(action: () => void): void {\n const media = gallery.getActiveMedia();\n if (!media || media.style.transition === '') {\n action();\n return;\n }\n waitForTransitionEnd(media, action);\n }\n\n /**\n * `translate3d`/`scale3d`, not the 2D `translate`/`scale` this used to\n * use — a real bug, reported from real usage: evenly-spaced horizontal\n * lines visible across a zoomed photo, at certain zoom levels, on real\n * GPU hardware (not reproducible in headless/software rendering, so\n * this can't be verified here). The regular spacing matches Chromium's\n * own raster-tile boundaries — a known quirk where scaling large\n * content via a 2D `scale()` transform can show seams between GPU\n * tiles. Forcing the fully 3D compositing path instead (functionally\n * identical — `scale3d(s, s, 1)` and `scale(s)` produce the same\n * on-screen result) is the commonly effective fix, since it takes a\n * different rasterization path than the 2D one.\n */\n function apply(): void {\n const img = getImg();\n if (!img) return;\n img.style.transformOrigin = '0 0';\n img.style.transform =\n scale === 1 && pan.tx === 0 && pan.ty === 0\n ? 'none'\n : `translate3d(${pan.tx}px, ${pan.ty}px, 0) scale3d(${scale}, ${scale}, 1)`;\n img.classList.toggle('shoji-zoomed', scale > 1);\n }\n\n function emitChange(): void {\n ctx.emit('zoomChange', { index: gallery.currentIndex, scale });\n }\n\n /** Wraps a transform-setting `run` in a transition, for discrete jumps (buttons, double-tap, actual-size) — never for pinch/pan/wheel, which already track the input 1:1 and would visibly lag behind it under a transition. `afterEnd`, if given, runs once the transition actually completes, not before — `reset()` uses it to clear `transformOrigin` only once it's safe to (see its own comment for why clearing it any earlier is a real bug). The transition itself is always cleared afterward, so it doesn't linger onto the next, possibly-continuous, zoom action. */\n function withTransition(img: HTMLImageElement, run: () => void, afterEnd?: () => void): void {\n img.style.transition = 'transform var(--shoji-duration) var(--shoji-easing)';\n run();\n waitForTransitionEnd(img, () => {\n img.style.transition = '';\n afterEnd?.();\n });\n }\n\n /** Shared by every zoom-in/out entry point (pinch, wheel, buttons, double-tap, actual-size) — anchors on (anchorX, anchorY), clamps scale to [1, ceiling] and pan to the container bounds. `ceiling` defaults to maxScale; actual-size passes its own (possibly larger) target so it isn't capped by the gesture-zoom limit. Deferred via `whenSettled` — see its doc comment — so a zoom action landing right as the lightbox opens doesn't measure mid-animation. `animate` — see `withTransition`. */\n function zoomTo(\n targetScale: number,\n anchorX: number,\n anchorY: number,\n ceiling = maxScale,\n animate = false,\n ): void {\n whenSettled(() => {\n const img = getImg();\n if (!img || !ensureNatural(img)) return;\n const clampedScale = clampScale(targetScale, 1, Math.max(ceiling, 1));\n pan = zoomTowardPoint(natural!, pan, scale, clampedScale, anchorX, anchorY);\n scale = clampedScale;\n pan = clampPan(natural!, container!, scale, pan);\n if (animate) withTransition(img, apply);\n else apply();\n emitChange();\n });\n }\n\n function reset(animate = false): void {\n scale = 1;\n pan = { tx: 0, ty: 0 };\n natural = null;\n container = null;\n const img = getImg();\n if (!img) return;\n const clearTransform = (): void => {\n img.style.transform = '';\n img.classList.remove('shoji-zoomed');\n };\n if (animate) {\n // transform-origin has to stay put (0 0) for the duration of the\n // transition — clearing it to the browser default (center) in the\n // same tick as starting the transition snaps the scale anchor\n // instantly, which visibly jumped the image before it eased down\n // to neutral. Deferred to `afterEnd`, once the transition is done\n // and transform-origin no longer affects anything visible.\n withTransition(img, clearTransform, () => {\n img.style.transformOrigin = '';\n });\n } else {\n clearTransform();\n img.style.transformOrigin = '';\n }\n }\n\n /** Each slide gets a freshly-created `<img>` (SlideManager never reuses elements across renders), so the cursor-affordance marker (`zoom.css`) needs reapplying every time the active media changes, not just once. */\n function markEnabled(): void {\n getImg()?.classList.add('shoji-zoom-enabled');\n }\n\n function toggleZoom(x: number, y: number): void {\n if (scale > ZOOM_EPSILON) reset(true);\n else zoomTo(doubleTapScale, x, y, maxScale, true);\n }\n\n // --- pinch (relayed by core, §2.4 — no built-in effect until this plugin exists) ---\n const offPinchStart = ctx.on('pinchStart', () => {\n pinchStartScale = scale;\n });\n const offPinchMove = ctx.on('pinchMove', ({ scale: relative, centerX, centerY }) => {\n zoomTo(pinchStartScale * relative, centerX, centerY);\n });\n const offPinchEnd = ctx.on('pinchEnd', () => {\n if (scale <= ZOOM_EPSILON) reset(); // snap fully back to neutral rather than leaving float residue\n });\n\n // --- double-tap / double-click (relayed by core; Pointer Events unify the two, see GestureEngine) ---\n const offDoubleTap = ctx.on('doubleTap', ({ x, y }) => toggleZoom(x, y));\n\n // --- ctrl+wheel / trackpad pinch (relayed by core) ---\n const offWheelZoom = ctx.on('wheelZoom', ({ deltaScale, x, y }) => {\n zoomTo(scale + deltaScale, x, y);\n });\n\n // --- pan while zoomed — the one gesture core's relay doesn't cover; see the plugin doc comment for why this can't reuse GestureEngine. ---\n let panPointerId: number | null = null;\n let lastX = 0;\n let lastY = 0;\n const outer = ctx.ui.outer();\n\n function onPointerDown(event: PointerEvent): void {\n if (scale <= ZOOM_EPSILON || isRealControl(event)) return;\n panPointerId = event.pointerId;\n lastX = event.clientX;\n lastY = event.clientY;\n }\n function onPointerMove(event: PointerEvent): void {\n if (panPointerId !== event.pointerId || !natural || !container) return;\n event.preventDefault();\n const dx = event.clientX - lastX;\n const dy = event.clientY - lastY;\n lastX = event.clientX;\n lastY = event.clientY;\n pan = clampPan(natural, container, scale, { tx: pan.tx + dx, ty: pan.ty + dy });\n apply();\n }\n function onPointerUp(event: PointerEvent): void {\n if (panPointerId === event.pointerId) panPointerId = null;\n }\n\n outer.addEventListener('pointerdown', onPointerDown);\n outer.addEventListener('pointermove', onPointerMove, { passive: false });\n outer.addEventListener('pointerup', onPointerUp);\n outer.addEventListener('pointercancel', onPointerUp);\n\n // --- toolbar buttons ---\n function buildButton(icon: string, label: string): HTMLButtonElement {\n const button = document.createElement('button');\n button.type = 'button';\n button.className = 'shoji-toolbar-button';\n button.innerHTML = icon;\n button.setAttribute('aria-label', label);\n button.title = label;\n return button;\n }\n\n function centerAnchor(): { x: number; y: number } {\n const media = gallery.getActiveMedia();\n const rect = media?.getBoundingClientRect();\n return rect\n ? { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }\n : { x: 0, y: 0 };\n }\n\n /** Shared by the zoom-in toolbar button and the `w` keyboard shortcut — same fixed `buttonStep` multiplier either way. */\n function zoomInStep(): void {\n const { x, y } = centerAnchor();\n zoomTo(scale * buttonStep, x, y, maxScale, true);\n }\n\n /** Shared by the zoom-out toolbar button and the `s` keyboard shortcut. */\n function zoomOutStep(): void {\n const { x, y } = centerAnchor();\n if (scale / buttonStep <= ZOOM_EPSILON) reset(true);\n else zoomTo(scale / buttonStep, x, y, maxScale, true);\n }\n\n const zoomInBtn = buildButton(ZOOM_IN_ICON, zoomInLabel);\n const zoomOutBtn = buildButton(ZOOM_OUT_ICON, zoomOutLabel);\n const actualSizeBtn = buildButton(ZOOM_ACTUAL_SIZE_ICON, actualSizeLabel);\n\n zoomInBtn.addEventListener('click', zoomInStep);\n zoomOutBtn.addEventListener('click', zoomOutStep);\n actualSizeBtn.addEventListener('click', () => {\n // Reads natural.width directly (unlike every other entry point, which\n // just hands zoomTo a target and lets it call ensureNatural itself) —\n // needs its own whenSettled wrap for that reason, not just zoomTo's.\n whenSettled(() => {\n const img = getImg();\n if (!img || !img.naturalWidth) return;\n if (!ensureNatural(img)) return;\n if (scale > ZOOM_EPSILON) {\n reset(true);\n return;\n }\n const targetScale = img.naturalWidth / natural!.width;\n const { x, y } = centerAnchor();\n zoomTo(targetScale, x, y, targetScale, true); // ceiling = targetScale — bypasses maxScale deliberately\n });\n });\n\n // 'right' — registered in this order, so they cluster left-to-right as\n // zoomIn, zoomOut, actualSize, then whatever later plugin (or the close\n // button) follows (DESIGN.md §3.1).\n const removeButtons = [zoomInBtn, zoomOutBtn, actualSizeBtn].map((button) =>\n ctx.ui.toolbar('right', button),\n );\n\n // w/s zoom in/out, same step as the toolbar buttons — both cases\n // registered explicitly (registerShortcut matches event.key verbatim,\n // no case-insensitive matching of its own) so Shift/CapsLock still work.\n const removeShortcuts = [\n ctx.ui.registerShortcut('w', zoomInStep),\n ctx.ui.registerShortcut('W', zoomInStep),\n ctx.ui.registerShortcut('s', zoomOutStep),\n ctx.ui.registerShortcut('S', zoomOutStep),\n ];\n\n const offOpen = ctx.on('afterOpen', () => {\n reset();\n markEnabled();\n });\n // Un-animated, and on beforeSlide rather than only afterSlide below:\n // SlideManager.render() (called synchronously between the two) reuses a\n // still-cached slide's node via a plain reparent (moveIn(), no state\n // clearing of its own) into whichever pool slot its new offset needs —\n // there is no code path afterward that can still find *this* image to\n // reset it. A real bug, reported from real usage: zoom in via \"Actual\n // size\", click next — the old, still-scaled image, now reparented into\n // the (unclipped, per shoji.css) neighboring slot, visibly bled into the\n // new slide instead of being invisible off-screen like an unzoomed one\n // always is. Resetting here, while getActiveMedia() still resolves to\n // the about-to-move image, clears it before that reparent ever happens.\n const offBeforeSlide = ctx.on('beforeSlide', () => reset());\n const offSlide = ctx.on('afterSlide', () => {\n reset();\n markEnabled();\n });\n // Fires synchronously, before Gallery.close() measures the active\n // media's rect to compute the zoom-out-to-thumbnail animation — reset\n // here (not just afterOpen/afterSlide) so that measurement sees the\n // image at its natural position/scale, not wherever it was left\n // zoomed/panned to. Skipping this made closing while zoomed animate\n // from the image's current (zoomed, often partly off-screen) rect\n // instead of its real thumbnail-relative size, landing \"closed\" at a\n // seemingly random spot instead of visibly shrinking into the thumbnail.\n const offBeforeClose = ctx.on('beforeClose', () => reset());\n const unregisterGate = gallery.registerZoomGate(() => scale > ZOOM_EPSILON);\n markEnabled(); // covers the (unusual but possible) case of the gallery already being open when this plugin initializes\n\n return () => {\n for (const remove of removeButtons) remove();\n for (const remove of removeShortcuts) remove();\n offOpen();\n offBeforeSlide();\n offSlide();\n offBeforeClose();\n offPinchStart();\n offPinchMove();\n offPinchEnd();\n offDoubleTap();\n offWheelZoom();\n outer.removeEventListener('pointerdown', onPointerDown);\n outer.removeEventListener('pointermove', onPointerMove);\n outer.removeEventListener('pointerup', onPointerUp);\n outer.removeEventListener('pointercancel', onPointerUp);\n unregisterGate();\n reset();\n getImg()?.classList.remove('shoji-zoom-enabled');\n };\n },\n};\n","import { Gallery } from './core';\nimport { ActiveThumbnail } from './plugins/activeThumbnail';\nimport { Autoplay } from './plugins/autoplay';\nimport { Fullscreen } from './plugins/fullscreen';\nimport { Layout } from './plugins/layout';\nimport { RotateFlip } from './plugins/rotateFlip';\nimport { Video } from './plugins/video';\nimport { Zoom } from './plugins/zoom';\nimport { version } from '../package.json';\n\nimport './styles/shoji.css';\n\nexport type {\n GalleryItem,\n GalleryOptions,\n GalleryEvents,\n MediaSource,\n VideoDescriptor,\n Unsubscribe,\n} from './core';\nexport type { ShojiPlugin, PluginContext, VideoProviderRenderer } from './core/plugin';\n\n/**\n * `new Shoji(el, options)` — the UMD global (CLAUDE.md: \"UMD build exposes\n * exactly one global\"). The default export *is* the constructor, not a\n * namespace object, so `<script src=\"shoji.js\">` usage matches the default\n * import used by bundler/ESM consumers. Headless/tree-shaking consumers who\n * want the class as a named import can pull it from the `shoji/core`\n * subpath instead.\n *\n * Official plugins are attached as static properties (`Shoji.Autoplay`) so\n * the single-file build stays \"one global\" (CLAUDE.md) while still shipping\n * every official plugin's code — self-registering, no extra <script> tags —\n * per CLAUDE.md's single-file distribution requirement. A host opts a\n * gallery into one with `new Shoji(el, { plugins: [Shoji.Autoplay] })`.\n * Bundler users who only want to pull in specific plugins' code (tree-shaking\n * this static attachment away) should import from the `shoji/plugins/*`\n * subpath instead of this default export.\n */\nconst Shoji = Object.assign(Gallery, {\n Autoplay,\n Layout,\n ActiveThumbnail,\n Fullscreen,\n RotateFlip,\n Video,\n Zoom,\n /** `package.json`'s own version — the single source of truth, read via a named JSON import so a bundler can tree-shake away everything else in package.json (devDependencies, scripts, ...) rather than embedding the whole file. */\n version,\n});\n\nexport default Shoji;\n"],"names":["lockCount","savedOverflow","savedHtmlOverflow","lockBodyScroll","unlockBodyScroll","EventBus","__publicField","event","fn","set","_a","detail","CLOSE_ICON","PREV_ICON","NEXT_ICON","PLAY_ICON","CAPTION_ICON","idCounter","iconButton","className","icon","label","button","buildLightboxDom","slides","labels","labelId","outer","backdrop","dialog","toolbar","toolbarLeft","toolbarCenter","toolbarRight","closeButton","captionToggleButton","counter","prevButton","nextButton","caption","FOCUSABLE_SELECTOR","FocusTrap","focusable","first","last","active","activeIsInside","container","DOUBLE_TAP_MAX_DELAY_MS","DOUBLE_TAP_MAX_DISTANCE","TAP_MAX_DELAY_MS","GestureEngine","target","callbacks","options","_b","a","b","distanceBetween","center","centerOf","_d","_c","state","distance","scale","dx","dy","absX","absY","_f","_e","delta","_h","_g","deltaScale","cancelled","elapsed","totalDelta","velocity","completed","x","y","now","prefersReducedMotion","parseCssTime","value","containedBox","aspectRatio","containerRatio","width","height","effectiveTargetBox","child","isPlaceholder","rect","containerRect","computeTransform","origin","originRect","targetRect","translateX","translateY","waitForTransitionEnd","cb","durationMs","done","finish","onEnd","clearInlineTransform","expectedTransform","zoomIn","transform","zoomOut","onComplete","appliedTransform","DRAG_SETTLE_TRANSITION","DRAG_FEEDBACK_TRANSITION","VERTICAL_FEEDBACK_DISTANCE","INTERACTIVE_CONTROL_SELECTOR","shouldIgnoreGesture","node","GestureController","host","relay","direction","_velocity","centerX","centerY","goingNext","canMove","targetPx","onSettled","settleEl","progress","animate","LiveRegion","text","DEFAULT_SELECTOR","scanContainer","selector","elements","scanned","element","item","scanVideo","scanImage","attr","el","name","numAttr","raw","NO_OWN_DATA_FIELD","applyCommon","src","data","key","img","thumb","alt","srcset","sizes","videoIdAttr","videoEl","sources","s","poster","videoUrl","providerAttr","resolved","resolveExplicitVideo","KNOWN_VIDEO_PROVIDERS","html5Fallback","url","guessVideoType","idAttr","youtube","resolveDetectedProvider","isYouTubeUrl","detectYouTubeId","vimeo","isVimeoUrl","detectVimeoId","provider","isHost","detectId","explicit","id","YOUTUBE_HOSTS","VIMEO_HOSTS","normalizeVideoHost","hostname","parsed","match","DETECTABLE_PROVIDERS","resolveDynamicVideoItems","items","detected","p","video","clean","ext","PAUSE_OVERLAY_DELAY_MS","SlideManager","count","_","i","offset","root","media","slot","px","transition","centerIndex","onLoad","openPlaceholderSrc","index","entry","releaseVideoNode","targets","claimedIndices","claimedSlots","owner","freeSlots","t","releaseVideo","createSpinner","extra","reveal","createOpenPlaceholder","placeholder","source","overlay","pauseTimer","clearPauseTimer","renderFn","controller","providerAbortControllers","revealed","spinner","pauseMedia","releaseProviderNode","resolveAxis","config","sign","parts","tx","ty","preset","spec","PRESET_LIST","TRANSITION_PRESETS","waitForEnd","style","applyEnter","opacity","applyLeave","SlideTransition","swapContent","ghost","incoming","leave","enter","mode","dirAttr","kickOff","cleanupIncoming","outgoing","itemKey","resolveElement","instanceRegistry","allInstances","DEFAULT_LOCALE","isBackdropClick","isDangerousHtmlCaption","Gallery","shortcut","isZoomed","dom","plugin","missing","pluginOptions","ctx","layer","render","cleanup","isRawElement","unhover","hovering","onEnter","onLeave","marked","loading","noCaption","isVideo","hiddenByToggle","btn","loadedIndex","total","dataThumb","clamped","from","swap","modeName","builtin","thumbImg","currentIndex","activeItem","activeKey","nextIndex","preserved","atIndex","next","matches","indices","m","keys","ActiveThumbnail","gallery","activeClass","scrollIntoView","current","scrollTimer","cancelPendingScroll","apply","clear","offOpen","offSlide","to","offClose","PAUSE_ICON","findPlayable","isPendingProviderVideo","PROVIDER_PLAY_RETRY_MS","MAX_PROVIDER_PLAY_ATTEMPTS","Autoplay","interval","showProgress","autoStart","locale","playLabel","pauseLabel","playing","timer","currentVideo","awaitingProviderVideo","onVideoError","advance","progressBar","clearTimer","resetProgressBar","runProgressBar","ms","setButtonState","isPlaying","onVideoEnded","onVideoPause","stop","detachVideo","ensureProviderPlaying","attemptsLeft","playResult","pollWithoutReissuing","reissueOnEachRetry","enterSlide","error","before","start","toggle","removeButton","removeProgress","removeShortcut","offBeforeSlide","offSlideItemLoad","EXPAND_ICON","COMPRESS_ICON","isSupported","currentFullscreenElement","requestFullscreen","exitFullscreen","doc","Fullscreen","enterLabel","exitLabel","isActive","setState","onChange","computeColumnCount","containerWidth","gutterX","columns","maxAllowedByMin","minRequiredByMax","computeColumnWidth","columnCount","layoutMasonry","tiles","startHeights","colWidth","columnHeights","positions","tile","colIndex","indexOfShortest","tileHeight","containerHeight","heights","shortest","layoutMasonryHorizontal","gutterY","rowHeight","currentY","rowStart","rowWidth","rowCount","tileWidth","placeRow","endExclusive","w","countInRow","widthIfIncluded","aspectRatioOf","layoutJustified","minRowHeight","maxRowHeight","lastRow","rows","aspectSum","naturalHeightOf","startIndex","sum","resolveScaledRow","end","rowSum","natural","j","DEFAULT_ASPECT","resolveGutter","obj","n","keyOf","isPureAppend","oldItems","newItems","Layout","type","gutter","orientation","columnWidth","minColumnWidth","maxColumnWidth","fill","autoMeasureEnabled","horizontal","groupByFn","renderHeadingFn","headingOverflow","stickyHeadingsRaw","stickyHeadings","groupingEnabled","baseConfig","breakpointsRaw","sortedBreakpoints","overrides","resolveConfig","maxWidth","headings","previousItems","warnedMissingDims","measuredAspect","computeSections","sections","createHeadingRoot","sectionItems","rendered","structured","titleEl","subtitleEl","headingSegments","heading","measurableSrc","aspectOf","measured","selfCorrecting","createTile","applyGrid","applyMasonry","appendOnly","masonryOptions","segments","headingHeights","segTiles","segResult","pos","masonryTiles","result","applyMasonryHorizontal","applyJustifiedGrouped","justifiedOptions","fullWidths","titleOnlyWidths","tileAspects","tileStart","tileEnd","tryFit","startSection","endSection","k","nextPos","available","computeSegments","segStart","base","best","trial","applyContentDecisions","segment","applyWrapDecisions","parsedLineHeight","lineHeight","maxLines","cumulativeExtra","headingPtr","row","globalStart","globalEnd","bandHeight","segLastCovered","applyJustified","justifiedTiles","relayoutAll","destroyed","measureFrame","scheduleMeasureRelayout","fullRender","autoMeasureTiles","newTiles","emitLayoutRender","section","headingRoot","appendRender","offItemsUpdated","resizeFrame","resizeObserver","normalizeRotateFlip","rotation","ROTATE_LEFT_ICON","ROTATE_RIGHT_ICON","FLIP_H_ICON","FLIP_V_ICON","NEUTRAL","transformFor","flipH","flipV","rotationDeg","fitScale","scaleX","scaleY","fitScaleFor","mediaWidth","mediaHeight","naturalWidth","naturalHeight","scaleAt0","RotateFlip","rotateLeftLabel","rotateRightLabel","flipHLabel","flipVLabel","visualRotation","visualFlipH","visualFlipV","resolveNaturalSize","buildButton","rotateLeftBtn","rotateRightBtn","flipHBtn","flipVBtn","update","patch","visualRotationDelta","rotateDelta","clockwise","reset","removeButtons","remove","apiPromise","PAUSE_DISPATCH_DELAY_MS","loadVimeoApi","resolve","script","wirePlayableContract","player","playable","mutedState","renderVimeo","onReady","signal","mount","Vimeo","pendingPauseTimer","clearPendingPause","loadYouTubeApi","previous","handleStateChange","YT","renderYouTube","videoId","Video","offYouTube","offVimeo","ZOOM_IN_ICON","ZOOM_OUT_ICON","ZOOM_ACTUAL_SIZE_ICON","clampScale","min","max","clampAxis","naturalPos","naturalSize","containerPos","containerSize","scaledSize","minPos","maxPos","proposedPos","clampPan","pan","zoomTowardPoint","oldScale","newScale","anchorX","anchorY","currentLeft","currentTop","factor","ZOOM_EPSILON","isRealControl","maxScale","doubleTapScale","buttonStep","zoomInLabel","zoomOutLabel","actualSizeLabel","pinchStartScale","getImg","boxOf","ensureNatural","whenSettled","action","emitChange","withTransition","run","afterEnd","zoomTo","targetScale","ceiling","clampedScale","clearTransform","markEnabled","toggleZoom","offPinchStart","offPinchMove","relative","offPinchEnd","offDoubleTap","offWheelZoom","panPointerId","lastX","lastY","onPointerDown","onPointerMove","onPointerUp","centerAnchor","zoomInStep","zoomOutStep","zoomInBtn","zoomOutBtn","actualSizeBtn","removeShortcuts","offBeforeClose","unregisterGate"],"mappings":"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"}
|