@michaelyagi/shoji 0.1.0-alpha.6
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 +81 -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 +50 -0
- package/dist/esm/core/LiveRegion.d.ts +6 -0
- package/dist/esm/core/SlideManager.d.ts +83 -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 +1966 -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 +8 -0
- package/dist/esm/core/types.d.ts +268 -0
- package/dist/esm/core/zoomTransition.d.ts +33 -0
- package/dist/esm/gestures/GestureEngine.d.ts +78 -0
- package/dist/esm/index.css +475 -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 +166 -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 +48 -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 +17 -0
- package/dist/esm/plugins/autoplay/index.js +177 -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 +746 -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 +95 -0
- package/dist/esm/plugins/video/index.js.map +1 -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 +690 -0
- package/dist/shoji.js +3605 -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/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 this.previouslyFocused?.focus();\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/** A click/drag starting on a real control shouldn't also be captured as a gesture — buttons/inputs/links keep their native behavior, `<video>` keeps its own native touch controls, and `data-shoji-no-drag` is an explicit host/plugin escape hatch (CLAUDE.md's `data-shoji-*` prefix). */\nfunction shouldIgnoreGesture(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/** 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, MediaSource } 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; `no-drag` (GestureController's\n * drag-opt-out) is the one exception with no field of its own.\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 (key !== 'no-drag' && !(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/** DESIGN.md §2.1 — a nested `<video>` is always html5; `data-shoji-video` checks known provider hosts first. */\nfunction scanVideo(element: HTMLElement): GalleryItem | undefined {\n const videoEl = element.querySelector('video');\n if (videoEl) {\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 youtubeId = detectYouTubeId(videoUrl);\n const item: GalleryItem = youtubeId\n ? { src: videoUrl, video: { provider: 'youtube', id: youtubeId, url: videoUrl } }\n : {\n src: videoUrl,\n video: { provider: 'html5' },\n sources: [{ src: videoUrl, type: guessVideoType(videoUrl) }],\n };\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 return undefined;\n}\n\n/** `youtu.be/{id}`, `watch?v=`, `/embed/`, `/shorts/` — `undefined` otherwise. */\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 = parsed.hostname.replace(/^(www|m)\\./, '');\n if (host === 'youtu.be') return parsed.pathname.slice(1).split('/')[0] || undefined;\n if (host !== 'youtube.com') return undefined;\n if (parsed.pathname === '/watch') return parsed.searchParams.get('v') ?? undefined;\n const match = parsed.pathname.match(/^\\/(?:embed|shorts)\\/([^/?]+)/);\n return match?.[1];\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 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. Rather\n * than diffing which physical node maps to which item, each pool slot has a\n * fixed structural offset (-preload…+preload) and its *content* is what gets\n * reassigned on navigation — same pattern most virtualized carousels use,\n * and it keeps re-render cost O(preload), never O(item count).\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 /**\n * Ready nodes keyed by item index, not slot offset — a `Slot` only\n * remembers its *own* previous index, so it can't tell that some *other*\n * slot already decoded the exact index it's now asked to show (the\n * routine case: stepping forward moves the +1 slot's content into the 0\n * slot). Without this, already-decoded content is thrown away and\n * redecoded on every step. Trimmed to `centerIndex ± preload` each\n * `render()`, same window the slots cover.\n */\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((slot) => {\n const index = centerIndex + slot.offset;\n const item = index >= 0 && index < items.length ? items[index] : undefined;\n return { slot, index, item };\n });\n\n // Phase 1 — claim every cache hit first, before anything gets\n // destructively released. Stepping backward can need slot A's current\n // content to become slot B's new content in this same call — releasing\n // it (pausing a <video>) before B reclaims it would destroy content\n // still needed a few lines later.\n for (const { slot, index, item } of targets) {\n if (!item || slot.assignedIndex === index) continue;\n const cached = this.cache.get(index);\n if (!cached) continue;\n // Reparenting a live <iframe> resets it, breaking a provider video's\n // player API — never moved via the cache; DESIGN.md §2.3 real-bug entry.\n if (cached.node.classList.contains('shoji-slide-provider-video')) continue;\n slot.assignedIndex = index;\n this.moveIn(slot, cached, index);\n onLoad(index);\n }\n\n // Phase 2 — whatever's left: clear out-of-range slots, start/await a\n // decode behind a spinner for the rest. Anything still here is\n // confirmed stale — phase 1 already reclaimed what's reusable.\n for (const { slot, index, item } of targets) {\n if (!item) {\n slot.assignedIndex = null;\n slot.ready = false;\n releaseVideo(slot.media);\n slot.media.replaceChildren();\n continue;\n }\n if (slot.assignedIndex === index) continue; // unchanged, or already resolved by phase 1\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\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 * Moves an already-cached, ready node into `slot` — skips `releaseVideo`,\n * unlike `swapIn`: what `slot` holds might be a live cache entry another\n * slot reclaims this same `render()` pass (Phase 1). Reparenting is\n * always safe; only the later fresh/clear pass releases stale content.\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/** 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 } from './GestureController';\nimport { LiveRegion } from './LiveRegion';\nimport type { ButtonSpec, PluginContext, VideoProviderRenderer } from './plugin';\nimport { DEFAULT_SELECTOR, scanContainer } from './scan';\nimport { SlideManager } from './SlideManager';\nimport type { DangerousHtmlCaption, GalleryEvents, GalleryItem, GalleryOptions } from './types';\nimport { TRANSITION_PRESETS } from '../transitions/presets';\nimport { SlideTransition } from '../transitions/SlideTransition';\nimport { zoomIn, zoomOut } from './zoomTransition';\n\nfunction itemKey(item: GalleryItem): 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 */\nfunction isBackdropClick(event: Event): boolean {\n return !event\n .composedPath()\n .some(\n (node) =>\n node instanceof Element &&\n node.matches('.shoji-slide-img, button, video, .shoji-counter, .shoji-caption'),\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 = 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 this.bus.emit('beforeSlide', { from, to: clamped });\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 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: GalleryItem[], 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 = 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: GalleryItem[], atIndex?: number): void {\n if (this.destroyed) return;\n const next = [...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\n function apply(index: number): void {\n const el = gallery.getOriginElement(index);\n if (current && current !== el) current.classList.remove(activeClass);\n if (el) {\n el.classList.add(activeClass);\n if (scrollIntoView) {\n el.scrollIntoView({\n block: 'nearest',\n inline: 'nearest',\n behavior: prefersReducedMotion() ? 'auto' : 'smooth',\n });\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.\n function clear(): void {\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}\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 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 // .shoji-slide-media (offset 0) is a stable node for the gallery's whole\n // lifetime (SlideManager's pool, DESIGN.md §2.3) — registered once here,\n // not per enterSlide(), rather than tracked/detached alongside\n // currentVideo. A provider's own error event (§4-video, e.g. YouTube's\n // onError) bubbles up to it regardless of which slide is currently\n // showing there, or whether findPlayable() would even consider it\n // \"ready\" yet — a video that errors out before ever becoming playable\n // would otherwise just sit through the full `interval` fallback timer\n // instead of skipping ahead immediately.\n const media = gallery.getActiveMedia();\n function onVideoError(): void {\n if (playing) advance();\n }\n media?.addEventListener('error', onVideoError);\n\n const button = document.createElement('button');\n button.type = 'button';\n button.className = 'shoji-toolbar-button';\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 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 function ensureProviderPlaying(video: PlayableMedia, attemptsLeft: number): void {\n video.muted = true;\n video.play();\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 else stop(); // exhausted retries — don't leave the slideshow silently stuck\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 // Browsers can block an unmuted native <video> play() that isn't a\n // direct continuation of a user gesture (e.g. one arriving via this\n // setTimeout/'ended' chain rather than the toggle button's click) —\n // pause the slideshow and wait for the viewer rather than getting\n // stuck with a video that silently never plays or advances.\n if (playResult && typeof playResult.catch === 'function') {\n playResult.catch(() => stop());\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 // 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\n return () => {\n stop();\n media?.removeEventListener('error', onVideoError);\n removeButton();\n removeProgress?.();\n removeShortcut();\n offSlide();\n offSlideItemLoad();\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 /** [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 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 if (!item || (item.width && item.height) || !measurableSrc(item)) continue;\n\n const onLoad = (): void => {\n if (destroyed) return;\n if (tile.img.naturalWidth > 0 && tile.img.naturalHeight > 0) {\n item.width = tile.img.naturalWidth;\n item.height = tile.img.naturalHeight;\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 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\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 { renderYouTube } from './youtube';\n\n/**\n * DESIGN.md §4-video — renders `{ video: { provider: 'youtube', id } }`\n * items (produced by `scan.ts`'s `data-shoji-video` host detection, or\n * authored directly in dynamic mode) as a real YouTube embed instead of\n * `SlideManager`'s native-`<video>` fallback, which can't play a YouTube\n * URL at 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\n * `wirePlayableContract`.\n */\nexport const Video: ShojiPlugin = {\n name: 'video',\n\n init(ctx: PluginContext): () => void {\n return ctx.ui.registerVideoProvider('youtube', renderYouTube);\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","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","applyCommon","src","data","key","img","thumb","alt","srcset","sizes","videoEl","sources","s","poster","videoUrl","youtubeId","detectYouTubeId","guessVideoType","url","parsed","match","clean","ext","PAUSE_OVERLAY_DELAY_MS","SlideManager","count","_","i","offset","root","media","slot","px","transition","items","centerIndex","onLoad","openPlaceholderSrc","index","entry","releaseVideoNode","targets","cached","releaseVideo","createSpinner","extra","reveal","createOpenPlaceholder","placeholder","video","source","overlay","pauseTimer","clearPauseTimer","renderFn","controller","providerAbortControllers","revealed","spinner","provider","releaseProviderNode","resolveAxis","config","sign","parts","tx","ty","preset","spec","PRESET_LIST","TRANSITION_PRESETS","p","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","apply","clear","offOpen","offSlide","to","offClose","PAUSE_ICON","findPlayable","isPendingProviderVideo","PROVIDER_PLAY_RETRY_MS","MAX_PROVIDER_PLAY_ATTEMPTS","Autoplay","interval","showProgress","locale","playLabel","pauseLabel","playing","timer","currentVideo","awaitingProviderVideo","onVideoError","advance","progressBar","clearTimer","resetProgressBar","runProgressBar","ms","setButtonState","isPlaying","onVideoEnded","onVideoPause","stop","detachVideo","ensureProviderPlaying","attemptsLeft","enterSlide","playResult","before","start","toggle","removeButton","removeProgress","removeShortcut","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","computeSections","sections","createHeadingRoot","sectionItems","rendered","structured","titleEl","subtitleEl","headingSegments","heading","measurableSrc","aspectOf","selfCorrecting","createTile","applyGrid","applyMasonry","appendOnly","masonryOptions","segments","headingHeights","segTiles","segResult","t","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","loadYouTubeApi","resolve","previous","script","wirePlayableContract","player","playable","handleStateChange","YT","renderYouTube","onReady","signal","videoId","mount","Video","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","offBeforeSlide","offBeforeClose","unregisterGate"],"mappings":"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"}
|