@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.
Files changed (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +81 -0
  3. package/dist/esm/core/EventBus.d.ts +16 -0
  4. package/dist/esm/core/FocusTrap.d.ts +9 -0
  5. package/dist/esm/core/Gallery.d.ts +221 -0
  6. package/dist/esm/core/GestureController.d.ts +50 -0
  7. package/dist/esm/core/LiveRegion.d.ts +6 -0
  8. package/dist/esm/core/SlideManager.d.ts +83 -0
  9. package/dist/esm/core/bodyScrollLock.d.ts +2 -0
  10. package/dist/esm/core/dom.d.ts +27 -0
  11. package/dist/esm/core/icons.d.ts +6 -0
  12. package/dist/esm/core/index.d.ts +5 -0
  13. package/dist/esm/core/index.js +1966 -0
  14. package/dist/esm/core/index.js.map +1 -0
  15. package/dist/esm/core/plugin.d.ts +54 -0
  16. package/dist/esm/core/rotateFlipNormalize.d.ts +19 -0
  17. package/dist/esm/core/scan.d.ts +8 -0
  18. package/dist/esm/core/types.d.ts +268 -0
  19. package/dist/esm/core/zoomTransition.d.ts +33 -0
  20. package/dist/esm/gestures/GestureEngine.d.ts +78 -0
  21. package/dist/esm/index.css +475 -0
  22. package/dist/esm/index.d.ts +32 -0
  23. package/dist/esm/index.js +24 -0
  24. package/dist/esm/index.js.map +1 -0
  25. package/dist/esm/index2.css +22 -0
  26. package/dist/esm/index3.css +166 -0
  27. package/dist/esm/index4.css +27 -0
  28. package/dist/esm/plugins/activeThumbnail/index.d.ts +26 -0
  29. package/dist/esm/plugins/activeThumbnail/index.js +48 -0
  30. package/dist/esm/plugins/activeThumbnail/index.js.map +1 -0
  31. package/dist/esm/plugins/autoplay/icons.d.ts +3 -0
  32. package/dist/esm/plugins/autoplay/index.d.ts +17 -0
  33. package/dist/esm/plugins/autoplay/index.js +177 -0
  34. package/dist/esm/plugins/autoplay/index.js.map +1 -0
  35. package/dist/esm/plugins/fullscreen/icons.d.ts +3 -0
  36. package/dist/esm/plugins/fullscreen/index.d.ts +17 -0
  37. package/dist/esm/plugins/fullscreen/index.js +74 -0
  38. package/dist/esm/plugins/fullscreen/index.js.map +1 -0
  39. package/dist/esm/plugins/layout/index.d.ts +191 -0
  40. package/dist/esm/plugins/layout/index.js +746 -0
  41. package/dist/esm/plugins/layout/index.js.map +1 -0
  42. package/dist/esm/plugins/layout/justified.d.ts +68 -0
  43. package/dist/esm/plugins/layout/masonry.d.ts +92 -0
  44. package/dist/esm/plugins/rotateFlip/icons.d.ts +5 -0
  45. package/dist/esm/plugins/rotateFlip/index.d.ts +17 -0
  46. package/dist/esm/plugins/rotateFlip/index.js +138 -0
  47. package/dist/esm/plugins/rotateFlip/index.js.map +1 -0
  48. package/dist/esm/plugins/video/index.d.ts +13 -0
  49. package/dist/esm/plugins/video/index.js +95 -0
  50. package/dist/esm/plugins/video/index.js.map +1 -0
  51. package/dist/esm/plugins/video/youtube.d.ts +61 -0
  52. package/dist/esm/plugins/zoom/icons.d.ts +4 -0
  53. package/dist/esm/plugins/zoom/index.d.ts +30 -0
  54. package/dist/esm/plugins/zoom/index.js +274 -0
  55. package/dist/esm/plugins/zoom/index.js.map +1 -0
  56. package/dist/esm/plugins/zoom/zoomMath.d.ts +24 -0
  57. package/dist/esm/transitions/SlideTransition.d.ts +25 -0
  58. package/dist/esm/transitions/presets.d.ts +21 -0
  59. package/dist/esm/zoomTransition-bbKHpVpA.js +110 -0
  60. package/dist/esm/zoomTransition-bbKHpVpA.js.map +1 -0
  61. package/dist/shoji.css +690 -0
  62. package/dist/shoji.js +3605 -0
  63. package/dist/shoji.js.map +1 -0
  64. package/dist/shoji.min.css +1 -0
  65. package/dist/shoji.min.js +2 -0
  66. package/dist/shoji.min.js.map +1 -0
  67. package/package.json +77 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.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/GestureController.ts","../../../src/core/LiveRegion.ts","../../../src/core/scan.ts","../../../src/core/SlideManager.ts","../../../src/transitions/presets.ts","../../../src/transitions/SlideTransition.ts","../../../src/core/Gallery.ts"],"sourcesContent":["/** DESIGN.md §2.6a — reference-counted page scroll lock. */\nlet lockCount = 0;\nlet 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","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"],"names":["preset"],"mappings":"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"}
@@ -0,0 +1,54 @@
1
+ import { Gallery } from './Gallery';
2
+ import { GalleryEvents, GalleryItem } from './types';
3
+ import { Unsubscribe } from './EventBus';
4
+ /**
5
+ * DESIGN.md §3 — the plugin contract. Plugins init at `new Shoji()`
6
+ * construction time — the lightbox DOM (hidden, `display:none` until the
7
+ * first `open()`) is built eagerly specifically so `init()` can rely on
8
+ * `ctx.ui.*` synchronously, with zero queueing, from the very start. This
9
+ * also means a plugin can act *before* any `open()` call — the layout
10
+ * plugin (§5) depends on that: it renders `ctx.gallery.element` (the inline
11
+ * thumbnail container) immediately, since that's what the viewer clicks to
12
+ * open in the first place, not something that only matters once already open.
13
+ */
14
+ export interface ShojiPlugin {
15
+ name: string;
16
+ version?: string;
17
+ requires?: string[];
18
+ defaults?: Record<string, unknown>;
19
+ init(ctx: PluginContext): void | (() => void);
20
+ }
21
+ export interface ButtonSpec {
22
+ label: string;
23
+ icon?: string;
24
+ onClick: (event: MouseEvent) => void;
25
+ }
26
+ export type KeySpec = string;
27
+ /**
28
+ * DESIGN.md §4-video — supplies the embed for a non-`'html5'` provider.
29
+ * Build into `container`, call `onReady()` once it should show (sync or
30
+ * async — a spinner shows until then). `signal` fires `'abort'` once this
31
+ * slide is no longer wanted; a returned cleanup can't work here since setup
32
+ * is often async and may resolve after eviction — release everything on
33
+ * abort instead, checking `signal.aborted` before acting on a stale slide.
34
+ */
35
+ export type VideoProviderRenderer = (container: HTMLElement, item: GalleryItem, onReady: () => void, signal: AbortSignal) => void;
36
+ export interface PluginContext {
37
+ gallery: Gallery;
38
+ options: Readonly<Record<string, unknown>>;
39
+ on: Gallery['on'];
40
+ /** Emits through the same typed bus `gallery.on()` reads from — this is how plugin-contributed events (e.g. `autoplayStart`/`autoplayStop`) reach host listeners. */
41
+ emit<K extends keyof GalleryEvents>(event: K, detail: GalleryEvents[K]): void;
42
+ ui: {
43
+ toolbar(slot: 'left' | 'center' | 'right', el: HTMLElement | ButtonSpec): Unsubscribe;
44
+ overlay(el: HTMLElement, layer?: number): Unsubscribe;
45
+ outer(): HTMLElement;
46
+ registerShortcut(key: KeySpec, fn: (e: KeyboardEvent) => void): Unsubscribe;
47
+ /** `name` matches `VideoDescriptor['provider']` (e.g. `'youtube'`). Last registration wins; unregistering a stale one is a no-op if something else has since replaced it. */
48
+ registerVideoProvider(name: string, render: VideoProviderRenderer): Unsubscribe;
49
+ };
50
+ storage: {
51
+ get(key: string): unknown;
52
+ set(key: string, value: unknown): void;
53
+ };
54
+ }
@@ -0,0 +1,19 @@
1
+ export interface RotateFlipState {
2
+ flipH: boolean;
3
+ flipV: boolean;
4
+ /** Degrees; normalized to one of 0/90/180/270 by `normalizeRotateFlip`. */
5
+ rotation: number;
6
+ }
7
+ /**
8
+ * DESIGN.md §8.1's flip/rotation canonicalization table, verbatim — CLAUDE.md:
9
+ * "Flip + rotate compose non-commutatively. Use the normalization table...
10
+ * don't re-derive it." `flipH && flipV` is always visually equivalent to a
11
+ * 180°-rotated state with neither flip set; this collapses any
12
+ * `flipH`/`flipV`/`rotation` combination down to that canonical form, so two
13
+ * states reached via different sequences of clicks compare equal and never
14
+ * accumulate redundant flip+flip-again or unbounded rotation values.
15
+ * Shared between the standalone rotate/flip *view* plugin (§4) and the
16
+ * future Editor plugin (§8), which both need the identical composition —
17
+ * not duplicated per-plugin.
18
+ */
19
+ export declare function normalizeRotateFlip(state: RotateFlipState): RotateFlipState;
@@ -0,0 +1,8 @@
1
+ import { GalleryItem } from './types';
2
+ /** DESIGN.md §2.1 zero-config quickstart. */
3
+ export declare const DEFAULT_SELECTOR = ":scope > a, :scope > [data-shoji-src], :scope > [data-shoji-video]";
4
+ export interface ScannedItem {
5
+ element: HTMLElement;
6
+ item: GalleryItem;
7
+ }
8
+ export declare function scanContainer(container: HTMLElement, selector?: string): ScannedItem[];