@zinejs/core 0.9.0 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +73 -0
- package/dist/controls/icons.d.ts +4 -0
- package/dist/controls/sidebar.d.ts +2 -0
- package/dist/controls/types.d.ts +3 -0
- package/dist/{controls-CcOu_19g.js → controls-Dh1okIx6.js} +60 -44
- package/dist/controls-Dh1okIx6.js.map +1 -0
- package/dist/{cssRenderer-Deskm2ND.js → cssRenderer-CTaM4cAj.js} +35 -29
- package/dist/cssRenderer-CTaM4cAj.js.map +1 -0
- package/dist/flipSound-BFXSkWBo.js +78 -0
- package/dist/flipSound-BFXSkWBo.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +709 -569
- package/dist/index.js.map +1 -1
- package/dist/index.umd.js +4 -4
- package/dist/index.umd.js.map +1 -1
- package/dist/loading/loading.d.ts +2 -1
- package/dist/{loading-DF88K9yJ.js → loading-BgbqJz5k.js} +7 -8
- package/dist/loading-BgbqJz5k.js.map +1 -0
- package/dist/renderer/types.d.ts +7 -0
- package/dist/{share-BrQa6aYm.js → share-CZorhnKI.js} +25 -22
- package/dist/share-CZorhnKI.js.map +1 -0
- package/dist/sound/clip.d.ts +9 -0
- package/dist/sound/flipSound.d.ts +30 -0
- package/dist/strings.d.ts +74 -0
- package/dist/{styles-D_zOGk2q.js → styles-C1Qp5pby.js} +4 -2
- package/dist/styles-C1Qp5pby.js.map +1 -0
- package/dist/{webglRenderer-CmQ_mUrX.js → webglRenderer-DQn4pc1S.js} +85 -79
- package/dist/webglRenderer-DQn4pc1S.js.map +1 -0
- package/dist/zine.d.ts +35 -0
- package/options.schema.json +27 -0
- package/package.json +12 -10
- package/dist/controls-CcOu_19g.js.map +0 -1
- package/dist/cssRenderer-Deskm2ND.js.map +0 -1
- package/dist/loading-DF88K9yJ.js.map +0 -1
- package/dist/share-BrQa6aYm.js.map +0 -1
- package/dist/styles-D_zOGk2q.js.map +0 -1
- package/dist/webglRenderer-CmQ_mUrX.js.map +0 -1
package/dist/index.umd.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.umd.js","names":["#radius","#mounted","#listeners","#state","#handlers","#activeId","#start","#prev","#last","#end","#reset","#pinching","#points","#pair","#startDistance","#remove","#container","#clip","#viewport","#book","#pageLeft","#makeCanvas","#pageRight","#leaf","#leafFront","#leafBack","#leafShadow","#paint","#fill","#contentShift","#trackAspect","#layoutBook","#applyShift","#fromShiftUnit","#toShiftUnit","#bookW","#pageAspect","#bookBox","#contentBox","#container","#canvas","#onContextLost","#onContextRestored","#onVisibilityChange","#gl","#maxTextureSize","#buildGlResources","#clearRestoreTimer","#texCache","#blankTex","#quadVao","#curlVao","#flat","#curl","#cacheBytes","#content","#flip","#fatalHandler","#fill","#trackAspect","#render","#pageAspect","#flipT","#curlModel","#anchor","#fromShiftUnit","#toShiftUnit","#view","#bookBox","#contentBox","#shiftX","#dpr","#contextLost","#restoreTimer","#fatal","#fatalFired","#flatU","#curlU","#makeQuadVao","#buildCurlMesh","#makeBlankTexture","#idxCount","#posBuf","#normBuf","#mesh","#resize","#fitBook","#drawFlip","#drawStatic","#bookW","#bookH","#useFlat","#drawQuad","#bindFace","#fillFade","#textureFor","#evictTextures","#base","#front","#back","#rawPages","#resolvePages","#resolved","#image","#cache","#doc","#lastFocus","#backdrop","#dialog","#linkRow","#socialRow","#qr","#onKey","#trapFocus","#copy","#timer","#doc","#context","#onAfterAction","#hidden","#items","#doc","#holder","#pageBox","#place","#armDismiss","#trackPage","#fitToPage","#resizeObserver","#wrap","#close","#scrim","#onKeydown","#zine","#doc","#bar","#build","#markCurrent","#unsubscribe","#rows","#observeRows","#paint","#observer","#zine","#doc","#onDismiss","#bar","#note","#build","#unsubscribe","#markCurrent","#addLevel","#rows","#zine","#doc","#onDismiss","#bar","#input","#timer","#search","#hits","#run","#renderResults","#results","#note","#zine","#device","#prev","#button","#next","#place","#wrap","#unsubscribe","#refresh","#edges","#zine","#container","#hidden","#doc","#colorScheme","#position","#root","#bar","#onKeyDown","#isolate","#place","#refresh","#unsubscribe","#popover","#closePopover","#wrap","#build","#arrows","#probeOutline","#panel","#panelKind","#context","#activate","#buttons","#openMenu","#placePopover","#pageInput","#hasOutline","#searchState","#zine","#container","#hidden","#doc","#colorScheme","#items","#open","#unbind","#menu","#close","#context","#place","#renderer","#container","#source","#direction","#deepLinkEnabled","#disableContextMenu","#spreadMode","#configuredMode","#curl","#clickToFlip","#clickZoneSize","#cursorHints","#hintsEnabled","#hintsPersist","#loadLearned","#singlePageThreshold","#responsiveSpread","#controlsOption","#contextMenuOption","#hiddenControls","#loadingOption","#startPageOption","#buildSpreadModel","#flipDuration","#zoomEnabled","#wheelZoom","#doubleClickLevels","#maxZoom","#honorDoubleClickInFlipZone","#clickFlipDelayValue","#ready","#init","#currentPage","#spreads","#current","#singlePage","#narrow","#rebuildSpreads","#renderCurrent","#applySinglePage","#getPage","#sameOriginFile","#outline","#isCrossOrigin","#emitter","#stepFlip","#requestFlip","#startFlip","#spreadIndexForPage","#machine","#activeAnim","#queuedFlip","#finishActiveAnim","#raf","#flushPendingUpgrades","#scale","#cancelPeek","#cancelNudge","#tx","#ty","#clampPan","#applyTouchAction","#refreshZoomTiles","#updateCursor","#markLearned","#showPanCaption","#destroyed","#clearPendingClickFlip","#clearIdleNudge","#clearZoomHint","#hideCaption","#resizeObserver","#deepLink","#unsubscribeDeepLink","#controlsCleanup","#contextMenuCleanup","#dismissLoader","#a11yCleanup","#unbindWheel","#unbindDblClick","#unbindContextMenu","#unbindCursor","#unbindInput","#tileTimer","#tileGeneration","#leadPage","#runFlip","#flipGeneration","#resolveContent","#currentContent","#effectiveCurl","#anchorY","#animateProgress","#commit","#announcePage","#effectiveDuration","#announce","#paintSpread","#prefetchWindow","#lastFlipAt","#drainQueuedFlip","#pinching","#pan","#contentRect","#toBookPoint","#press","#pendingGrab","#drag","#stageDrag","#promoteGrab","#swipeFlip","#maybeClickFlip","#cancelFlip","#canFlip","#playEdgeNudge","#clickFlipDirection","#pendingClickTimer","#pointerClient","#cursorFor","#learned","#reducedMotion","#idleTimer","#playPeek","#peekAnim","#repaintRestingSpread","#nudgeAnim","#showCaption","#captionEl","#captionTimer","#zoomHintShown","#lastPointerType","#zoomHintTimer","#zoomHintText","#pinchBaseScale","#lastClick","#maybeArmZoomHint","#zoomAt","#onLoadProgress","#mountLoader","#setLoaderPhase","#mountWithFallback","#onPageUpdate","#onDragStart","#onDragMove","#onDragEnd","#onPinchStart","#onPinchMove","#onPinchEnd","#bindWheelZoom","#bindDoubleClickZoom","#bindContextMenu","#bindCursorHints","#setupReducedMotion","#setupA11y","#observeResize","#bindDeepLink","#mountControls","#armIdleNudge","#lastProgress","#loader","#loaderPhase","#fallbackToCss","#fellBack","#onKeyDown","#liveRegion","#effectiveMode","#effectiveModeShown","#applyContainerAspect","#zoomedAt","#lastAspect","#pendingUpgrades","#upgradeScale","#renderZoomTiles","#fitFactor","#scheduleUpdate","#updateScheduled","#virtualizer","#urls","#preload","#decode","#cache"],"sources":["../src/engine/spread.ts","../src/engine/virtualizer.ts","../src/engine/emitter.ts","../src/engine/stateMachine.ts","../src/engine/input.ts","../src/engine/deeplink.ts","../src/geometry/curls/types.ts","../src/geometry/flipProgressToPose.ts","../src/renderer/cssRenderer.ts","../src/geometry/curls/mesh.ts","../src/geometry/curls/cone.ts","../src/geometry/curls/simple.ts","../src/geometry/curls/bundled.ts","../src/renderer/gl/context.ts","../src/renderer/gl/program.ts","../src/renderer/gl/texture.ts","../src/renderer/webglRenderer.ts","../src/renderer/select.ts","../src/source/compose.ts","../src/loading/loading.ts","../src/controls/icons.ts","../src/controls/registry.ts","../src/controls/socials.ts","../src/controls/qr.ts","../src/controls/styles.ts","../src/controls/share.ts","../src/controls/builtins.ts","../src/controls/menu.ts","../src/controls/sidebar.ts","../src/controls/thumbnails.ts","../src/controls/outline.ts","../src/controls/search.ts","../src/controls/arrows.ts","../src/controls/toolbar.ts","../src/controls/contextMenu.ts","../src/controls/controls.ts","../src/zine.ts","../src/source/imageSource.ts","../src/index.ts"],"sourcesContent":["export type Direction = 'ltr' | 'rtl';\n\n/**\n * How pages group into spreads:\n * - `double`: paired from the start — `[0|1] [2|3] …`\n * - `single`: one page per spread — `[0] [1] …`\n * - `cover`: first page alone, then paired — `[·|0] [1|2] …`\n * - `book`: first AND last alone (front + back cover) — `[·|0] [1|2] … [N-1|·]`\n */\nexport type SpreadMode = 'double' | 'single' | 'cover' | 'book';\n\nexport interface Spread {\n /** Page index shown on the left, or null for a blank side. */\n left: number | null;\n /** Page index shown on the right, or null for a blank side. */\n right: number | null;\n}\n\nexport interface SpreadOptions {\n direction?: Direction;\n mode?: SpreadMode;\n}\n\n/**\n * Map a flat page list (indices 0..pageCount-1) into display spreads.\n *\n * Reading order is always preserved; `direction` only mirrors each spread's\n * left/right positions, so RTL is just an LTR layout with the sides swapped.\n * Out-of-range slots are `null` (a blank half on odd counts / a lone cover).\n */\nexport function buildSpreads(pageCount: number, options: SpreadOptions = {}): Spread[] {\n const { direction = 'ltr', mode = 'cover' } = options;\n if (pageCount <= 0) return [];\n\n const page = (i: number): number | null => (i >= 0 && i < pageCount ? i : null);\n const spreads: Spread[] = [];\n\n if (mode === 'single') {\n for (let i = 0; i < pageCount; i++) spreads.push({ left: null, right: i });\n } else if (mode === 'double') {\n for (let i = 0; i < pageCount; i += 2) spreads.push({ left: page(i), right: page(i + 1) });\n } else if (mode === 'cover') {\n spreads.push({ left: null, right: 0 });\n for (let i = 1; i < pageCount; i += 2) spreads.push({ left: page(i), right: page(i + 1) });\n } else {\n // book: front cover alone, interior paired, back cover alone\n spreads.push({ left: null, right: 0 });\n if (pageCount > 1) {\n for (let i = 1; i <= pageCount - 2; i += 2) {\n spreads.push({ left: page(i), right: i + 1 <= pageCount - 2 ? page(i + 1) : null });\n }\n spreads.push({ left: pageCount - 1, right: null });\n }\n }\n\n return direction === 'rtl'\n ? spreads.map((s) => ({ left: s.right, right: s.left }))\n : spreads;\n}\n\n/** Whether a container this wide should fall back to one page per spread. */\nexport function shouldSinglePage(containerWidth: number, threshold: number): boolean {\n return containerWidth < threshold;\n}\n","/** Spread indices that should be mounted around `current`, clamped to the book. */\nexport function mountWindow(current: number, spreadCount: number, radius: number): number[] {\n const window: number[] = [];\n const from = Math.max(0, current - radius);\n const to = Math.min(spreadCount - 1, current + radius);\n for (let i = from; i <= to; i++) {\n window.push(i);\n }\n return window;\n}\n\nexport interface VirtualizerDelta {\n toMount: number[];\n toEvict: number[];\n}\n\n/**\n * Tracks which spreads are mounted and, on each move, reports the difference:\n * spreads entering the ±radius window (`toMount`) and those leaving it\n * (`toEvict`). Default radius 2 = current spread ±2.\n */\nexport class Virtualizer {\n #radius: number;\n #mounted = new Set<number>();\n\n constructor(radius = 2) {\n this.#radius = radius;\n }\n\n get mounted(): number[] {\n return [...this.#mounted].sort((a, b) => a - b);\n }\n\n /** Recenter on `current`; mounts/evicts to match the window and returns the delta. */\n update(current: number, spreadCount: number): VirtualizerDelta {\n const window = new Set(mountWindow(current, spreadCount, this.#radius));\n const toMount = [...window].filter((i) => !this.#mounted.has(i)).sort((a, b) => a - b);\n const toEvict = [...this.#mounted].filter((i) => !window.has(i)).sort((a, b) => a - b);\n for (const i of toEvict) this.#mounted.delete(i);\n for (const i of toMount) this.#mounted.add(i);\n return { toMount, toEvict };\n }\n}\n","import type { SpreadMode } from './spread.js';\nimport type { LoadProgress } from '../source/types.js';\n\nexport type Listener<T> = (payload: T) => void;\n\n/** Events typed as `void` are emitted with no payload argument. */\ntype PayloadArgs<T> = [T] extends [void] ? [] : [payload: T];\n\n/** Minimal typed event emitter keyed by an event map. */\nexport class Emitter<Events> {\n #listeners = new Map<keyof Events, Set<Listener<unknown>>>();\n\n /** Subscribe; returns an unsubscribe function. */\n on<K extends keyof Events>(type: K, listener: Listener<Events[K]>): () => void {\n let set = this.#listeners.get(type);\n if (!set) {\n set = new Set();\n this.#listeners.set(type, set);\n }\n set.add(listener as Listener<unknown>);\n return () => this.off(type, listener);\n }\n\n off<K extends keyof Events>(type: K, listener: Listener<Events[K]>): void {\n this.#listeners.get(type)?.delete(listener as Listener<unknown>);\n }\n\n emit<K extends keyof Events>(type: K, ...args: PayloadArgs<Events[K]>): void {\n const payload = args[0] as Events[K];\n // Copy so a listener that unsubscribes mid-emit doesn't disturb iteration.\n const set = this.#listeners.get(type);\n if (!set) return;\n for (const listener of [...set]) {\n (listener as Listener<Events[K]>)(payload);\n }\n }\n\n clear(): void {\n this.#listeners.clear();\n }\n}\n\n/**\n * Public event surface of a Zine instance. Payloads are provisional and will\n * firm up as the engine wires the emitters that fire them.\n */\nexport interface ZineEventMap {\n ready: void;\n flipStart: { from: number; to: number };\n flipEnd: { page: number };\n pageChanged: { page: number };\n /** Pages regrouped: the spread mode changed, or a narrow container forced one page. */\n spreadChanged: { mode: SpreadMode; singlePage: boolean };\n zoomChanged: { scale: number };\n /** The document is downloading. Fires only for sources that report byte progress (a PDF from a\n * URL); use it to drive a custom loading indicator, and hide it on `ready`. */\n progress: LoadProgress;\n sourceError: { index: number; error: unknown };\n rendererFallback: { from: string; to: string };\n}\n\nexport type ZineEmitter = Emitter<ZineEventMap>;\n","export type FlipState = 'idle' | 'dragging' | 'animating' | 'zoomed-panning';\n\nexport type FlipEvent = 'grab' | 'release' | 'settle' | 'panStart' | 'panEnd' | 'flip';\n\n/**\n * Legal transitions. A flip drag runs idle → dragging → animating → idle; a\n * programmatic flip jumps idle → animating directly via `flip`; zoom panning\n * (only reachable when zoomed in) is a separate branch idle → zoomed-panning →\n * idle. Any (state, event) pair absent here is illegal.\n */\nconst TRANSITIONS: Record<FlipState, Partial<Record<FlipEvent, FlipState>>> = {\n idle: { grab: 'dragging', panStart: 'zoomed-panning', flip: 'animating' },\n dragging: { release: 'animating' },\n animating: { settle: 'idle' },\n 'zoomed-panning': { panEnd: 'idle' },\n};\n\n/** Pure transition: the next state, or null if `event` is illegal from `state`. */\nexport function nextState(state: FlipState, event: FlipEvent): FlipState | null {\n return TRANSITIONS[state][event] ?? null;\n}\n\n/** Stateful holder around `nextState`; illegal events are ignored. */\nexport class FlipMachine {\n #state: FlipState;\n\n constructor(initial: FlipState = 'idle') {\n this.#state = initial;\n }\n\n get state(): FlipState {\n return this.#state;\n }\n\n /** Apply `event`; returns the new state, or null (no-op) if the event was illegal. */\n send(event: FlipEvent): FlipState | null {\n const next = nextState(this.#state, event);\n if (next !== null) {\n this.#state = next;\n }\n return next;\n }\n}\n","export interface GestureStart {\n x: number;\n y: number;\n t: number;\n}\n\nexport interface GestureMove {\n x: number;\n y: number;\n /** Displacement from the start point. */\n dx: number;\n dy: number;\n}\n\nexport interface GestureEnd {\n x: number;\n y: number;\n dx: number;\n dy: number;\n /** Elapsed time from start to end (ms). */\n dt: number;\n /** Velocity of the final move segment (px/ms). */\n vx: number;\n vy: number;\n /** A fast flick: final speed exceeded the swipe threshold and the gesture was not canceled. */\n swipe: boolean;\n /** The pointer was canceled (e.g. pointercancel) rather than lifted. */\n canceled: boolean;\n}\n\nexport interface PointerHandlers {\n onStart?: (g: GestureStart) => void;\n onMove?: (g: GestureMove) => void;\n onEnd?: (g: GestureEnd) => void;\n}\n\n/** Flick threshold in px/ms; a release faster than this is a swipe. */\nconst SWIPE_VELOCITY = 0.3;\n\ninterface Sample {\n x: number;\n y: number;\n t: number;\n}\n\n/**\n * Recognizes a single-pointer drag from raw pointer samples and reports normalized\n * gestures. DOM-agnostic on purpose: `bindPointerInput` maps real PointerEvents onto\n * it, but the kinematics stay pure and unit-testable. Extra pointers are ignored\n * (multi-touch pinch is a separate recognizer).\n */\nexport class PointerRecognizer {\n #handlers: PointerHandlers;\n #activeId: number | null = null;\n #start: Sample | null = null;\n #prev: Sample | null = null;\n #last: Sample | null = null;\n\n constructor(handlers: PointerHandlers = {}) {\n this.#handlers = handlers;\n }\n\n get active(): boolean {\n return this.#activeId !== null;\n }\n\n down(id: number, x: number, y: number, t: number): void {\n if (this.#activeId !== null) return; // single-pointer; ignore extra pointers\n this.#activeId = id;\n const s: Sample = { x, y, t };\n this.#start = s;\n this.#prev = s;\n this.#last = s;\n this.#handlers.onStart?.({ x, y, t });\n }\n\n move(id: number, x: number, y: number, t: number): void {\n if (id !== this.#activeId || this.#start === null) return;\n this.#prev = this.#last;\n this.#last = { x, y, t };\n this.#handlers.onMove?.({ x, y, dx: x - this.#start.x, dy: y - this.#start.y });\n }\n\n up(id: number, x: number, y: number, t: number): void {\n this.#end(id, x, y, t, false);\n }\n\n cancel(id: number, x: number, y: number, t: number): void {\n this.#end(id, x, y, t, true);\n }\n\n #end(id: number, x: number, y: number, t: number, canceled: boolean): void {\n if (id !== this.#activeId || this.#start === null) return;\n const start = this.#start;\n // Velocity comes from the last move segment, not the release point — on lift the\n // pointer usually stops, so release-relative velocity would kill every flick.\n const last = this.#last ?? start;\n const prev = this.#prev ?? start;\n const segDt = last.t - prev.t;\n const vx = segDt > 0 ? (last.x - prev.x) / segDt : 0;\n const vy = segDt > 0 ? (last.y - prev.y) / segDt : 0;\n const speed = Math.hypot(vx, vy);\n this.#reset();\n this.#handlers.onEnd?.({\n x,\n y,\n dx: x - start.x,\n dy: y - start.y,\n dt: t - start.t,\n vx,\n vy,\n swipe: !canceled && speed > SWIPE_VELOCITY,\n canceled,\n });\n }\n\n #reset(): void {\n this.#activeId = null;\n this.#start = null;\n this.#prev = null;\n this.#last = null;\n }\n}\n\n/**\n * Wire real PointerEvents from `target` into a recognizer. Returns an unbind\n * function that removes every listener. Coordinates are passed through as\n * client-space; mapping to page-local space is the engine's job.\n */\nexport function bindPointerInput(target: EventTarget, recognizer: PointerRecognizer): () => void {\n const down = (e: Event): void => {\n const p = e as PointerEvent;\n recognizer.down(p.pointerId, p.clientX, p.clientY, p.timeStamp);\n };\n const move = (e: Event): void => {\n const p = e as PointerEvent;\n recognizer.move(p.pointerId, p.clientX, p.clientY, p.timeStamp);\n };\n const up = (e: Event): void => {\n const p = e as PointerEvent;\n recognizer.up(p.pointerId, p.clientX, p.clientY, p.timeStamp);\n };\n const cancel = (e: Event): void => {\n const p = e as PointerEvent;\n recognizer.cancel(p.pointerId, p.clientX, p.clientY, p.timeStamp);\n };\n target.addEventListener('pointerdown', down);\n target.addEventListener('pointermove', move);\n target.addEventListener('pointerup', up);\n target.addEventListener('pointercancel', cancel);\n return () => {\n target.removeEventListener('pointerdown', down);\n target.removeEventListener('pointermove', move);\n target.removeEventListener('pointerup', up);\n target.removeEventListener('pointercancel', cancel);\n };\n}\n\n/**\n * Wire PointerEvents from `target` into a drag recognizer and/or a pinch\n * recognizer at once, so a single pointer stream feeds both. Returns an unbind.\n */\nexport function bindGestures(\n target: EventTarget,\n recognizers: { pointer?: PointerRecognizer; pinch?: PinchRecognizer },\n): () => void {\n const { pointer, pinch } = recognizers;\n const down = (e: Event): void => {\n const p = e as PointerEvent;\n pointer?.down(p.pointerId, p.clientX, p.clientY, p.timeStamp);\n pinch?.down(p.pointerId, p.clientX, p.clientY);\n };\n const move = (e: Event): void => {\n const p = e as PointerEvent;\n pointer?.move(p.pointerId, p.clientX, p.clientY, p.timeStamp);\n pinch?.move(p.pointerId, p.clientX, p.clientY);\n };\n const up = (e: Event): void => {\n const p = e as PointerEvent;\n pointer?.up(p.pointerId, p.clientX, p.clientY, p.timeStamp);\n pinch?.up(p.pointerId);\n };\n const cancel = (e: Event): void => {\n const p = e as PointerEvent;\n pointer?.cancel(p.pointerId, p.clientX, p.clientY, p.timeStamp);\n pinch?.cancel(p.pointerId);\n };\n target.addEventListener('pointerdown', down);\n target.addEventListener('pointermove', move);\n target.addEventListener('pointerup', up);\n target.addEventListener('pointercancel', cancel);\n return () => {\n target.removeEventListener('pointerdown', down);\n target.removeEventListener('pointermove', move);\n target.removeEventListener('pointerup', up);\n target.removeEventListener('pointercancel', cancel);\n };\n}\n\nexport interface PinchStart {\n centerX: number;\n centerY: number;\n /** Distance between the two pointers at pinch start. */\n distance: number;\n}\n\nexport interface PinchMove {\n centerX: number;\n centerY: number;\n /** Current distance / start distance. */\n scale: number;\n}\n\nexport interface PinchHandlers {\n onPinchStart?: (g: PinchStart) => void;\n onPinchMove?: (g: PinchMove) => void;\n onPinchEnd?: () => void;\n}\n\ninterface Pt {\n x: number;\n y: number;\n}\n\nfunction distance(a: Pt, b: Pt): number {\n return Math.hypot(a.x - b.x, a.y - b.y);\n}\n\n/**\n * Recognizes a two-pointer pinch and reports zoom scale (relative to the start\n * distance) and the live center point. No timestamps: pinch drives zoom, not a\n * flick. A third pointer is ignored; lifting either tracked pointer ends the pinch.\n * Coordinating this with the drag recognizer (a second finger should suspend a\n * single-finger drag) is the engine's job.\n */\nexport class PinchRecognizer {\n #handlers: PinchHandlers;\n #points = new Map<number, Pt>();\n #startDistance = 0;\n #pinching = false;\n\n constructor(handlers: PinchHandlers = {}) {\n this.#handlers = handlers;\n }\n\n get pinching(): boolean {\n return this.#pinching;\n }\n\n down(id: number, x: number, y: number): void {\n if (this.#points.has(id) || this.#points.size >= 2) return; // track only two\n this.#points.set(id, { x, y });\n if (this.#points.size === 2) {\n const { a, b } = this.#pair();\n this.#startDistance = distance(a, b);\n this.#pinching = true;\n this.#handlers.onPinchStart?.({\n centerX: (a.x + b.x) / 2,\n centerY: (a.y + b.y) / 2,\n distance: this.#startDistance,\n });\n }\n }\n\n move(id: number, x: number, y: number): void {\n const p = this.#points.get(id);\n if (!p) return;\n p.x = x;\n p.y = y;\n if (!this.#pinching) return;\n const { a, b } = this.#pair();\n this.#handlers.onPinchMove?.({\n centerX: (a.x + b.x) / 2,\n centerY: (a.y + b.y) / 2,\n scale: this.#startDistance > 0 ? distance(a, b) / this.#startDistance : 1,\n });\n }\n\n up(id: number): void {\n this.#remove(id);\n }\n\n cancel(id: number): void {\n this.#remove(id);\n }\n\n #remove(id: number): void {\n if (!this.#points.delete(id)) return;\n if (this.#pinching && this.#points.size < 2) {\n this.#pinching = false;\n this.#startDistance = 0;\n this.#handlers.onPinchEnd?.();\n }\n }\n\n #pair(): { a: Pt; b: Pt } {\n const values = [...this.#points.values()];\n const a = values[0];\n const b = values[1];\n if (!a || !b) throw new Error('pinch requires two active pointers');\n return { a, b };\n }\n}\n","/**\n * Keeping the page in the URL, so a link opens where the reader was.\n *\n * The hash is treated as shared ground rather than ours: only a `page` key is read or written,\n * anything else in it is preserved, and updates go through `replaceState` so turning pages does\n * not fill the reader's back button with history entries.\n */\nconst KEY = 'page';\n\n/**\n * Whether some book on this page already owns the hash.\n *\n * Two books sharing one URL would each overwrite the other's page, and the second to load would\n * open on the first one's page. Only the first claims it; the rest still work, they just do not\n * appear in the address bar.\n */\nlet claimed = false;\n\n/** Take the hash if it is going, and report whether this caller got it. */\nexport function claimHash(): boolean {\n if (claimed) return false;\n claimed = true;\n return true;\n}\n\n/** Give the hash back, so a book that replaces this one can own it. */\nexport function releaseHash(): void {\n claimed = false;\n}\n\n/** Read the 1-based page from a hash, as a 0-based index. Null when it carries no page. */\nexport function pageFromHash(hash: string): number | null {\n const params = new URLSearchParams(hash.replace(/^#/, ''));\n const raw = params.get(KEY);\n if (raw === null) return null;\n const page = Number(raw);\n return Number.isInteger(page) && page >= 1 ? page - 1 : null;\n}\n\n/** The hash that names `page` (0-based), leaving any other keys in `hash` untouched. */\nexport function hashWithPage(hash: string, page: number): string {\n const params = new URLSearchParams(hash.replace(/^#/, ''));\n params.set(KEY, String(page + 1));\n return `#${params.toString()}`;\n}\n\nexport interface DeepLinkHandle {\n /** Write the current page into the address bar. */\n push(page: number): void;\n stop(): void;\n}\n\n/**\n * Mirror the reader's page in the URL, and follow it when the URL changes (back/forward, or a\n * pasted link). `onNavigate` fires only for a page the book is not already on.\n */\nexport function bindDeepLink(win: Window, onNavigate: (page: number) => void): DeepLinkHandle {\n // The page the URL is known to describe. Compared against rather than the hash string itself:\n // some environments emit `hashchange` for our own replaceState, and a string comparison there\n // would let the echo through and turn the page a second time.\n let shown: number | null = null;\n const onHashChange = (): void => {\n const page = pageFromHash(win.location.hash);\n if (page === null || page === shown) return; // absent, or already where we are\n shown = page;\n onNavigate(page);\n };\n win.addEventListener('hashchange', onHashChange);\n return {\n push(page) {\n if (page === shown) return;\n shown = page;\n const hash = hashWithPage(win.location.hash, page);\n if (hash === win.location.hash) return;\n // replaceState, not assignment: a reader flipping through fifty pages should not have to\n // press Back fifty times to leave.\n win.history?.replaceState?.(win.history.state, '', hash);\n },\n stop() {\n win.removeEventListener('hashchange', onHashChange);\n },\n };\n}\n","/**\n * Curl type identifiers and the deform contract. Kept free of the deform\n * implementations so the engine (and CSS-only builds) can reference the union and the\n * validation list without pulling the WebGL-only curl math into the eager bundle.\n */\nimport type { PageMesh } from './mesh';\n\n/** Where a corner-anchored curl folds from: y in [0,1], 0 = top edge, 1 = bottom edge. */\nexport interface CurlAnchor {\n y: number;\n /**\n * Full-width lone page (single-page mode). The leaf hinges at the container edge and\n * has no facing half to land on — deforms may soften and delay the flop so the curl\n * reads naturally before the renderer dissolves the sheet.\n */\n fill?: boolean;\n}\n\nexport type CurlDeform = (mesh: PageMesh, W: number, H: number, t: number, anchor: CurlAnchor) => void;\n\nexport interface CurlModel {\n readonly deform: CurlDeform;\n /** True when the fold originates at a corner (`anchor.y`) rather than the whole edge. */\n readonly anchored: boolean;\n /**\n * True when the sheet is flat by mid-turn, so a lone page can start dissolving earlier: there\n * is no curl left to read. Defaults to false, which suits a model still bent as it lands.\n */\n readonly flat?: boolean;\n /**\n * False to drop the specular highlight, for a sheet that never bends and so has no curve to\n * catch the light. Defaults to true.\n */\n readonly gloss?: boolean;\n}\n\n/**\n * The curl models carried in the bundle: the default, and the flat turn reduced motion falls\n * back to. Everything else is imported from `@zinejs/core/curls` and passed as a model, so a\n * book that never uses it never pays for it.\n */\nexport type CurlType = 'cone' | 'simple';\n\nexport const CURL_TYPES: CurlType[] = ['cone', 'simple'];\n\n/**\n * Curl models that exist but are not bundled. Named only so passing one as a string can say what\n * to do about it instead of failing as an unknown value.\n */\nexport const IMPORTABLE_CURLS = ['roll', 'leaf', 'flick', 'silk'];\n\nexport const DEFAULT_CURL: CurlType = 'cone';\n\n/** What the `curl` option accepts: a bundled name, or a model imported (or written) by the caller. */\nexport type CurlSpec = CurlType | CurlModel;\n","export interface FlipPose {\n /** Turning-page rotation about the spine, radians: 0 (flat/open) → π (fully turned). */\n angle: number;\n /** Mesh cylinder-wrap curl, 0..1 — flat at the ends, peaks edge-on at t=0.5. CSS renderer ignores it. */\n curl: number;\n /** Fold-shadow intensity, 0..1 — peaks edge-on at t=0.5. */\n shadowAlpha: number;\n}\n\n/**\n * Map a 0→1 flip progress to a renderer-neutral fold pose.\n *\n * Linear in `t` on purpose — easing belongs to the animator that produces `t`,\n * so seeking to a fixed progress (e.g. the 0/.25/.5/.75/1 visual-regression stops)\n * is deterministic. The CSS renderer consumes `angle` as a spine `rotateY`; the\n * WebGL2 renderer additionally bends its mesh by `curl`.\n */\nexport function flipProgressToPose(t: number): FlipPose {\n const clamped = t < 0 ? 0 : t > 1 ? 1 : t;\n const arc = Math.sin(clamped * Math.PI);\n return {\n angle: clamped * Math.PI,\n curl: arc,\n shadowAlpha: arc,\n };\n}\n","import { flipProgressToPose } from '../geometry/flipProgressToPose';\nimport type { Spread } from '../engine/spread';\nimport type {\n FlipDirection,\n LayoutMetrics,\n PageContent,\n Renderer,\n RenderOptions,\n SpreadContent,\n} from './types';\n\nconst RAD_TO_DEG = 180 / Math.PI;\n\n/** How far to shift a spread to centre a lone page, in units of a quarter container width:\n * -1 = only a right page (cover), +1 = only a left page (back), 0 = a full spread. */\nfunction shiftUnit(content: SpreadContent): number {\n const lone = (content.left === null) !== (content.right === null);\n return lone ? (content.right !== null ? -1 : 1) : 0;\n}\n\n/**\n * Layered gradients painted over the turning leaf to fake paper depth on the flat\n * fold: a crease shadow at the spine, a soft curve sheen, and a free-edge shade.\n * The spine is the leaf's pivot edge — left for a forward flip, right for backward.\n */\nfunction foldOverlay(direction: FlipDirection): string {\n const dir = direction === 'forward' ? 'to right' : 'to left';\n return [\n `linear-gradient(${dir}, rgba(0,0,0,0.6), rgba(0,0,0,0.15) 40%, rgba(0,0,0,0) 60%)`,\n `linear-gradient(${dir}, transparent 40%, rgba(255,255,255,0.32) 66%, transparent 90%)`,\n `linear-gradient(${dir}, transparent 66%, rgba(0,0,0,0.35))`,\n ].join(',');\n}\n\n/**\n * Baseline renderer: a 2-panel CSS-3D fold. Runs on every browser, no GPU. Pages\n * are <canvas> elements; the turning leaf (front + back faces, backface-hidden)\n * rotates about the spine driven by flipProgressToPose(), with a gradient shadow\n * overlay for depth. `beginFlip` stages the surfaces; `setFlipProgress` animates.\n */\nexport class CssRenderer implements Renderer {\n #container: HTMLElement | null = null;\n #clip!: HTMLDivElement;\n #viewport!: HTMLDivElement;\n #book!: HTMLDivElement;\n #pageLeft!: HTMLCanvasElement;\n #pageRight!: HTMLCanvasElement;\n #leaf!: HTMLDivElement;\n #leafFront!: HTMLCanvasElement;\n #leafBack!: HTMLCanvasElement;\n #leafShadow!: HTMLDivElement;\n // Lone-page centering, interpolated across a flip (see the WebGL renderer).\n #fromShiftUnit = 0;\n #toShiftUnit = 0;\n // Book aspect fit: the spread is letterboxed to the page aspect (see the WebGL renderer).\n #pageAspect = 0;\n #bookW = 0;\n #fill = false;\n #contentShift = 0; // shiftUnit of the current static spread (nonzero = lone page, centered)\n\n mount(container: HTMLElement): Promise<void> {\n const doc = container.ownerDocument;\n this.#container = container;\n\n // Fixed-size clip box; NOT transformed, so zoom can't scale the crop region.\n this.#clip = doc.createElement('div');\n this.#clip.className = 'zine-clip';\n this.#clip.style.cssText = 'position:relative;width:100%;height:100%;overflow:hidden;';\n\n // Transform target for zoom/pan; lives inside the clip.\n this.#viewport = doc.createElement('div');\n this.#viewport.className = 'zine-viewport';\n this.#viewport.style.cssText = 'position:absolute;inset:0;transform-origin:0 0;';\n\n this.#book = doc.createElement('div');\n this.#book.className = 'zine-book';\n this.#book.style.cssText =\n 'position:absolute;inset:0;perspective:2000px;transform-style:preserve-3d;';\n\n this.#pageLeft = this.#makeCanvas(doc, 'zine-page-left', 'left:0;');\n this.#pageRight = this.#makeCanvas(doc, 'zine-page-right', 'right:0;');\n\n this.#leaf = doc.createElement('div');\n this.#leaf.className = 'zine-leaf';\n this.#leaf.style.cssText =\n 'position:absolute;top:0;width:50%;height:100%;transform-style:preserve-3d;display:none;';\n\n this.#leafFront = doc.createElement('canvas');\n this.#leafFront.className = 'zine-leaf-front';\n this.#leafFront.style.cssText =\n 'position:absolute;inset:0;width:100%;height:100%;backface-visibility:hidden;';\n\n this.#leafBack = doc.createElement('canvas');\n this.#leafBack.className = 'zine-leaf-back';\n this.#leafBack.style.cssText =\n 'position:absolute;inset:0;width:100%;height:100%;backface-visibility:hidden;transform:rotateY(180deg);';\n\n this.#leafShadow = doc.createElement('div');\n this.#leafShadow.className = 'zine-leaf-shadow';\n this.#leafShadow.style.cssText = 'position:absolute;inset:0;opacity:0;pointer-events:none;';\n this.#leafShadow.style.background = foldOverlay('forward'); // default; beginFlip sets per direction\n\n this.#leaf.append(this.#leafFront, this.#leafBack, this.#leafShadow);\n this.#book.append(this.#pageLeft, this.#pageRight, this.#leaf);\n this.#viewport.append(this.#book);\n this.#clip.append(this.#viewport);\n container.append(this.#clip);\n\n return Promise.resolve();\n }\n\n destroy(): void {\n this.#clip?.remove();\n this.#container = null;\n }\n\n renderSpread(_spread: Spread, content: SpreadContent, options?: RenderOptions): void {\n if (options?.fill) {\n // Single-page mode: the lone page fills the container; hide the other panel.\n this.#pageLeft.style.width = '100%';\n this.#pageRight.style.display = 'none';\n this.#paint(this.#pageLeft, content.right ?? content.left);\n } else {\n this.#pageLeft.style.width = '50%';\n this.#pageRight.style.display = '';\n this.#paint(this.#pageLeft, content.left);\n this.#paint(this.#pageRight, content.right);\n }\n this.#fill = options?.fill ?? false;\n this.#contentShift = this.#fill ? 0 : shiftUnit(content);\n this.#trackAspect(content);\n this.#layoutBook();\n this.#applyShift(this.#contentShift);\n // A fresh spread cancels any in-progress flip.\n this.#leaf.style.display = 'none';\n this.#leaf.style.transform = '';\n this.#leafShadow.style.opacity = '0';\n }\n\n beginFlip(\n from: SpreadContent,\n to: SpreadContent,\n direction: FlipDirection,\n options?: RenderOptions,\n ): void {\n if (options?.fill) {\n // Single-page: a full-width leaf turns about one edge, revealing `to` underneath.\n this.#pageLeft.style.width = '100%';\n this.#pageRight.style.display = 'none';\n this.#leaf.style.width = '100%';\n this.#leaf.style.left = '0';\n this.#leaf.style.transformOrigin = direction === 'forward' ? 'left center' : 'right center';\n this.#paint(this.#pageLeft, to.right ?? to.left);\n this.#paint(this.#leafFront, from.right ?? from.left);\n this.#paint(this.#leafBack, to.right ?? to.left);\n } else {\n this.#pageLeft.style.width = '50%';\n this.#pageRight.style.display = '';\n this.#leaf.style.width = '50%';\n if (direction === 'forward') {\n // Right page lifts and swings left about the spine. Left stays; right reveals `to`.\n this.#paint(this.#pageLeft, from.left);\n this.#paint(this.#pageRight, to.right);\n this.#paint(this.#leafFront, from.right);\n this.#paint(this.#leafBack, to.left);\n this.#leaf.style.left = '50%';\n this.#leaf.style.transformOrigin = 'left center';\n } else {\n // Left page swings right about its right edge. Right stays; left reveals `to`.\n this.#paint(this.#pageRight, from.right);\n this.#paint(this.#pageLeft, to.left);\n this.#paint(this.#leafFront, from.left);\n this.#paint(this.#leafBack, to.right);\n this.#leaf.style.left = '0';\n this.#leaf.style.transformOrigin = 'right center';\n }\n }\n this.#fromShiftUnit = options?.fill ? 0 : shiftUnit(from);\n this.#toShiftUnit = options?.fill ? 0 : shiftUnit(to);\n this.#fill = options?.fill ?? false;\n this.#trackAspect(to);\n this.#layoutBook();\n this.#applyShift(this.#fromShiftUnit);\n this.#leaf.style.display = 'block';\n this.#leaf.style.transform = 'rotateY(0deg)';\n this.#leafShadow.style.background = foldOverlay(direction);\n this.#leafShadow.style.opacity = '0';\n }\n\n setFlipProgress(t: number, direction: FlipDirection): void {\n const pose = flipProgressToPose(t);\n const deg = pose.angle * RAD_TO_DEG;\n this.#applyShift(this.#fromShiftUnit + (this.#toShiftUnit - this.#fromShiftUnit) * t);\n this.#leaf.style.display = 'block';\n this.#leaf.style.transform = `rotateY(${direction === 'forward' ? -deg : deg}deg)`;\n this.#leafShadow.style.opacity = String(pose.shadowAlpha);\n }\n\n #applyShift(unit: number): void {\n const px = unit * (this.#bookW / 4);\n this.#book.style.transform = px ? `translateX(${px}px)` : '';\n }\n\n /** Remember the page aspect (width/height) so the book can be letterboxed to it. */\n #trackAspect(content: SpreadContent): void {\n const p = content.left ?? content.right;\n if (p && p.height) this.#pageAspect = p.width / p.height;\n }\n\n /** Letterbox the 2-page book to the page aspect, centered in the container, so pages\n * fill their halves without stretching; the surround is the container background. */\n #layoutBook(): void {\n const b = this.#bookBox();\n this.#bookW = b.width;\n const s = this.#book.style;\n s.inset = 'auto';\n s.left = `${b.x}px`;\n s.top = `${b.y}px`;\n s.width = `${b.width}px`;\n s.height = `${b.height}px`;\n }\n\n /** The fitted book rect in container px (letterbox aware) — a stable 2-page area. */\n #bookBox(): { x: number; y: number; width: number; height: number } {\n const cw = this.#container?.clientWidth ?? 0;\n const ch = this.#container?.clientHeight ?? 0;\n if (this.#pageAspect <= 0 || cw <= 0 || ch <= 0) return { x: 0, y: 0, width: cw, height: ch };\n const ba = (this.#fill ? 1 : 2) * this.#pageAspect;\n let bw = cw;\n let bh = ch;\n if (ba > cw / ch) bh = cw / ba;\n else bw = ch * ba;\n return { x: (cw - bw) / 2, y: (ch - bh) / 2, width: bw, height: bh };\n }\n\n /** Where the current spread is actually painted: the book, or its centered half for a lone\n * page (mirrors #applyShift). This is what the engine hit-tests against. */\n #contentBox(): { x: number; y: number; width: number; height: number } {\n const b = this.#bookBox();\n return this.#contentShift !== 0 ? { x: b.x + b.width / 4, y: b.y, width: b.width / 2, height: b.height } : b;\n }\n\n setViewTransform(scale: number, x: number, y: number): void {\n this.#viewport.style.transform = `translate(${x}px, ${y}px) scale(${scale})`;\n }\n\n measure(): LayoutMetrics {\n const el = this.#container;\n const w = el?.clientWidth ?? 0;\n const h = el?.clientHeight ?? 0;\n const known = this.#pageAspect > 0;\n const book = known ? this.#bookBox() : undefined;\n const content = known ? this.#contentBox() : undefined;\n // The shift lives on #book, inside the scaled #viewport, so unlike the WebGL2 renderer it\n // magnifies along with everything else and the whole box simply scales.\n const screenAt = content\n ? (scale: number): { x: number; y: number; width: number; height: number } => ({\n x: content.x * scale,\n y: content.y * scale,\n width: content.width * scale,\n height: content.height * scale,\n })\n : undefined;\n return {\n containerWidth: w,\n containerHeight: h,\n pageWidth: w / 2,\n pageHeight: h,\n book,\n content,\n screenAt,\n };\n }\n\n #makeCanvas(doc: Document, className: string, extra: string): HTMLCanvasElement {\n const c = doc.createElement('canvas');\n c.className = className;\n c.style.cssText = `position:absolute;top:0;width:50%;height:100%;${extra}`;\n return c;\n }\n\n #paint(canvas: HTMLCanvasElement, content: PageContent | null): void {\n const ctx = canvas.getContext('2d');\n if (content === null) {\n ctx?.clearRect(0, 0, canvas.width, canvas.height);\n return;\n }\n canvas.width = content.width;\n canvas.height = content.height;\n ctx?.drawImage(content, 0, 0);\n }\n}\n","/**\n * Shared grid mesh + normal utilities for the page-curl models.\n *\n * A curl model fills `positions`/`normals` in place each frame; UVs are static grid\n * coordinates (no texture swimming). All models bend the same (cols+1)×(rows+1) grid.\n */\n\nexport interface PageMesh {\n readonly cols: number;\n readonly rows: number;\n /** ((cols+1)*(rows+1))*2 — static grid UVs (u across width, v down height). */\n readonly uvs: Float32Array;\n /** *3 — deformed positions, updated by the curl model. */\n readonly positions: Float32Array;\n /** *3 — per-vertex normals, updated by the curl model. */\n readonly normals: Float32Array;\n}\n\n/** Allocate a (cols+1)×(rows+1) grid mesh with static UVs (positions/normals filled by a curl). */\nexport function createPageMesh(cols: number, rows: number): PageMesh {\n const nx = cols + 1;\n const ny = rows + 1;\n const count = nx * ny;\n const uvs = new Float32Array(count * 2);\n for (let j = 0; j < ny; j++) {\n for (let i = 0; i < nx; i++) {\n const k = j * nx + i;\n uvs[k * 2] = i / cols;\n uvs[k * 2 + 1] = j / rows;\n }\n }\n return {\n cols,\n rows,\n uvs,\n positions: new Float32Array(count * 3),\n normals: new Float32Array(count * 3),\n };\n}\n\n/** Per-vertex normals from central differences of the deformed grid (front = +z when flat). */\nexport function computeNormals(mesh: PageMesh): void {\n const nx = mesh.cols + 1;\n const ny = mesh.rows + 1;\n const pos = mesh.positions;\n const nrm = mesh.normals;\n\n for (let j = 0; j < ny; j++) {\n for (let i = 0; i < nx; i++) {\n const iPrev = i > 0 ? i - 1 : i;\n const iNext = i < nx - 1 ? i + 1 : i;\n const jPrev = j > 0 ? j - 1 : j;\n const jNext = j < ny - 1 ? j + 1 : j;\n\n const a = (j * nx + iNext) * 3;\n const b = (j * nx + iPrev) * 3;\n const c = (jNext * nx + i) * 3;\n const e = (jPrev * nx + i) * 3;\n\n const tuX = pos[a]! - pos[b]!;\n const tuY = pos[a + 1]! - pos[b + 1]!;\n const tuZ = pos[a + 2]! - pos[b + 2]!;\n const tvX = pos[c]! - pos[e]!;\n const tvY = pos[c + 1]! - pos[e + 1]!;\n const tvZ = pos[c + 2]! - pos[e + 2]!;\n\n let nx0 = tuY * tvZ - tuZ * tvY;\n let ny0 = tuZ * tvX - tuX * tvZ;\n let nz0 = tuX * tvY - tuY * tvX;\n const l = Math.hypot(nx0, ny0, nz0) || 1;\n nx0 /= l;\n ny0 /= l;\n nz0 /= l;\n\n const k = (j * nx + i) * 3;\n nrm[k] = nx0;\n nrm[k + 1] = ny0;\n nrm[k + 2] = nz0;\n }\n }\n}\n","import { computeNormals, type PageMesh } from './mesh';\nimport type { CurlAnchor } from './types';\n\n/**\n * Cone angle at peak curl. π/2 is flat; smaller values wind the sheet tighter.\n * ~30° gives a soft, paper-like ridge without self-intersection on typical page\n * aspect ratios (Hong / Nuon / Project Austin all land in the 18–35° band).\n */\nconst THETA_MIN = (30 * Math.PI) / 180;\n\n/**\n * How far past the page the cone apex sits, as a fraction of height, at rest.\n * Closer mid-turn sharpens the diagonal; farther reads more cylindrical.\n * Kept moderately distant so the sheet stays page-like without cloth-y sliding.\n */\nconst APEX_REST = 1.35;\nconst APEX_PEAK = 0.7;\n\n/**\n * Above this |anchor.y − 0.5| the deform is a pure cone; at the mid-edge it\n * blends fully to a cylinder (Project Austin’s conicContribution).\n */\nconst CORNER_PURE = 0.42;\n\n/**\n * Curl envelope ends before t=1 so the last stretch is a flat spine settle.\n * Holding sin(πt) curl into the 0.9s left a tall residual ridge that deflated\n * while the free edge was already nearly home — that read as a rebound bounce.\n */\nconst CURL_END = 0.84;\n\n/**\n * Spine rotation reaches ±π before t=1 and holds. Flip easing decelerates hard into\n * the end (a near-still tail after PAGE_LEAD); finishing the flop early keeps that\n * tail from slowly dropping the leaf into place (the settle/rebound feel).\n * Same schedule for lone pages: the dissolve then covers a still, landed sheet\n * rather than a late whip off the hinge.\n */\nconst RHO_END = 0.88;\n\n/**\n * Natural conical page curl — the Xerox PARC / iBooks model.\n *\n * The leaf wraps an imaginary right circular cone whose apex rides the spine and\n * whose opening angle θ breathes over the turn (Hong, Card & Chen, 2004). A corner\n * grab yields a diagonal cone; a mid-edge grab blends toward a cylinder so the\n * free edge lifts as one. The curled sheet then rotates about the spine and settles\n * flat on the far side. Arc length from the apex is preserved, so the paper never\n * stretches — the motion reads as stiff, premium stock rather than cloth.\n *\n * Lone pages (`anchor.fill`) use the same cone and flop as a spread half; the\n * renderer hinges at the container edge and dissolves the leaf as it finishes.\n * Softening or delaying the fill path made single-page turns read as a peel-fade\n * instead of a page turn.\n */\nexport function deformCone(mesh: PageMesh, W: number, H: number, t: number, anchor: CurlAnchor): void {\n const nx = mesh.cols + 1;\n const ny = mesh.rows + 1;\n const pos = mesh.positions;\n\n // Curl envelope: 0 at the ends (flat), 1 near mid-turn. Span ends at CURL_END\n // so the ridge is gone before the spine holds the landed pose through the ease tail.\n const curlT = t >= CURL_END ? 1 : t / CURL_END;\n const curl = Math.sin(Math.PI * curlT);\n\n // θ: π/2 (identity) → THETA_MIN (pronounced curl) → π/2.\n const theta = Math.PI / 2 - curl * (Math.PI / 2 - THETA_MIN);\n const sinTheta = Math.sin(theta);\n const cosTheta = Math.cos(theta);\n const invSinTheta = 1 / Math.max(sinTheta, 1e-6);\n\n // Apex distance: pulled in as the curl develops for a tighter diagonal.\n const apexDist = H * (APEX_REST + (APEX_PEAK - APEX_REST) * curl);\n\n // Corner bias: −1 = top edge, +1 = bottom. Near 0 → cylinder blend.\n const bias = (anchor.y - 0.5) * 2;\n const fromBottom = bias >= 0;\n const absBias = Math.abs(bias);\n let conic = Math.min(1, absBias / CORNER_PURE);\n const cylWeight = 1 - conic;\n\n // Cylinder radius matched to the cone’s free-edge cross-section (Austin), so\n // the blend is continuous as the grab slides from corner to mid-edge.\n // Apex sits past the *leading* edge (below for a bottom grab, above for top).\n const AyRef = fromBottom ? H + apexDist : -apexDist;\n const cylR = Math.hypot(W, 0.5 * H - AyRef);\n const cylRadius = Math.max(cylR * sinTheta, W / Math.PI);\n\n // Spine rotation: concurrent with the cone so the page flops while it curls.\n // Same x–z convention as `roll` (ρ: 0 → −π). Finish by RHO_END and hold so the\n // ease-out / dissolve tail stays still (spread land or lone fade alike).\n // `anchor.fill` is a renderer concern; geometry matches a spread half.\n const rhoT = Math.min(1, t / RHO_END);\n const rho = -Math.PI * rhoT;\n const cosR = Math.cos(rho);\n const sinR = Math.sin(rho);\n\n // Near the rests the cone is the identity plane. A finite-radius cylinder blend\n // still carries a tiny residual arc — skip it and rotate the flat sheet.\n if (curl < 1e-4) {\n for (let j = 0; j < ny; j++) {\n const y = (j / mesh.rows) * H;\n for (let i = 0; i < nx; i++) {\n const x0 = (i / mesh.cols) * W;\n const k = (j * nx + i) * 3;\n pos[k] = x0 * cosR;\n pos[k + 1] = y;\n pos[k + 2] = -x0 * sinR;\n }\n }\n computeNormals(mesh);\n return;\n }\n\n for (let j = 0; j < ny; j++) {\n const y0 = (j / mesh.rows) * H;\n for (let i = 0; i < nx; i++) {\n const x0 = (i / mesh.cols) * W;\n\n let cx = x0;\n let cy = y0;\n let cz = 0;\n\n if (conic > 1e-5) {\n // Canonical cone: apex above the page (A < 0) makes the *near* edge lead.\n // In our y-down leaf, that means the top. For a bottom grab we flip into\n // that space, deform, then flip back — one code path for both corners.\n const yIn = fromBottom ? H - y0 : y0;\n const A = -apexDist;\n const R = Math.hypot(x0, yIn - A);\n if (R > 1e-8) {\n const r = R * sinTheta;\n const beta = Math.asin(Math.min(1, Math.max(0, x0 / R))) * invSinTheta;\n const oneCos = 1 - Math.cos(beta);\n const yCone = R + A - r * oneCos * sinTheta;\n cx = r * Math.sin(beta);\n cy = fromBottom ? H - yCone : yCone;\n cz = r * oneCos * cosTheta;\n }\n }\n\n let bx = cx;\n let by = cy;\n let bz = cz;\n\n if (cylWeight > 1e-5) {\n const beta = x0 / cylRadius;\n const sx = cylRadius * Math.sin(beta);\n const sz = cylRadius * (1 - Math.cos(beta));\n bx = conic * cx + cylWeight * sx;\n by = conic * cy + cylWeight * y0;\n bz = conic * cz + cylWeight * sz;\n }\n\n const k = (j * nx + i) * 3;\n pos[k] = bx * cosR + bz * sinR;\n pos[k + 1] = by;\n pos[k + 2] = -bx * sinR + bz * cosR;\n }\n }\n\n computeNormals(mesh);\n}\n","import { computeNormals, type PageMesh } from './mesh';\n\n/**\n * Flat page turn — the plainest flip, matching the CSS renderer. The leaf stays a rigid\n * flat sheet and rotates around the spine (angle 0→π), going edge-on at the midpoint. No\n * cylinder bend; GPU-rendered here with the shared perspective so it reads like the CSS fold.\n */\nexport function deformSimple(mesh: PageMesh, W: number, H: number, t: number): void {\n const nx = mesh.cols + 1;\n const ny = mesh.rows + 1;\n const pos = mesh.positions;\n\n const angle = t * Math.PI;\n const cosA = Math.cos(angle);\n const sinA = Math.sin(angle);\n\n for (let i = 0; i < nx; i++) {\n const x0 = (i / mesh.cols) * W; // distance from the spine\n const X = x0 * cosA;\n const Z = x0 * sinA;\n for (let j = 0; j < ny; j++) {\n const k = (j * nx + i) * 3;\n pos[k] = X;\n pos[k + 1] = (j / mesh.rows) * H;\n pos[k + 2] = Z;\n }\n }\n\n computeNormals(mesh);\n}\n","import { deformCone } from './cone';\nimport { deformSimple } from './simple';\nimport { IMPORTABLE_CURLS, type CurlModel, type CurlSpec, type CurlType } from './types';\n\n/**\n * The curl models carried in the bundle, and the only ones nameable by string.\n *\n * Kept apart from `./index` so importing them does not drag in the other four: the barrel names\n * every model, which is right for a caller picking one but wrong for the renderer, which must\n * only ever pull these two.\n */\nexport const cone: CurlModel = { deform: deformCone, anchored: true };\nexport const simple: CurlModel = {\n deform: deformSimple,\n anchored: false,\n flat: true,\n gloss: false,\n};\n\nexport const BUNDLED_CURLS: Record<CurlType, CurlModel> = { cone, simple };\n\n/**\n * Turn a `curl` option into a model. Strings name a bundled curl; anything else is already a\n * model, imported from `@zinejs/core/curls` or written by the caller.\n */\nexport function resolveCurl(spec: CurlSpec): CurlModel {\n if (typeof spec !== 'string') return spec;\n const model = BUNDLED_CURLS[spec];\n if (model) return model;\n // Reached only when validation was bypassed (a renderer used directly, say), so name the fix\n // rather than the mistake: these curls are real, they just are not bundled.\n if (IMPORTABLE_CURLS.includes(spec)) {\n throw new Error(\n `Zine: the '${spec}' curl is not bundled. Import it and pass the model: ` +\n `import { ${spec} } from '@zinejs/core/curls' → curl: ${spec}`,\n );\n }\n throw new Error(`Zine: unknown curl ${JSON.stringify(spec)}.`);\n}\n","/** Create a WebGL2 context, or throw so the caller can fall back to CSS (§8.4). */\nexport function createGlContext(canvas: HTMLCanvasElement): WebGL2RenderingContext {\n const gl = canvas.getContext('webgl2', {\n alpha: true,\n premultipliedAlpha: true,\n antialias: true,\n depth: false,\n stencil: false,\n // We render on demand (not every frame), so the buffer must persist between\n // composites — otherwise the settled page flickers once the flip animation\n // stops and nothing is redrawing each frame.\n preserveDrawingBuffer: true,\n });\n if (gl === null) {\n throw new Error('WebglRenderer: could not create a WebGL2 context.');\n }\n return gl;\n}\n","/** Compile + link a shader program; throws with the driver log on failure. */\nexport function createProgram(\n gl: WebGL2RenderingContext,\n vertexSrc: string,\n fragmentSrc: string,\n): WebGLProgram {\n const program = gl.createProgram();\n if (program === null) throw new Error('WebglRenderer: could not create a program.');\n\n const vs = compileShader(gl, gl.VERTEX_SHADER, vertexSrc);\n const fs = compileShader(gl, gl.FRAGMENT_SHADER, fragmentSrc);\n gl.attachShader(program, vs);\n gl.attachShader(program, fs);\n gl.linkProgram(program);\n\n // Shaders can be detached/deleted once linked.\n gl.deleteShader(vs);\n gl.deleteShader(fs);\n\n if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n const log = gl.getProgramInfoLog(program);\n gl.deleteProgram(program);\n throw new Error(`WebglRenderer: program link failed: ${log ?? 'unknown error'}`);\n }\n return program;\n}\n\nfunction compileShader(gl: WebGL2RenderingContext, type: number, src: string): WebGLShader {\n const shader = gl.createShader(type);\n if (shader === null) throw new Error('WebglRenderer: could not create a shader.');\n gl.shaderSource(shader, src);\n gl.compileShader(shader);\n if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n const log = gl.getShaderInfoLog(shader);\n gl.deleteShader(shader);\n throw new Error(`WebglRenderer: shader compile failed: ${log ?? 'unknown error'}`);\n }\n return shader;\n}\n","/** Create a page texture with clamp + linear filtering (no mipmaps — §8.3). */\nexport function createTexture(gl: WebGL2RenderingContext): WebGLTexture {\n const tex = gl.createTexture();\n if (tex === null) throw new Error('WebglRenderer: could not create a texture.');\n gl.bindTexture(gl.TEXTURE_2D, tex);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);\n return tex;\n}\n\n/**\n * Upload a decoded page raster (ImageBitmap or canvas) into `tex`, returning the dimensions actually\n * uploaded (which differ from the source when it was too big for the GPU).\n *\n * A raster larger than `gl.MAX_TEXTURE_SIZE` on either axis cannot be uploaded: `texImage2D` fails\n * and leaves the texture incomplete, which samples as solid black. A zoomed page on a high-DPR phone\n * (small MAX_TEXTURE_SIZE, e.g. 4096) hits this readily. Downscale over-large rasters to fit,\n * preserving aspect, so a slightly-softer page beats a black one.\n */\nexport function uploadTexture(\n gl: WebGL2RenderingContext,\n tex: WebGLTexture,\n source: TexImageSource,\n): { width: number; height: number } {\n const max = gl.getParameter(gl.MAX_TEXTURE_SIZE) as number;\n const upload = fitToLimit(source, max);\n gl.bindTexture(gl.TEXTURE_2D, tex);\n // No Y flip: the vertex shader maps aUnit.y=0 to the page top and samples the\n // source's top row there. UNPACK_FLIP_Y is honored for <canvas> sources but\n // IGNORED for ImageBitmap, so flipping would invert PDF (canvas) pages while\n // leaving image (ImageBitmap) pages upright. Off keeps both consistent.\n gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);\n gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);\n gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, upload);\n return { width: upload.width, height: upload.height };\n}\n\n/** Return `source` unchanged if it fits `max`, else a canvas holding it scaled down to fit. */\nfunction fitToLimit(source: TexImageSource, max: number): TexImageSource & { width: number; height: number } {\n const dims = source as unknown as { width: number; height: number };\n const longest = Math.max(dims.width, dims.height);\n if (!Number.isFinite(max) || max <= 0 || longest <= max) {\n return dims as TexImageSource & { width: number; height: number };\n }\n const factor = max / longest;\n const canvas = document.createElement('canvas');\n canvas.width = Math.max(1, Math.floor(dims.width * factor));\n canvas.height = Math.max(1, Math.floor(dims.height * factor));\n canvas.getContext('2d')?.drawImage(source as CanvasImageSource, 0, 0, canvas.width, canvas.height);\n return canvas;\n}\n","// Imported from the leaf modules, not the barrel: the barrel pulls in every curl, which would\n// defeat the point of only bundling the two the engine can name.\nimport { createPageMesh, type PageMesh } from '../geometry/curls/mesh';\nimport { BUNDLED_CURLS, resolveCurl } from '../geometry/curls/bundled';\nimport { DEFAULT_CURL, type CurlAnchor, type CurlModel, type CurlSpec } from '../geometry/curls/types';\nimport type { Spread } from '../engine/spread';\nimport type {\n FlipDirection,\n LayoutMetrics,\n Renderer,\n RenderOptions,\n SpreadContent,\n} from './types';\nimport { createGlContext } from './gl/context';\nimport { createProgram } from './gl/program';\nimport { createTexture, uploadTexture } from './gl/texture';\n\n// Flat program: a page rect (device px, top-left origin) with zoom/pan and a soft\n// permanent gutter shadow toward the spine so the turning leaf's shadow lands into it.\nconst FLAT_VERT = `#version 300 es\nin vec2 aUnit;\nuniform vec2 uViewport;\nuniform vec4 uRect;\nuniform vec3 uView;\nout vec2 vUv;\nvoid main() {\n vec2 px = uRect.xy + aUnit * uRect.zw;\n px = px * uView.x + uView.yz;\n vec2 clip = px / uViewport * 2.0 - 1.0;\n gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0);\n vUv = aUnit;\n}`;\n\nconst FLAT_FRAG = `#version 300 es\nprecision mediump float;\nin vec2 vUv;\nuniform sampler2D uTex;\nuniform float uGutterSide; // +1 = spine at right edge, -1 = at left edge, 0 = none\nout vec4 outColor;\nvoid main() {\n vec4 c = texture(uTex, vUv);\n if (uGutterSide != 0.0) {\n float d = uGutterSide > 0.0 ? (1.0 - vUv.x) : vUv.x;\n c.rgb *= mix(0.72, 1.0, smoothstep(0.0, 0.10, d));\n }\n outColor = c;\n}`;\n\n// Curl program: a thin pass-through for the CPU-deformed page mesh (positions and\n// normals come from the selected curl model). It places the leaf, applies a top-safe depth\n// perspective, and lights from the true surface normal.\nconst CURL_VERT = `#version 300 es\nin vec3 aPos; // leaf-local device px: x = bent offset from spine, y 0..H, z depth\nin vec3 aNormal;\nin vec2 aUv;\nuniform vec2 uViewport;\nuniform vec3 uView; // scale, tx, ty\nuniform float uOriginX; // spine x in device px\nuniform float uDir; // +1 leaf to the right of spine, -1 to the left\nuniform float uLeafW; // leaf width in device px (perspective scale)\nout vec2 vUv;\nout float vFacing;\nout float vU;\nvoid main() {\n float bookX = uOriginX + uDir * aPos.x;\n float bookY = aPos.y;\n float Z = aPos.z;\n // Eye distance, in leaf widths. Keeps the perspective resolution-independent. 5.3 puts the\n // roll's deepest point ~14% larger than flat; stronger than that (a nearer eye) balloons the\n // turning sheet and reads more like a fisheye than a page lifting.\n float D = uLeafW * 5.3;\n float persp = D / max(D - Z, 1.0);\n float cx = uViewport.x * 0.5;\n float cy = uViewport.y * 0.5;\n float px = cx + (bookX - cx) * persp;\n float py = cy + (bookY - cy) * min(persp, 1.0); // Y only shrinks (top-safe)\n vec2 p = vec2(px, py) * uView.x + uView.yz;\n vec2 clip = p / uViewport * 2.0 - 1.0;\n gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0);\n vUv = aUv;\n vFacing = aNormal.z;\n vU = aUv.x;\n}`;\n\nconst CURL_FRAG = `#version 300 es\nprecision mediump float;\nin vec2 vUv;\nin float vFacing;\nin float vU;\nuniform sampler2D uFront;\nuniform sampler2D uBack;\nuniform highp float uDir;\nuniform float uGloss; // 0 disables the specular highlight (e.g. the flat 'simple' curl)\nuniform float uAlpha; // leaf opacity; a lone page dissolves into the page landing beneath it\nuniform float uCrease; // spine-crease strength: 1 matches a spread's gutter; ramps from 0 for a lone leaf\nout vec4 outColor;\nvoid main() {\n // The turn mesh carries its own facing in the normal; the front points at the viewer\n // (normal.z > 0) until the leaf passes vertical, for both flip directions. u->x still\n // flips with uDir so a left-hand leaf reads spine-inward.\n bool showFront = vFacing > 0.0;\n float fx = uDir > 0.0 ? vUv.x : 1.0 - vUv.x;\n vec4 c = showFront ? texture(uFront, vec2(fx, vUv.y)) : texture(uBack, vec2(1.0 - fx, vUv.y));\n\n // Fold shading: the sheet darkens sharply where it curves away from the viewer, so the\n // rolled edge reads as a deep crease with a soft highlight riding the ridge.\n float diff = clamp(abs(vFacing), 0.0, 1.0);\n float lit = mix(0.42, 1.0, diff);\n // Spine crease matches the gutter shadow on a spread. A lone page has no gutter, so uCrease\n // ramps it in from 0 as the leaf lifts — otherwise a still-flat leaf paints a shadow band on\n // the anchor edge before the turn even begins.\n lit *= mix(1.0, mix(0.7, 1.0, smoothstep(0.0, 0.12, vU)), uCrease);\n\n // Glossy specular: brightest where the surface tilts ~30 deg from facing the viewer,\n // so a thin glossy highlight rides across the sheet as it rolls.\n float spec = pow(max(cos(acos(diff) - 0.52), 0.0), 220.0) * 0.22 * uGloss;\n\n c.rgb = clamp(c.rgb * lit + spec, 0.0, 1.0);\n // Premultiplied alpha (blendFunc ONE, ONE_MINUS_SRC_ALPHA): scale colour as well as alpha,\n // or fading would brighten the sheet additively instead of dissolving it.\n outColor = vec4(c.rgb * uAlpha, c.a * uAlpha);\n}`;\n\nconst GRID_COLS = 28;\nconst GRID_ROWS = 36;\n/** Progress at which a lone page starts dissolving. A full-width leaf has no facing half to\n * flop onto, so instead of landing on empty space it fades into the page arriving beneath it\n * over the rest of the turn. Roll is already flat by mid-turn; curling models stay bent\n * longer, so they dissolve later. Timed so the fade covers the still landed pose (curl RHO_END)\n * rather than washing out a sheet that is still swinging. */\nconst FILL_FADE_START_ROLL = 0.7;\nconst FILL_FADE_START_CURL = 0.78;\nconst RESTORE_TIMEOUT_MS = 4000;\nconst MAX_TEXTURE_CACHE_BYTES = 256 * 1024 * 1024;\n\ninterface Rect {\n x: number;\n y: number;\n w: number;\n h: number;\n}\n\ninterface FlipState {\n underLeft: TexImageSource | null;\n underRight: TexImageSource | null;\n underFull: TexImageSource | null; // fill mode\n front: TexImageSource | null;\n back: TexImageSource | null;\n dir: number; // +1 forward (leaf right of spine), -1 backward (left)\n fill: boolean;\n}\n\n/** How far to shift a spread to centre a lone page, in units of a quarter container width:\n * -1 = only a right page (cover), +1 = only a left page (back), 0 = a full spread. */\nfunction shiftUnit(content: SpreadContent): number {\n const lone = (content.left === null) !== (content.right === null);\n return lone ? (content.right !== null ? -1 : 1) : 0;\n}\n\n/**\n * GPU renderer (WebGL2, hand-written). The turning leaf is a dense grid deformed on\n * the CPU by the selected curl model (see geometry/curls) and\n * uploaded each frame, lit from real normals; the static pages are flat quads with a\n * gutter shadow. Restores\n * GL state on context loss; signals onFatal (→ CSS fallback) when unrecoverable.\n */\nexport class WebglRenderer implements Renderer {\n #container: HTMLElement | null = null;\n #canvas: HTMLCanvasElement | null = null;\n #gl: WebGL2RenderingContext | null = null;\n\n #flat: WebGLProgram | null = null;\n #curl: WebGLProgram | null = null;\n #quadVao: WebGLVertexArrayObject | null = null;\n #curlVao: WebGLVertexArrayObject | null = null;\n #posBuf: WebGLBuffer | null = null;\n #normBuf: WebGLBuffer | null = null;\n #idxCount = 0;\n #mesh: PageMesh = createPageMesh(GRID_COLS, GRID_ROWS);\n\n #texCache = new Map<TexImageSource, { tex: WebGLTexture; bytes: number }>();\n #cacheBytes = 0;\n #blankTex: WebGLTexture | null = null;\n // The GPU's largest accepted texture dimension; a zoom tile past it would upload black (see\n // uploadTexture). Read once at mount and exposed so the source can cap its rasters to fit.\n #maxTextureSize = 0;\n\n #flatU: Record<string, WebGLUniformLocation | null> = {};\n #curlU: Record<string, WebGLUniformLocation | null> = {};\n\n #view = { scale: 1, tx: 0, ty: 0 };\n #dpr = 1;\n #content: SpreadContent | null = null;\n #fill = false;\n #flip: FlipState | null = null;\n #flipT = 0;\n #curlModel: CurlModel = BUNDLED_CURLS[DEFAULT_CURL];\n #anchor: CurlAnchor = { y: 0.5 };\n // A lone page (cover / book front-back) sits on one half; these center it by shifting\n // the view a quarter-width, interpolated across a flip so the open/close doesn't jump.\n #shiftX = 0;\n #fromShiftUnit = 0;\n #toShiftUnit = 0;\n // Book aspect fit: the 2-page spread is letterboxed to the pages' aspect, centered\n // in the container, so pages fill their halves without stretching. 0 = aspect unknown.\n #pageAspect = 0;\n #bookW = 0;\n #bookH = 0;\n\n #contextLost = false;\n #fatalFired = false;\n #restoreTimer: ReturnType<typeof setTimeout> | null = null;\n #fatalHandler: (() => void) | null = null;\n\n mount(container: HTMLElement): Promise<void> {\n this.#container = container;\n const doc = container.ownerDocument;\n const canvas = doc.createElement('canvas');\n canvas.style.cssText = 'display:block;width:100%;height:100%;';\n container.append(canvas);\n this.#canvas = canvas;\n\n canvas.addEventListener('webglcontextlost', this.#onContextLost as EventListener);\n canvas.addEventListener('webglcontextrestored', this.#onContextRestored as EventListener);\n doc.addEventListener('visibilitychange', this.#onVisibilityChange);\n\n this.#gl = createGlContext(canvas); // throws if unavailable -> engine falls back (§8.4)\n this.#maxTextureSize = this.#gl.getParameter(this.#gl.MAX_TEXTURE_SIZE) as number;\n this.#buildGlResources();\n return Promise.resolve();\n }\n\n destroy(): void {\n this.#clearRestoreTimer();\n const canvas = this.#canvas;\n if (canvas !== null) {\n canvas.removeEventListener('webglcontextlost', this.#onContextLost as EventListener);\n canvas.removeEventListener('webglcontextrestored', this.#onContextRestored as EventListener);\n canvas.ownerDocument.removeEventListener('visibilitychange', this.#onVisibilityChange);\n }\n const gl = this.#gl;\n if (gl !== null) {\n for (const cached of this.#texCache.values()) gl.deleteTexture(cached.tex);\n if (this.#blankTex !== null) gl.deleteTexture(this.#blankTex);\n if (this.#quadVao !== null) gl.deleteVertexArray(this.#quadVao);\n if (this.#curlVao !== null) gl.deleteVertexArray(this.#curlVao);\n if (this.#flat !== null) gl.deleteProgram(this.#flat);\n if (this.#curl !== null) gl.deleteProgram(this.#curl);\n gl.getExtension('WEBGL_lose_context')?.loseContext();\n }\n canvas?.remove();\n this.#texCache.clear();\n this.#cacheBytes = 0;\n this.#blankTex = null;\n this.#container = null;\n this.#canvas = null;\n this.#gl = null;\n this.#content = null;\n this.#flip = null;\n }\n\n onFatal(handler: () => void): void {\n this.#fatalHandler = handler;\n }\n\n get maxTextureSize(): number | undefined {\n return this.#maxTextureSize || undefined;\n }\n\n renderSpread(_spread: Spread, content: SpreadContent, options?: RenderOptions): void {\n this.#content = content;\n this.#fill = options?.fill ?? false;\n this.#flip = null;\n this.#trackAspect(content);\n this.#render();\n }\n\n /** Remember the page aspect (width/height) so the book can be letterboxed to it. */\n #trackAspect(content: SpreadContent): void {\n const p = content.left ?? content.right;\n if (p && p.height) this.#pageAspect = p.width / p.height;\n }\n\n beginFlip(\n from: SpreadContent,\n to: SpreadContent,\n direction: FlipDirection,\n options?: RenderOptions,\n ): void {\n const fill = options?.fill ?? false;\n this.#flipT = 0;\n if (options?.curl) this.#curlModel = resolveCurl(options.curl);\n if (options?.anchor) this.#anchor = options.anchor;\n this.#fromShiftUnit = fill ? 0 : shiftUnit(from);\n this.#toShiftUnit = fill ? 0 : shiftUnit(to);\n this.#trackAspect(to);\n if (fill) {\n this.#flip = {\n underLeft: null,\n underRight: null,\n underFull: to.right ?? to.left,\n front: from.right ?? from.left,\n // A lone page has no facing \"next\" leaf on its back; show the same page mirrored\n // (the shader flips the back UV) so it reads as the sheet's own reverse side.\n back: from.right ?? from.left,\n dir: direction === 'forward' ? 1 : -1,\n fill: true,\n };\n } else if (direction === 'forward') {\n this.#flip = {\n underLeft: from.left,\n underRight: to.right,\n underFull: null,\n front: from.right,\n back: to.left,\n dir: 1,\n fill: false,\n };\n } else {\n this.#flip = {\n underLeft: to.left,\n underRight: from.right,\n underFull: null,\n front: from.left,\n back: to.right,\n dir: -1,\n fill: false,\n };\n }\n this.#render();\n }\n\n setFlipProgress(t: number, _direction: FlipDirection): void {\n this.#flipT = t;\n this.#render();\n }\n\n setViewTransform(scale: number, x: number, y: number): void {\n this.#view = { scale, tx: x, ty: y };\n this.#render();\n }\n\n measure(): LayoutMetrics {\n const el = this.#container;\n const w = el?.clientWidth ?? 0;\n const h = el?.clientHeight ?? 0;\n const known = this.#pageAspect > 0;\n const book = known ? this.#bookBox() : undefined;\n const content = known ? this.#contentBox() : undefined;\n const screenAt = content\n ? (scale: number): { x: number; y: number; width: number; height: number } => {\n // The shader applies `px * scale + view.t + shiftX`, so the lone-page shift sits\n // *outside* the scale: a fixed screen offset, not part of the magnified geometry.\n const shiftCss = this.#shiftX / this.#dpr;\n return {\n x: (content.x - shiftCss) * scale + shiftCss,\n y: content.y * scale,\n width: content.width * scale,\n height: content.height * scale,\n };\n }\n : undefined;\n return {\n containerWidth: w,\n containerHeight: h,\n pageWidth: w / 2,\n pageHeight: h,\n book,\n content,\n screenAt,\n };\n }\n\n /** The fitted book rect in container CSS px (letterbox aware) — a stable 2-page area. */\n #bookBox(): { x: number; y: number; width: number; height: number } {\n const el = this.#container;\n const cw = el?.clientWidth ?? 0;\n const ch = el?.clientHeight ?? 0;\n if (this.#pageAspect <= 0 || cw <= 0 || ch <= 0) return { x: 0, y: 0, width: cw, height: ch };\n const ba = (this.#fill ? 1 : 2) * this.#pageAspect;\n let bw = cw;\n let bh = ch;\n if (ba > cw / ch) bh = cw / ba;\n else bw = ch * ba;\n return { x: (cw - bw) / 2, y: (ch - bh) / 2, width: bw, height: bh };\n }\n\n /** Where the current spread is actually painted: the book, or its centered half for a lone\n * page (mirrors the draw-time #shiftX centering). This is what the engine hit-tests against. */\n #contentBox(): { x: number; y: number; width: number; height: number } {\n const b = this.#bookBox();\n const lone = !this.#fill && this.#content !== null && shiftUnit(this.#content) !== 0;\n return lone ? { x: b.x + b.width / 4, y: b.y, width: b.width / 2, height: b.height } : b;\n }\n\n // --- context loss / restore ---------------------------------------------------\n\n #onContextLost = (event: Event): void => {\n event.preventDefault();\n this.#contextLost = true;\n if (this.#restoreTimer === null) {\n this.#restoreTimer = setTimeout(() => this.#fatal(), RESTORE_TIMEOUT_MS);\n }\n };\n\n #onContextRestored = (): void => {\n this.#clearRestoreTimer();\n this.#contextLost = false;\n this.#buildGlResources();\n this.#render();\n };\n\n #onVisibilityChange = (): void => {\n if (this.#canvas?.ownerDocument.visibilityState === 'visible') this.#render();\n };\n\n #fatal(): void {\n if (this.#fatalFired) return;\n this.#fatalFired = true;\n this.#fatalHandler?.();\n }\n\n #clearRestoreTimer(): void {\n if (this.#restoreTimer !== null) {\n clearTimeout(this.#restoreTimer);\n this.#restoreTimer = null;\n }\n }\n\n #buildGlResources(): void {\n const gl = this.#gl;\n if (gl === null) return;\n\n this.#flat = createProgram(gl, FLAT_VERT, FLAT_FRAG);\n this.#curl = createProgram(gl, CURL_VERT, CURL_FRAG);\n for (const name of ['uViewport', 'uRect', 'uView', 'uTex', 'uGutterSide']) {\n this.#flatU[name] = gl.getUniformLocation(this.#flat, name);\n }\n for (const name of ['uViewport', 'uView', 'uOriginX', 'uDir', 'uLeafW', 'uFront', 'uBack', 'uGloss', 'uAlpha', 'uCrease']) {\n this.#curlU[name] = gl.getUniformLocation(this.#curl, name);\n }\n\n this.#quadVao = this.#makeQuadVao(gl, this.#flat);\n this.#buildCurlMesh(gl, this.#curl);\n\n this.#texCache.clear();\n this.#cacheBytes = 0;\n this.#blankTex = this.#makeBlankTexture(gl);\n gl.useProgram(this.#flat);\n gl.uniform1i(this.#flatU.uTex ?? null, 0);\n gl.useProgram(this.#curl);\n gl.uniform1i(this.#curlU.uFront ?? null, 0);\n gl.uniform1i(this.#curlU.uBack ?? null, 1);\n gl.clearColor(0, 0, 0, 0);\n gl.enable(gl.BLEND);\n gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); // premultiplied alpha\n }\n\n #makeQuadVao(gl: WebGL2RenderingContext, program: WebGLProgram): WebGLVertexArrayObject {\n const vao = gl.createVertexArray();\n const buffer = gl.createBuffer();\n if (vao === null || buffer === null) throw new Error('WebglRenderer: could not create buffers.');\n gl.bindVertexArray(vao);\n gl.bindBuffer(gl.ARRAY_BUFFER, buffer);\n gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]), gl.STATIC_DRAW);\n const aUnit = gl.getAttribLocation(program, 'aUnit');\n gl.enableVertexAttribArray(aUnit);\n gl.vertexAttribPointer(aUnit, 2, gl.FLOAT, false, 0, 0);\n gl.bindVertexArray(null);\n return vao;\n }\n\n #buildCurlMesh(gl: WebGL2RenderingContext, program: WebGLProgram): void {\n const nx = GRID_COLS + 1;\n const indices: number[] = [];\n for (let j = 0; j < GRID_ROWS; j++) {\n for (let i = 0; i < GRID_COLS; i++) {\n const a = j * nx + i;\n const b = a + 1;\n const c = a + nx;\n const d = c + 1;\n indices.push(a, c, b, b, c, d);\n }\n }\n this.#idxCount = indices.length;\n\n const vao = gl.createVertexArray();\n this.#posBuf = gl.createBuffer();\n this.#normBuf = gl.createBuffer();\n const uvBuf = gl.createBuffer();\n const idxBuf = gl.createBuffer();\n if (vao === null || this.#posBuf === null || this.#normBuf === null || uvBuf === null || idxBuf === null) {\n throw new Error('WebglRenderer: could not create mesh buffers.');\n }\n gl.bindVertexArray(vao);\n\n gl.bindBuffer(gl.ARRAY_BUFFER, this.#posBuf);\n gl.bufferData(gl.ARRAY_BUFFER, this.#mesh.positions, gl.DYNAMIC_DRAW);\n const aPos = gl.getAttribLocation(program, 'aPos');\n gl.enableVertexAttribArray(aPos);\n gl.vertexAttribPointer(aPos, 3, gl.FLOAT, false, 0, 0);\n\n gl.bindBuffer(gl.ARRAY_BUFFER, this.#normBuf);\n gl.bufferData(gl.ARRAY_BUFFER, this.#mesh.normals, gl.DYNAMIC_DRAW);\n const aNormal = gl.getAttribLocation(program, 'aNormal');\n gl.enableVertexAttribArray(aNormal);\n gl.vertexAttribPointer(aNormal, 3, gl.FLOAT, false, 0, 0);\n\n gl.bindBuffer(gl.ARRAY_BUFFER, uvBuf);\n gl.bufferData(gl.ARRAY_BUFFER, this.#mesh.uvs, gl.STATIC_DRAW);\n const aUv = gl.getAttribLocation(program, 'aUv');\n gl.enableVertexAttribArray(aUv);\n gl.vertexAttribPointer(aUv, 2, gl.FLOAT, false, 0, 0);\n\n gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, idxBuf);\n gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);\n\n gl.bindVertexArray(null);\n this.#curlVao = vao;\n }\n\n // --- drawing ------------------------------------------------------------------\n\n #render(): void {\n const gl = this.#gl;\n const canvas = this.#canvas;\n if (gl === null || canvas === null || this.#contextLost || gl.isContextLost()) return;\n this.#resize();\n gl.clear(gl.COLOR_BUFFER_BIT);\n this.#fitBook();\n if (this.#flip !== null) this.#drawFlip();\n else if (this.#content !== null) this.#drawStatic();\n }\n\n /** Letterbox the 2-page book to the page aspect, centered; restricts the GL viewport\n * so pages fill their halves without stretching (the surround stays cleared). */\n #fitBook(): void {\n const gl = this.#gl!;\n const canvas = this.#canvas!;\n const cw = canvas.width;\n const ch = canvas.height;\n if (this.#pageAspect > 0) {\n const bookAspect = (this.#fill ? 1 : 2) * this.#pageAspect;\n if (bookAspect > cw / ch) {\n this.#bookW = cw;\n this.#bookH = Math.round(cw / bookAspect);\n } else {\n this.#bookH = ch;\n this.#bookW = Math.round(ch * bookAspect);\n }\n } else {\n this.#bookW = cw;\n this.#bookH = ch;\n }\n const bx = Math.round((cw - this.#bookW) / 2);\n const by = Math.round((ch - this.#bookH) / 2); // centered, so GL's bottom offset == by\n gl.viewport(bx, by, this.#bookW, this.#bookH);\n }\n\n #drawStatic(): void {\n const w = this.#bookW;\n const h = this.#bookH;\n const content = this.#content!;\n this.#shiftX = this.#fill ? 0 : shiftUnit(content) * (w / 4);\n this.#useFlat();\n if (this.#fill) {\n this.#drawQuad({ x: 0, y: 0, w, h }, content.right ?? content.left);\n } else {\n // A lone page (one side null) gets no gutter shadow — it's a standalone, centered page.\n this.#drawQuad({ x: 0, y: 0, w: w / 2, h }, content.left, content.right ? 1 : 0);\n this.#drawQuad({ x: w / 2, y: 0, w: w / 2, h }, content.right, content.left ? -1 : 0);\n }\n }\n\n #drawFlip(): void {\n const gl = this.#gl!;\n const w = this.#bookW;\n const h = this.#bookH;\n const flip = this.#flip!;\n\n // Interpolate the lone-page centering across the flip so an opening cover (or a\n // closing back page) slides between centered and spread instead of jumping.\n this.#shiftX = flip.fill\n ? 0\n : (this.#fromShiftUnit + (this.#toShiftUnit - this.#fromShiftUnit) * this.#flipT) * (w / 4);\n\n // Static pages underneath (with gutter shadow).\n this.#useFlat();\n if (flip.fill) {\n this.#drawQuad({ x: 0, y: 0, w, h }, flip.underFull);\n } else {\n this.#drawQuad({ x: 0, y: 0, w: w / 2, h }, flip.underLeft, 1);\n this.#drawQuad({ x: w / 2, y: 0, w: w / 2, h }, flip.underRight, -1);\n }\n\n if (flip.front === null && flip.back === null) return;\n\n // Deform the leaf mesh with the selected curl model on the CPU, upload it, then draw.\n const leafW = flip.fill ? w : w / 2;\n const originX = flip.fill ? (flip.dir > 0 ? 0 : w) : w / 2;\n this.#curlModel.deform(this.#mesh, leafW, h, this.#flipT, {\n y: this.#anchor.y,\n fill: flip.fill,\n });\n gl.bindBuffer(gl.ARRAY_BUFFER, this.#posBuf);\n gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.#mesh.positions);\n gl.bindBuffer(gl.ARRAY_BUFFER, this.#normBuf);\n gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.#mesh.normals);\n\n this.#bindFace(gl.TEXTURE0, flip.front);\n this.#bindFace(gl.TEXTURE1, flip.back);\n gl.activeTexture(gl.TEXTURE0);\n\n gl.useProgram(this.#curl);\n gl.bindVertexArray(this.#curlVao);\n gl.uniform2f(this.#curlU.uViewport ?? null, w, h);\n gl.uniform3f(\n this.#curlU.uView ?? null,\n this.#view.scale,\n this.#view.tx * this.#dpr + this.#shiftX,\n this.#view.ty * this.#dpr,\n );\n gl.uniform1f(this.#curlU.uOriginX ?? null, originX);\n gl.uniform1f(this.#curlU.uDir ?? null, flip.dir);\n gl.uniform1f(this.#curlU.uLeafW ?? null, leafW);\n gl.uniform1f(this.#curlU.uGloss ?? null, this.#curlModel.gloss === false ? 0 : 1);\n gl.uniform1f(this.#curlU.uAlpha ?? null, flip.fill ? this.#fillFade() : 1);\n // Spreads carry the full gutter crease; a lone leaf ramps it in over the first fifth of the\n // turn so a still-flat sheet shows no shadow band at its anchor before it lifts.\n gl.uniform1f(this.#curlU.uCrease ?? null, flip.fill ? Math.min(1, this.#flipT / 0.2) : 1);\n gl.drawElements(gl.TRIANGLES, this.#idxCount, gl.UNSIGNED_SHORT, 0);\n }\n\n /** Leaf opacity for a lone page: solid until the curl-appropriate fade start, then\n * dissolving to 0 as it lands, so the turn resolves into the destination page rather\n * than onto blank space. The fade eases so opacity hangs longer then finishes cleanly. */\n #fillFade(): number {\n const start = this.#curlModel.flat ? FILL_FADE_START_ROLL : FILL_FADE_START_CURL;\n const over = (this.#flipT - start) / (1 - start);\n if (over <= 0) return 1;\n if (over >= 1) return 0;\n // Ease-out the remaining opacity: stay readable through most of the window, then settle.\n return Math.pow(1 - over, 1.5);\n }\n\n #useFlat(): void {\n const gl = this.#gl!;\n gl.useProgram(this.#flat);\n gl.bindVertexArray(this.#quadVao);\n gl.activeTexture(gl.TEXTURE0);\n gl.uniform2f(this.#flatU.uViewport ?? null, this.#bookW, this.#bookH);\n gl.uniform3f(\n this.#flatU.uView ?? null,\n this.#view.scale,\n this.#view.tx * this.#dpr + this.#shiftX,\n this.#view.ty * this.#dpr,\n );\n }\n\n #drawQuad(rect: Rect, source: TexImageSource | null, gutterSide = 0): void {\n const gl = this.#gl!;\n if (source === null) return;\n this.#bindFace(gl.TEXTURE0, source);\n gl.uniform4f(this.#flatU.uRect ?? null, rect.x, rect.y, rect.w, rect.h);\n gl.uniform1f(this.#flatU.uGutterSide ?? null, gutterSide);\n gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);\n }\n\n #bindFace(unit: number, source: TexImageSource | null): void {\n const gl = this.#gl!;\n gl.activeTexture(unit);\n gl.bindTexture(gl.TEXTURE_2D, source !== null ? this.#textureFor(source) : this.#blankTex);\n }\n\n #textureFor(source: TexImageSource): WebGLTexture {\n const gl = this.#gl!;\n const existing = this.#texCache.get(source);\n if (existing !== undefined) {\n this.#texCache.delete(source);\n this.#texCache.set(source, existing);\n return existing.tex;\n }\n const tex = createTexture(gl);\n // uploadTexture may downscale a raster too large for the GPU; account for what it really uploaded.\n const uploaded = uploadTexture(gl, tex, source);\n const bytes = uploaded.width * uploaded.height * 4;\n this.#texCache.set(source, { tex, bytes });\n this.#cacheBytes += bytes;\n this.#evictTextures();\n return tex;\n }\n\n #evictTextures(): void {\n const gl = this.#gl!;\n for (const [src, cached] of this.#texCache) {\n if (this.#cacheBytes <= MAX_TEXTURE_CACHE_BYTES || this.#texCache.size <= 4) break;\n this.#texCache.delete(src);\n this.#cacheBytes -= cached.bytes;\n gl.deleteTexture(cached.tex);\n }\n }\n\n #makeBlankTexture(gl: WebGL2RenderingContext): WebGLTexture {\n const tex = createTexture(gl);\n gl.bindTexture(gl.TEXTURE_2D, tex);\n gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([0, 0, 0, 0]));\n return tex;\n }\n\n #resize(): void {\n const gl = this.#gl;\n const canvas = this.#canvas;\n const el = this.#container;\n if (gl === null || canvas === null || el === null) return;\n this.#dpr = typeof devicePixelRatio === 'number' ? devicePixelRatio : 1;\n const w = Math.max(1, Math.round(el.clientWidth * this.#dpr));\n const h = Math.max(1, Math.round(el.clientHeight * this.#dpr));\n if (canvas.width !== w || canvas.height !== h) {\n canvas.width = w;\n canvas.height = h;\n gl.viewport(0, 0, w, h);\n }\n }\n}\n","import type { Renderer } from './types';\n\nexport type RendererKind = 'css' | 'webgl2';\n\n/**\n * How the consumer chooses a renderer:\n * - `'auto'` (default): prefer the GPU path (WebGL2), else CSS.\n * - a single kind: force it (`'webgl2'` throws if the device has no WebGL2).\n * - an ordered array: try each in turn, first viable wins.\n * - a `Renderer` instance: use it as-is (custom implementation, §6.1/§11).\n */\nexport type RendererOption = 'auto' | RendererKind | RendererKind[] | Renderer;\n\nexport interface Capabilities {\n /** WebGL2 available — the GPU renderer's only backend. */\n gpu: boolean;\n}\n\ntype RendererLoader = () => Promise<Renderer>;\n\n/**\n * Lazy loaders, one per kind. Each is a dynamic `import()` so the bundler\n * code-splits it into its own chunk and a device fetches only the one selected.\n */\nconst LOADERS: Partial<Record<RendererKind, RendererLoader>> = {\n css: async () => new (await import('./cssRenderer')).CssRenderer(),\n webgl2: async () => new (await import('./webglRenderer')).WebglRenderer(),\n};\n\n// The WebGL2 renderer is auto-selected on GPU-capable devices; CSS is the baseline\n// for non-GPU devices and the fallback for unrecoverable GPU failures (§8.4).\nconst WEBGL2_AUTO = true;\n\nfunction isRenderer(option: RendererOption): option is Renderer {\n return (\n typeof option === 'object' &&\n option !== null &&\n !Array.isArray(option) &&\n typeof (option as Renderer).mount === 'function'\n );\n}\n\nfunction autoOrder(caps: Capabilities): RendererKind[] {\n return caps.gpu && WEBGL2_AUTO ? ['webgl2', 'css'] : ['css'];\n}\n\n/** Detect WebGL2 support — the GPU renderer's only backend (no WebGL1, no WebGPU). */\nexport function detectCapabilities(): Capabilities {\n return { gpu: hasWebGl2() };\n}\n\nfunction hasWebGl2(): boolean {\n try {\n return Boolean(document.createElement('canvas').getContext('webgl2'));\n } catch {\n return false;\n }\n}\n\n/**\n * Resolve the `renderer` option to a mounted renderer instance, lazy-loading its\n * chunk. Throws an agent-actionable error if nothing viable can be loaded.\n */\nexport async function selectRenderer(\n option: RendererOption = 'auto',\n caps: Capabilities = detectCapabilities(),\n): Promise<Renderer> {\n if (isRenderer(option)) return option;\n\n const order = Array.isArray(option) ? option : option === 'auto' ? autoOrder(caps) : [option];\n\n for (const kind of order) {\n if (kind === 'webgl2' && !caps.gpu) continue; // skip the GPU path without WebGL2\n const loader = LOADERS[kind];\n if (loader) return loader();\n }\n\n if (order.length === 1 && order[0] === 'webgl2' && !caps.gpu) {\n throw new Error(\n \"renderer: 'webgl2' was forced but this device has no WebGL2. Use 'css' or 'auto'.\",\n );\n }\n throw new Error(\n `renderer: none of [${order.join(', ')}] could be loaded — use 'css' or 'auto'.`,\n );\n}\n","import type { PageContent } from '../renderer/types';\nimport type { DownloadInfo, LoadProgress, OutlineItem, PageRequest, Source } from './types';\n\nexport interface CompositionOptions {\n /** Image URL prepended as a lone front cover (adds a page). */\n frontCover?: string | null;\n /** Image URL appended as a lone back cover (adds a page). */\n backCover?: string | null;\n /** Replace source pages with image URLs, keyed by 0-based source index (negative = from the end). */\n pages?: Record<number, string> | null;\n}\n\n/**\n * Wrap `base` so image covers/replacements sit alongside it, or return `base`\n * unchanged when nothing is requested. Covers ADD pages (front prepended, back\n * appended); `pages` REPLACE existing source pages. `pages` indexes the base's own\n * pages, so it is unaffected by an added cover.\n */\nexport function composeSource(base: Source, options: CompositionOptions): Source {\n const hasFront = typeof options.frontCover === 'string';\n const hasBack = typeof options.backCover === 'string';\n const hasPages = options.pages != null && Object.keys(options.pages).length > 0;\n if (!hasFront && !hasBack && !hasPages) return base;\n return new CompositeSource(base, options);\n}\n\nfunction loadImage(url: string): Promise<ImageBitmap> {\n return fetch(url)\n .then((res) => {\n if (!res.ok) throw new Error(`zine: could not load image \"${url}\" (HTTP ${res.status}).`);\n return res.blob();\n })\n .then((blob) => createImageBitmap(blob));\n}\n\nclass CompositeSource implements Source {\n #base: Source;\n #front: string | null;\n #back: string | null;\n #rawPages: Record<number, string>;\n #resolved = new Map<number, string>(); // base-index → url (negatives resolved)\n #cache = new Map<string, Promise<ImageBitmap>>();\n open?: () => Promise<void>;\n onPageUpdate?: (handler: (index: number) => void) => void;\n onProgress?: (handler: (progress: LoadProgress) => void) => void;\n getText?: (index: number) => Promise<string>;\n getDownload?: () => Promise<DownloadInfo | null>;\n getOutline?: () => Promise<OutlineItem[]>;\n\n constructor(base: Source, options: CompositionOptions) {\n this.#base = base;\n this.#front = typeof options.frontCover === 'string' ? options.frontCover : null;\n this.#back = typeof options.backCover === 'string' ? options.backCover : null;\n this.#rawPages = options.pages ?? {};\n\n if (typeof base.open === 'function') {\n // Async base: page count (and negative indices) known only after open().\n this.open = async (): Promise<void> => {\n await base.open!();\n this.#resolvePages();\n };\n } else {\n this.#resolvePages(); // sync base: count is known now\n }\n\n if (typeof base.onPageUpdate === 'function') {\n // Forward base page updates, shifting to book indices (past the front cover).\n this.onPageUpdate = (handler) => {\n base.onPageUpdate!((src) => handler(src + (this.#front !== null ? 1 : 0)));\n };\n }\n\n if (typeof base.onProgress === 'function') {\n // Download progress is about the whole document, not any one page, so it passes straight\n // through: covers and replacements do not shift it.\n this.onProgress = (handler) => base.onProgress!(handler);\n }\n\n if (typeof base.getText === 'function') {\n // Mirrors get(): covers and replaced pages are images, so they have no text of their own —\n // returning the base page's text there would describe content the reader cannot see.\n this.getText = async (index) => {\n const frontOffset = this.#front !== null ? 1 : 0;\n if (this.#front !== null && index === 0) return '';\n if (this.#back !== null && index === this.pageCount - 1) return '';\n const src = index - frontOffset;\n if (this.#resolved.has(src)) return '';\n return base.getText!(src);\n };\n }\n\n if (typeof base.getDownload === 'function') {\n // Covers and replacements change what is displayed, not the original document, so the\n // download is the base source's file either way.\n this.getDownload = () => base.getDownload!();\n }\n\n if (typeof base.getOutline === 'function') {\n // The outline points at base pages; a front cover pushes every one of them along by one.\n this.getOutline = async () => {\n const items = await base.getOutline!();\n const offset = this.#front !== null ? 1 : 0;\n if (offset === 0) return items;\n const shift = (list: OutlineItem[]): OutlineItem[] =>\n list.map((item) => ({\n ...item,\n page: item.page === null ? null : item.page + offset,\n children: shift(item.children),\n }));\n return shift(items);\n };\n }\n }\n\n get pageCount(): number {\n return this.#base.pageCount + (this.#front !== null ? 1 : 0) + (this.#back !== null ? 1 : 0);\n }\n\n get(index: number, opts?: PageRequest): Promise<PageContent> {\n const frontOffset = this.#front !== null ? 1 : 0;\n // Covers and page overrides are images: they have no detail beyond their own resolution, so\n // the zoom request has nothing to act on and is dropped rather than forwarded.\n if (this.#front !== null && index === 0) return this.#image(this.#front);\n if (this.#back !== null && index === this.pageCount - 1) return this.#image(this.#back);\n const src = index - frontOffset;\n const override = this.#resolved.get(src);\n return override !== undefined ? this.#image(override) : this.#base.get(src, opts);\n }\n\n prefetch(indices: number[]): void {\n const frontOffset = this.#front !== null ? 1 : 0;\n const baseIndices: number[] = [];\n for (const index of indices) {\n if (this.#front !== null && index === 0) {\n void this.#image(this.#front).catch(() => {});\n } else if (this.#back !== null && index === this.pageCount - 1) {\n void this.#image(this.#back).catch(() => {});\n } else {\n const src = index - frontOffset;\n const override = this.#resolved.get(src);\n if (override !== undefined) void this.#image(override).catch(() => {});\n else baseIndices.push(src);\n }\n }\n this.#base.prefetch(baseIndices);\n }\n\n destroy(): void {\n this.#base.destroy();\n for (const promise of this.#cache.values()) {\n void promise.then((bmp) => bmp.close()).catch(() => {});\n }\n this.#cache.clear();\n }\n\n #resolvePages(): void {\n const n = this.#base.pageCount;\n this.#resolved.clear();\n for (const [key, url] of Object.entries(this.#rawPages)) {\n const raw = Number(key);\n const src = raw < 0 ? n + raw : raw;\n if (Number.isInteger(src) && src >= 0 && src < n) this.#resolved.set(src, url);\n }\n }\n\n #image(url: string): Promise<ImageBitmap> {\n let promise = this.#cache.get(url);\n if (promise === undefined) {\n promise = loadImage(url).catch((error: unknown) => {\n this.#cache.delete(url); // let a later get() retry\n throw error;\n });\n this.#cache.set(url, promise);\n }\n return promise;\n }\n}\n","import type { LoadProgress } from '../source/types';\n\n/**\n * The built-in loading overlay: a spinner, a status line, and a download progress bar.\n *\n * This module is the lazy chunk `zine.ts` reaches through a dynamic `import()`, so a book built\n * with `loading: false`, or one whose source never reports progress, never downloads it. The file\n * is named `loading.ts` rather than `index.ts` so the emitted chunk is `loading-<hash>.js`, which\n * is what the size-limit budget watches (the same trick as `controls/controls.ts`).\n *\n * Everything is scoped under `.zine-loading`, and the colours come from custom properties so a\n * consumer can retheme without overriding rules. Each falls back to `light-dark(light, dark)`, which\n * resolves against the used `color-scheme`, so the overlay follows the host page's light or dark\n * theme with no configuration (the same approach as the controls). A consumer who wants a wholly\n * different indicator passes `loading: false` and drives their own UI from the `progress` event and\n * the `ready` promise.\n */\n\nconst STYLE_ID = 'zine-loading-style';\n\n// Wait this long before showing the overlay: a small or cached PDF resolves in a few frames, and\n// flashing a spinner for that is worse than showing nothing.\nconst REVEAL_DELAY_MS = 150;\n\nexport const CSS = `\n.zine-loading {\n position: absolute;\n inset: 0;\n z-index: 3;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 14px;\n /* The overlay shows during download, before the book is measured and the container gets its\n aspect-ratio (and thus its height). A consumer who sizes the container by width alone leaves it\n near-zero-height meanwhile, which would crush the overlay to a strip and clip the spinner.\n A floor keeps it tall enough to lay out; the box snaps to the book's ratio once it paints. */\n min-height: 160px;\n padding: 24px;\n box-sizing: border-box;\n text-align: center;\n font: 500 14px/1.4 system-ui, sans-serif;\n color: var(--zine-loading-fg, light-dark(#3f3f46, #d4d4d8));\n background: var(--zine-loading-bg, light-dark(#fafafa, #0c0c11));\n border-radius: inherit;\n opacity: 0;\n transition: opacity 150ms ease;\n}\n.zine-loading-shown { opacity: 1; }\n.zine-loading-spinner {\n width: 32px;\n height: 32px;\n border: 3px solid var(--zine-loading-track, light-dark(rgba(0, 0, 0, 0.1), rgba(255, 255, 255, 0.14)));\n border-top-color: var(--zine-loading-accent, light-dark(#0284c7, #7dd3fc));\n border-radius: 50%;\n animation: zine-loading-spin 0.8s linear infinite;\n}\n@keyframes zine-loading-spin { to { transform: rotate(360deg); } }\n.zine-loading-text { font-variant-numeric: tabular-nums; }\n.zine-loading-bar {\n width: min(240px, 70%);\n height: 6px;\n background: var(--zine-loading-track, light-dark(rgba(0, 0, 0, 0.1), rgba(255, 255, 255, 0.14)));\n border-radius: 3px;\n overflow: hidden;\n}\n.zine-loading-fill {\n width: 0;\n height: 100%;\n background: var(--zine-loading-accent, light-dark(#0284c7, #7dd3fc));\n border-radius: 3px;\n transition: width 150ms ease-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .zine-loading { transition: none; }\n .zine-loading-spinner { animation-duration: 2s; }\n .zine-loading-fill { transition: none; }\n}\n`;\n\n/** Add the stylesheet to `doc` if it is not already there. */\nfunction ensureStyles(doc: Document): void {\n if (doc.getElementById(STYLE_ID)) return;\n const style = doc.createElement('style');\n style.id = STYLE_ID;\n style.textContent = CSS;\n (doc.head ?? doc.documentElement)?.appendChild(style);\n}\n\n/** Handle the engine uses to drive and tear down the overlay. */\nexport interface LoaderHandle {\n /** Download phase: reflect bytes fetched, showing a percentage bar when a total is known. */\n update(progress: LoadProgress): void;\n /** Downloading is done; the wait is now rasterizing the first spread. */\n preparing(): void;\n /** Remove the overlay and restore anything it changed on the container. */\n destroy(): void;\n}\n\nconst fmtMB = (bytes: number): string => `${(bytes / 1024 / 1024).toFixed(1)} MB`;\n\n/**\n * Build the overlay inside `container` and return a handle to drive it.\n *\n * The overlay is hidden for the first {@link REVEAL_DELAY_MS} so a fast load never flashes it;\n * `destroy()` before then cancels the reveal outright.\n */\nexport function mountLoading(container: HTMLElement): LoaderHandle {\n const doc = container.ownerDocument;\n ensureStyles(doc);\n\n const overlay = doc.createElement('div');\n overlay.className = 'zine-loading';\n overlay.setAttribute('role', 'status');\n overlay.setAttribute('aria-live', 'polite');\n\n const spinner = doc.createElement('div');\n spinner.className = 'zine-loading-spinner';\n spinner.setAttribute('aria-hidden', 'true');\n\n const text = doc.createElement('div');\n text.className = 'zine-loading-text';\n text.textContent = 'Opening document…';\n\n const bar = doc.createElement('div');\n bar.className = 'zine-loading-bar';\n bar.style.display = 'none';\n const fill = doc.createElement('div');\n fill.className = 'zine-loading-fill';\n bar.appendChild(fill);\n\n overlay.append(spinner, text, bar);\n container.appendChild(overlay);\n\n // The overlay is absolutely positioned; a statically-positioned container would let it escape.\n // Give the container a positioning context and restore it on teardown. getComputedStyle is\n // absent under some test DOMs, so guard it.\n let restorePosition: string | null = null;\n const computed =\n typeof globalThis.getComputedStyle === 'function' ? globalThis.getComputedStyle(container) : null;\n if (!computed || computed.position === 'static' || computed.position === '') {\n restorePosition = container.style.position;\n container.style.position = 'relative';\n }\n\n let revealed = false;\n const revealTimer = setTimeout(() => {\n revealed = true;\n overlay.classList.add('zine-loading-shown');\n }, REVEAL_DELAY_MS);\n\n return {\n update(progress: LoadProgress): void {\n if (progress.total > 0) {\n const pct = Math.min(100, Math.round((progress.loaded / progress.total) * 100));\n text.textContent = `Downloading document… ${pct}%`;\n bar.style.display = '';\n fill.style.width = `${pct}%`;\n } else {\n text.textContent = `Downloading document… ${fmtMB(progress.loaded)}`;\n bar.style.display = 'none';\n }\n },\n preparing(): void {\n text.textContent = 'Preparing pages…';\n bar.style.display = 'none';\n },\n destroy(): void {\n if (!revealed) clearTimeout(revealTimer);\n overlay.remove();\n if (restorePosition !== null) container.style.position = restorePosition;\n },\n };\n}\n","/**\n * Toolbar glyphs, inlined so the bundle carries no icon-font or sprite dependency.\n *\n * Paths are from Lucide (https://lucide.dev), ISC licensed:\n * Copyright (c) for portions of Lucide are held by Cole Bemis 2013-2022 as part of Feather\n * (MIT). All other copyright (c) for Lucide are held by Lucide Contributors 2022.\n *\n * Each value is the inner markup of a 24x24 `viewBox` drawn with `stroke=\"currentColor\"`,\n * `fill=\"none\"` and round caps/joins — {@link createIcon} supplies those, so the strings stay to\n * the geometry.\n */\n// Not annotated `Record<string, string>`: that would make every ICONS.foo possibly-undefined\n// under noUncheckedIndexedAccess, even for names written right here. `satisfies` keeps the\n// literal's exact keys while still checking the values are markup strings.\nexport const ICONS = {\n /** chevron-left */\n prev: '<path d=\"m15 18-6-6 6-6\"/>',\n /** chevron-right */\n next: '<path d=\"m9 18 6-6-6-6\"/>',\n /** chevrons-left */\n first: '<path d=\"m11 17-5-5 5-5\"/><path d=\"m18 17-5-5 5-5\"/>',\n /** chevrons-right */\n last: '<path d=\"m6 17 5-5-5-5\"/><path d=\"m13 17 5-5-5-5\"/>',\n /** circle-plus: zoom in. A plus, not a magnifier, so it never reads as a third \"search\". */\n zoomIn: '<circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M8 12h8\"/><path d=\"M12 8v8\"/>',\n /** circle-minus: zoom out, paired with zoomIn. */\n zoomOut: '<circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M8 12h8\"/>',\n /** search */\n search: '<circle cx=\"11\" cy=\"11\" r=\"8\"/><path d=\"m21 21-4.3-4.3\"/>',\n /** share-2 */\n share:\n '<circle cx=\"18\" cy=\"5\" r=\"3\"/><circle cx=\"6\" cy=\"12\" r=\"3\"/><circle cx=\"18\" cy=\"19\" r=\"3\"/><path d=\"m8.6 13.5 6.8 4\"/><path d=\"m15.4 6.5-6.8 4\"/>',\n /** ellipsis-vertical */\n menu: '<circle cx=\"12\" cy=\"12\" r=\"1\"/><circle cx=\"12\" cy=\"5\" r=\"1\"/><circle cx=\"12\" cy=\"19\" r=\"1\"/>',\n /** maximize */\n fullscreen:\n '<path d=\"M8 3H5a2 2 0 0 0-2 2v3\"/><path d=\"M21 8V5a2 2 0 0 0-2-2h-3\"/><path d=\"M3 16v3a2 2 0 0 0 2 2h3\"/><path d=\"M16 21h3a2 2 0 0 0 2-2v-3\"/>',\n /** minimize */\n exitFullscreen:\n '<path d=\"M8 3v3a2 2 0 0 1-2 2H3\"/><path d=\"M21 8h-3a2 2 0 0 1-2-2V3\"/><path d=\"M3 16h3a2 2 0 0 1 2 2v3\"/><path d=\"M16 21v-3a2 2 0 0 1 2-2h3\"/>',\n /** gallery-vertical-end */\n thumbnails:\n '<path d=\"M7 2h10\"/><path d=\"M5 6h14\"/><rect width=\"18\" height=\"12\" x=\"3\" y=\"10\" rx=\"2\"/>',\n /** list-tree */\n outline:\n '<path d=\"M21 12h-8\"/><path d=\"M21 6h-8\"/><path d=\"M21 18h-8\"/><path d=\"M3 6v4c0 1.1.9 2 2 2h3\"/><path d=\"M3 10v6c0 1.1.9 2 2 2h3\"/>',\n /** book-open: two facing pages */\n twoPages:\n '<path d=\"M12 7v14\"/><path d=\"M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z\"/>',\n /** file: a single page */\n onePage: '<path d=\"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z\"/><path d=\"M14 2v5h6\"/>',\n /** printer */\n print:\n '<path d=\"M6 9V2h12v7\"/><path d=\"M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2\"/><rect width=\"12\" height=\"8\" x=\"6\" y=\"14\" rx=\"1\"/>',\n /** download */\n download: '<path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\"/><path d=\"M7 10l5 5 5-5\"/><path d=\"M12 15V3\"/>',\n /** x */\n close: '<path d=\"M18 6 6 18\"/><path d=\"m6 6 12 12\"/>',\n} satisfies Record<string, string>;\n\n/** Build an `<svg>` for one of the {@link ICONS}, or null if the name is unknown. */\nexport function createIcon(doc: Document, markup: string): SVGSVGElement {\n const svg = doc.createElementNS('http://www.w3.org/2000/svg', 'svg');\n svg.setAttribute('viewBox', '0 0 24 24');\n svg.setAttribute('fill', 'none');\n svg.setAttribute('stroke', 'currentColor');\n // Slightly under Lucide's default 2: at the toolbar's icon size a full-weight stroke reads\n // heavy and closes up the tighter shapes.\n svg.setAttribute('stroke-width', '1.75');\n svg.setAttribute('stroke-linecap', 'round');\n svg.setAttribute('stroke-linejoin', 'round');\n svg.setAttribute('aria-hidden', 'true');\n svg.setAttribute('focusable', 'false');\n // Icon markup is library-authored, never consumer input.\n svg.innerHTML = markup;\n return svg;\n}\n","import type { ControlDef, ControlItem } from './types';\n\n/**\n * The control registry.\n *\n * Deliberately just the mechanism — no icons, no built-in definitions — because `defineControl`\n * has to be callable before the toolbar chunk loads, so this module lands in the base bundle.\n * The built-ins live in `./builtins`, which the lazy chunk pulls in along with the icon set.\n */\nconst registry = new Map<string, ControlDef>();\n\n/**\n * Add a control, or replace one by reusing its id. Returns the definition so it can be dropped\n * straight into a layout.\n *\n * ```js\n * defineControl({ id: 'print', title: 'Print', icon: '<path d=\"…\"/>', action: () => print() });\n * new Zine(el, { source, controls: { items: ['prev', 'next', 'print'] } });\n * ```\n */\nexport function defineControl(def: ControlDef): ControlDef {\n registry.set(def.id, def);\n return def;\n}\n\n/** Look up a registered control. */\nexport function getControl(id: string): ControlDef | undefined {\n return registry.get(id);\n}\n\n/**\n * Turn a layout entry into a concrete definition. A bare string names a registered control; an\n * object with only some fields is layered over the registered one, so `{ id: 'next', title: 'Go\n * on' }` relabels the built-in without reimplementing it.\n */\nexport function resolveControl(item: ControlItem): ControlDef {\n if (typeof item === 'string') {\n const found = registry.get(item);\n if (!found) {\n throw new Error(\n `controls: unknown control '${item}'. Register it with defineControl() or pass a definition object.`,\n );\n }\n return found;\n }\n const base = registry.get(item.id);\n if (!base) return item as ControlDef;\n return { ...base, ...item };\n}\n\n/** The toolbar shown when `items` is not given. */\nexport const DEFAULT_ITEMS: readonly ControlItem[] = [\n 'prev',\n 'pageInput',\n 'next',\n '|',\n 'zoomOut',\n 'zoomIn',\n 'search',\n 'share',\n 'menu',\n 'fullscreen',\n];\n","/**\n * Where a reader can send a link, and how each service wants it.\n *\n * These are plain share-intent URLs — no SDKs, no third-party scripts, nothing that could track\n * the reader before they choose to share. Each brand mark is drawn from its own official glyph;\n * they are filled shapes rather than the stroked outlines used elsewhere in the toolbar, so they\n * carry `fill=\"currentColor\"` and no stroke.\n */\nexport interface Social {\n id: string;\n label: string;\n /** Inner SVG markup for a 24x24 viewBox, filled rather than stroked. */\n icon: string;\n href(url: string, title: string): string;\n}\n\nconst e = encodeURIComponent;\n\nexport const SOCIALS: readonly Social[] = [\n {\n id: 'facebook',\n label: 'Facebook',\n icon: '<path fill=\"currentColor\" stroke=\"none\" d=\"M22 12a10 10 0 1 0-11.56 9.88v-6.99H7.9V12h2.54V9.8c0-2.5 1.49-3.89 3.77-3.89 1.1 0 2.24.2 2.24.2v2.46h-1.26c-1.24 0-1.63.77-1.63 1.56V12h2.78l-.45 2.89h-2.33v6.99A10 10 0 0 0 22 12Z\"/>',\n href: (url) => `https://www.facebook.com/sharer/sharer.php?u=${e(url)}`,\n },\n {\n id: 'x',\n label: 'X (Twitter)',\n icon: '<path fill=\"currentColor\" stroke=\"none\" d=\"M17.53 3h3.06l-6.69 7.64L21.75 21h-6.16l-4.82-6.3L5.24 21H2.18l7.15-8.17L2.25 3h6.32l4.36 5.77L17.53 3Zm-1.07 16.17h1.69L7.62 4.73H5.8l10.66 14.44Z\"/>',\n href: (url, title) => `https://twitter.com/intent/tweet?url=${e(url)}&text=${e(title)}`,\n },\n {\n id: 'linkedin',\n label: 'LinkedIn',\n icon: '<path fill=\"currentColor\" stroke=\"none\" d=\"M20.45 20.45h-3.56v-5.57c0-1.33-.02-3.04-1.85-3.04-1.85 0-2.14 1.45-2.14 2.94v5.67H9.35V9h3.41v1.56h.05a3.74 3.74 0 0 1 3.37-1.85c3.6 0 4.27 2.37 4.27 5.46v6.28ZM5.34 7.43a2.07 2.07 0 1 1 0-4.13 2.07 2.07 0 0 1 0 4.13Zm1.78 13.02H3.55V9h3.57v11.45ZM22.22 0H1.77C.79 0 0 .77 0 1.72v20.56C0 23.23.79 24 1.77 24h20.45c.98 0 1.78-.77 1.78-1.72V1.72C24 .77 23.2 0 22.22 0Z\"/>',\n href: (url) => `https://www.linkedin.com/sharing/share-offsite/?url=${e(url)}`,\n },\n {\n id: 'whatsapp',\n label: 'WhatsApp',\n icon: '<path fill=\"currentColor\" stroke=\"none\" d=\"M12.04 2a9.9 9.9 0 0 0-8.5 14.9L2 22l5.25-1.38A9.9 9.9 0 1 0 12.04 2Zm5.8 14.06c-.25.7-1.44 1.33-1.99 1.38-.53.05-1.02.24-3.44-.72-2.9-1.14-4.73-4.1-4.87-4.29-.14-.19-1.16-1.54-1.16-2.94s.73-2.08 1-2.37c.26-.28.57-.35.76-.35h.55c.17 0 .42-.07.65.5.24.58.8 2 .87 2.14.07.14.12.31.02.5-.09.19-.14.3-.28.47l-.42.48c-.14.14-.28.29-.12.57.16.28.7 1.16 1.51 1.88 1.04.93 1.91 1.21 2.19 1.35.28.15.44.12.6-.07.17-.19.7-.81.88-1.09.19-.28.37-.23.63-.14.25.1 1.63.77 1.9.91.29.14.48.21.55.33.07.11.07.67-.18 1.37Z\"/>',\n href: (url, title) => `https://api.whatsapp.com/send?text=${e(`${title} ${url}`)}`,\n },\n {\n id: 'pinterest',\n label: 'Pinterest',\n icon: '<path fill=\"currentColor\" stroke=\"none\" d=\"M12 2a10 10 0 0 0-3.65 19.31c-.09-.78-.17-1.98.03-2.83.19-.78 1.2-4.98 1.2-4.98s-.3-.61-.3-1.52c0-1.42.82-2.48 1.85-2.48.87 0 1.3.66 1.3 1.44 0 .88-.56 2.2-.85 3.42-.24 1.02.51 1.86 1.52 1.86 1.83 0 3.23-1.93 3.23-4.7 0-2.46-1.77-4.18-4.29-4.18-2.92 0-4.64 2.19-4.64 4.46 0 .88.34 1.83.76 2.35a.3.3 0 0 1 .07.29l-.28 1.16c-.05.19-.15.23-.34.14-1.28-.6-2.08-2.47-2.08-3.98 0-3.24 2.35-6.21 6.79-6.21 3.56 0 6.33 2.54 6.33 5.93 0 3.54-2.23 6.39-5.32 6.39-1.04 0-2.02-.54-2.35-1.18l-.64 2.44c-.23.89-.86 2.01-1.28 2.69A10 10 0 1 0 12 2Z\"/>',\n href: (url, title) => `https://pinterest.com/pin/create/button/?url=${e(url)}&description=${e(title)}`,\n },\n {\n id: 'email',\n label: 'Email',\n icon: '<path fill=\"currentColor\" stroke=\"none\" d=\"M20 4H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2Zm0 4.24-7.47 4.67a1 1 0 0 1-1.06 0L4 8.24V6.4l8 5 8-5v1.84Z\"/>',\n href: (url, title) => `mailto:?subject=${e(title)}&body=${e(url)}`,\n },\n];\n","/**\n * A small QR encoder, enough to turn a URL into a scannable grid.\n *\n * Written rather than pulled in so the package keeps its \"no dependencies\" promise, and so no\n * reader's document URL is handed to a third-party image service to be logged. Deliberately\n * narrow: byte mode, error-correction level M, versions 1-10 (up to 271 characters at level M),\n * which covers any sane page URL. Longer input throws rather than silently producing a code that\n * will not scan.\n */\n\n/** Data codewords available per version at error-correction level M, versions 1-10. */\nconst DATA_CODEWORDS = [16, 28, 44, 64, 86, 108, 124, 154, 182, 216];\n/** Error-correction codewords per block, level M, versions 1-10. */\nconst EC_PER_BLOCK = [10, 16, 26, 18, 24, 16, 18, 22, 22, 26];\n/** How many EC blocks the data is split into, level M, versions 1-10. */\nconst BLOCKS = [1, 1, 1, 2, 2, 4, 4, 4, 5, 5];\n/** Where alignment patterns sit, per version (empty for version 1). */\nconst ALIGNMENT = [\n [],\n [6, 18],\n [6, 22],\n [6, 26],\n [6, 30],\n [6, 34],\n [6, 22, 38],\n [6, 24, 42],\n [6, 26, 46],\n [6, 28, 50],\n];\n\n// Galois field tables for GF(256), used by the Reed-Solomon error correction.\nconst EXP = new Uint8Array(512);\nconst LOG = new Uint8Array(256);\nfor (let i = 0, x = 1; i < 255; i++) {\n EXP[i] = x;\n LOG[x] = i;\n x <<= 1;\n if (x & 0x100) x ^= 0x11d; // the QR field's primitive polynomial\n}\nfor (let i = 255; i < 512; i++) EXP[i] = EXP[i - 255]!;\n\nconst mul = (a: number, b: number): number =>\n a === 0 || b === 0 ? 0 : EXP[(LOG[a]! + LOG[b]!) % 255]!;\n\n/** The generator polynomial for `degree` error-correction codewords. */\nfunction generator(degree: number): Uint8Array {\n let poly = new Uint8Array([1]);\n for (let i = 0; i < degree; i++) {\n const next = new Uint8Array(poly.length + 1);\n for (let j = 0; j < poly.length; j++) {\n // Written as explicit reads: a compound assignment on an indexed element is what\n // noUncheckedIndexedAccess objects to, since the target is possibly-undefined.\n next[j] = next[j]! ^ poly[j]!;\n next[j + 1] = next[j + 1]! ^ mul(poly[j]!, EXP[i]!);\n }\n poly = next;\n }\n return poly;\n}\n\n/** Reed-Solomon remainder: the error-correction codewords for one block. */\nfunction ecBlock(data: Uint8Array, count: number): Uint8Array {\n const gen = generator(count);\n const out = new Uint8Array(count);\n for (const byte of data) {\n const factor = byte ^ out[0]!;\n out.copyWithin(0, 1);\n out[count - 1] = 0;\n if (factor !== 0) {\n for (let i = 0; i < count; i++) out[i] = out[i]! ^ mul(gen[i + 1]!, factor);\n }\n }\n return out;\n}\n\n/** The 15-bit format string for level M and a mask, with its BCH check bits. */\nfunction formatBits(mask: number): number {\n const data = (0b00 << 3) | mask; // 00 = level M\n let rem = data;\n for (let i = 0; i < 10; i++) rem = (rem << 1) ^ ((rem >>> 9) * 0x537);\n return ((data << 10) | rem) ^ 0x5412;\n}\n\ntype Grid = { size: number; modules: Uint8Array; reserved: Uint8Array };\n\nconst at = (g: Grid, x: number, y: number): number => g.modules[y * g.size + x]!;\nconst set = (g: Grid, x: number, y: number, dark: boolean, reserve = true): void => {\n g.modules[y * g.size + x] = dark ? 1 : 0;\n if (reserve) g.reserved[y * g.size + x] = 1;\n};\n\nfunction placeFinder(g: Grid, cx: number, cy: number): void {\n for (let dy = -1; dy <= 7; dy++) {\n for (let dx = -1; dx <= 7; dx++) {\n const x = cx + dx;\n const y = cy + dy;\n if (x < 0 || y < 0 || x >= g.size || y >= g.size) continue;\n const d = Math.max(Math.abs(dx - 3), Math.abs(dy - 3));\n set(g, x, y, d !== 2 && d <= 3);\n }\n }\n}\n\n/** Encode `text` as a QR matrix: a square of 0/1 by row. */\nexport function encodeQr(text: string): { size: number; modules: Uint8Array } {\n const bytes = new TextEncoder().encode(text);\n const version = DATA_CODEWORDS.findIndex((cap) => bytes.length + 2 <= cap) + 1;\n if (version === 0) {\n throw new Error(`qr: ${bytes.length} bytes is too long for this encoder (max 214).`);\n }\n\n // --- bit stream: mode, length, payload, terminator, padding ---\n const capacity = DATA_CODEWORDS[version - 1]!;\n const bits: number[] = [];\n const push = (value: number, width: number): void => {\n for (let i = width - 1; i >= 0; i--) bits.push((value >>> i) & 1);\n };\n push(0b0100, 4); // byte mode\n push(bytes.length, version < 10 ? 8 : 16);\n for (const byte of bytes) push(byte, 8);\n push(0, Math.min(4, capacity * 8 - bits.length)); // terminator\n while (bits.length % 8) bits.push(0);\n const data = new Uint8Array(capacity);\n for (let i = 0; i < bits.length; i += 8) {\n let byte = 0;\n for (let b = 0; b < 8; b++) byte = (byte << 1) | bits[i + b]!;\n data[i / 8] = byte;\n }\n for (let i = bits.length / 8, pad = 0; i < capacity; i++, pad++) {\n data[i] = pad % 2 === 0 ? 0xec : 0x11; // the spec's alternating pad bytes\n }\n\n // --- split into blocks, compute EC, interleave ---\n const blockCount = BLOCKS[version - 1]!;\n const ecCount = EC_PER_BLOCK[version - 1]!;\n const shortLen = Math.floor(capacity / blockCount);\n const longCount = capacity % blockCount;\n const dataBlocks: Uint8Array[] = [];\n const ecBlocks: Uint8Array[] = [];\n for (let i = 0, offset = 0; i < blockCount; i++) {\n const len = shortLen + (i >= blockCount - longCount ? 1 : 0);\n const block = data.subarray(offset, offset + len);\n offset += len;\n dataBlocks.push(block);\n ecBlocks.push(ecBlock(block, ecCount));\n }\n const codewords: number[] = [];\n for (let i = 0; i < shortLen + 1; i++) {\n for (const block of dataBlocks) if (i < block.length) codewords.push(block[i]!);\n }\n for (let i = 0; i < ecCount; i++) {\n for (const block of ecBlocks) codewords.push(block[i]!);\n }\n\n // --- lay out the matrix ---\n const size = version * 4 + 17;\n const g: Grid = {\n size,\n modules: new Uint8Array(size * size),\n reserved: new Uint8Array(size * size),\n };\n placeFinder(g, 0, 0);\n placeFinder(g, size - 7, 0);\n placeFinder(g, 0, size - 7);\n for (let i = 8; i < size - 8; i++) {\n const dark = i % 2 === 0; // timing patterns\n set(g, i, 6, dark);\n set(g, 6, i, dark);\n }\n for (const ax of ALIGNMENT[version - 1]!) {\n for (const ay of ALIGNMENT[version - 1]!) {\n // Alignment patterns skip the corners already held by finders.\n if ((ax < 9 && ay < 9) || (ax < 9 && ay > size - 10) || (ax > size - 10 && ay < 9)) continue;\n for (let dy = -2; dy <= 2; dy++) {\n for (let dx = -2; dx <= 2; dx++) {\n set(g, ax + dx, ay + dy, Math.max(Math.abs(dx), Math.abs(dy)) !== 1);\n }\n }\n }\n }\n set(g, 8, size - 8, true); // the always-dark module\n // Reserve the format areas so data does not land in them.\n for (let i = 0; i < 9; i++) {\n if (i !== 6) set(g, i, 8, false), set(g, 8, i, false);\n }\n for (let i = 0; i < 8; i++) set(g, size - 1 - i, 8, false), set(g, 8, size - 1 - i, false);\n set(g, 8, size - 8, true);\n\n // --- weave the codewords in, masking as we go (mask 0: (x + y) % 2) ---\n let bit = 0;\n const total = codewords.length * 8;\n for (let right = size - 1; right >= 1; right -= 2) {\n if (right === 6) right = 5; // the vertical timing column is skipped\n for (let step = 0; step < size; step++) {\n for (let col = 0; col < 2; col++) {\n const x = right - col;\n const upward = ((right + 1) & 2) === 0;\n const y = upward ? size - 1 - step : step;\n if (g.reserved[y * size + x]) continue;\n let dark = false;\n if (bit < total) {\n dark = ((codewords[bit >>> 3]! >>> (7 - (bit & 7))) & 1) === 1;\n bit++;\n }\n if ((x + y) % 2 === 0) dark = !dark; // mask 0\n set(g, x, y, dark, false);\n }\n }\n }\n\n // --- format information, now that the mask is known ---\n const format = formatBits(0);\n for (let i = 0; i < 15; i++) {\n const dark = ((format >>> i) & 1) === 1;\n // Around the top-left finder...\n const a = i < 6 ? i : i < 8 ? i + 1 : 8;\n const b = i < 8 ? 8 : i < 9 ? 7 : 14 - i;\n set(g, a, 8, dark);\n set(g, 8, b, dark);\n // ...and the copy split between the other two.\n if (i < 8) set(g, size - 1 - i, 8, dark);\n else set(g, 8, size - 15 + i, dark);\n }\n set(g, 8, size - 8, true);\n\n return { size, modules: g.modules };\n}\n\n/** Render a QR matrix as an SVG string, sized to `px`. */\nexport function qrSvg(text: string, px: number): string {\n const { size, modules } = encodeQr(text);\n let path = '';\n for (let y = 0; y < size; y++) {\n for (let x = 0; x < size; x++) {\n if (modules[y * size + x]) path += `M${x} ${y}h1v1h-1z`;\n }\n }\n const quiet = 2; // the spec asks for 4; 2 scans fine and wastes less space on screen\n const box = size + quiet * 2;\n return `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${px}\" height=\"${px}\" viewBox=\"0 0 ${box} ${box}\" shape-rendering=\"crispEdges\"><rect width=\"${box}\" height=\"${box}\" fill=\"#fff\"/><g transform=\"translate(${quiet} ${quiet})\" fill=\"#000\">${`<path d=\"${path}\"/>`}</g></svg>`;\n}\n","/**\n * The toolbar's stylesheet, injected once per document.\n *\n * The rest of the library styles elements inline, but a toolbar needs `:hover`, `:focus-visible`\n * and `:disabled` — states inline styles cannot express — so this is the one place a `<style>`\n * element is warranted. Everything is scoped under `.zine-controls`.\n *\n * Colours come from custom properties so a consumer can retheme without overriding rules. Left\n * unset, each falls back to `light-dark(light, dark)`, which resolves against the element's used\n * `color-scheme`: the toolbar follows the host page's scheme with no configuration, and a host\n * with a theme toggle only has to set `color-scheme` on its root for the controls to match. The\n * `colorScheme` control option can force `light` or `dark` regardless of the page, and a set\n * `--zine-controls-*` token still wins over both.\n */\nconst STYLE_ID = 'zine-controls-style';\n\nexport const CSS = `\n/* Docked mode wraps the book so the bar can sit beside it without shrinking the container,\n whose measured box the renderer and hit-testing both depend on.\n The wrapper deliberately does not stretch or grow its children: the container keeps whatever\n width, aspect-ratio and auto-margins the consumer's own CSS gave it. */\n.zine-controls-wrap { display: flex; }\n/* Stacked: children keep their own width (so a percentage or auto-margin still resolves against\n the wrapper) and take only the height they ask for. */\n.zine-controls-wrap-top,\n.zine-controls-wrap-bottom { flex-direction: column; align-items: stretch; }\n/* Side by side: the bar is only as wide as its buttons, and both are vertically centred. */\n.zine-controls-wrap-left,\n.zine-controls-wrap-right { flex-direction: row; align-items: center; }\n/* min-height:0 matters as much as min-width here. A flex item's automatic minimum size floors it\n at its content, so once the canvas has grown for a taller layout the book cannot shrink back —\n its aspect-ratio is restored but ignored, leaving the extra height behind. */\n.zine-controls-wrap > * { flex: 0 0 auto; min-width: 0; min-height: 0; }\n\n.zine-controls {\n position: relative;\n display: flex;\n gap: 4px;\n padding: 5px;\n z-index: 2;\n box-sizing: border-box;\n font: 500 12px/1.2 system-ui, sans-serif;\n color: var(--zine-controls-fg, light-dark(#18181b, #f4f4f5));\n touch-action: auto;\n -webkit-user-select: none;\n user-select: none;\n}\n.zine-controls-docked { flex: 0 0 auto; justify-content: center; align-items: center; }\n.zine-controls-floating {\n position: absolute;\n /* Transparent to the book except on the buttons themselves, so gaps stay draggable. */\n pointer-events: none;\n}\n.zine-controls-bar {\n display: flex;\n align-items: center;\n gap: 1px;\n padding: 3px;\n border-radius: 8px;\n pointer-events: auto;\n background: var(--zine-controls-bg, light-dark(rgba(255, 255, 255, 0.94), rgba(24, 24, 27, 0.82)));\n backdrop-filter: blur(8px);\n}\n/* Floating: lifted off the page it covers. Docked: flat, with an outline so it reads as a\n control strip rather than a shadow hanging in empty space. */\n.zine-controls-floating .zine-controls-bar { box-shadow: 0 2px 12px rgba(0, 0, 0, 0.28); }\n.zine-controls-docked .zine-controls-bar {\n border: 1px solid var(--zine-controls-hover, light-dark(rgba(0, 0, 0, 0.08), rgba(255, 255, 255, 0.14)));\n}\n.zine-controls-floating.zine-controls-bottom { inset: auto 0 0 0; justify-content: center; }\n.zine-controls-floating.zine-controls-top { inset: 0 0 auto 0; justify-content: center; }\n.zine-controls-floating.zine-controls-left { inset: 0 auto 0 0; align-items: center; }\n.zine-controls-floating.zine-controls-right { inset: 0 0 0 auto; align-items: center; }\n/* A bar on a vertical edge stacks its buttons, docked or floating. */\n.zine-controls-left .zine-controls-bar,\n.zine-controls-right .zine-controls-bar { flex-direction: column; }\n\n.zine-controls-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 26px;\n height: 26px;\n padding: 0;\n border: 0;\n border-radius: 6px;\n background: transparent;\n color: inherit;\n cursor: pointer;\n font: inherit;\n}\n.zine-controls-btn svg { width: 15px; height: 15px; }\n.zine-controls-btn:hover:not(:disabled) { background: var(--zine-controls-hover, light-dark(rgba(0,0,0,0.08), rgba(255,255,255,0.14))); }\n.zine-controls-btn:focus-visible {\n outline: 2px solid var(--zine-controls-accent, light-dark(#0284c7, #7dd3fc));\n outline-offset: 1px;\n}\n.zine-controls-btn:disabled { opacity: 0.38; cursor: default; }\n/* A button mid-slow-action (fetching a file to save or print): a spinner, kept full-strength\n rather than dimmed so it reads as \"working\", not \"disabled\". */\n.zine-controls-btn.zine-controls-busy { opacity: 1; cursor: default; position: relative; }\n.zine-controls-btn.zine-controls-busy::after {\n content: '';\n box-sizing: border-box;\n width: 14px;\n height: 14px;\n border: 2px solid currentColor;\n border-top-color: transparent;\n border-radius: 50%;\n opacity: 0.7;\n animation: zine-controls-spin 0.7s linear infinite;\n}\n/* Icon-only bar button: hide the glyph and center the spinner over its place. */\n.zine-controls-btn.zine-controls-busy > svg { visibility: hidden; }\n.zine-controls-btn.zine-controls-busy::after {\n position: absolute;\n top: 50%;\n left: 50%;\n margin: -7px 0 0 -7px;\n}\n/* Menu item (glyph + label): let the glyph and label stand, and trail the spinner after them. */\n.zine-controls-menu .zine-controls-btn.zine-controls-busy > svg { visibility: visible; }\n.zine-controls-menu .zine-controls-btn.zine-controls-busy::after { position: static; }\n@keyframes zine-controls-spin { to { transform: rotate(360deg); } }\n@media (prefers-reduced-motion: reduce) {\n .zine-controls-btn.zine-controls-busy::after { animation-duration: 1.6s; }\n}\n/* A custom widget cannot always be disabled itself, so it is marked instead and reads the same. */\n.zine-controls-off { opacity: 0.38; }\n.zine-controls-off[aria-disabled='true'] { pointer-events: none; }\n.zine-controls-btn[aria-pressed=\"true\"],\n.zine-controls-btn[aria-expanded=\"true\"] { background: var(--zine-controls-hover, light-dark(rgba(0,0,0,0.08), rgba(255,255,255,0.14))); }\n\n.zine-controls-sep {\n width: 1px;\n align-self: stretch;\n margin: 3px 3px;\n background: currentColor;\n opacity: 0.22;\n}\n.zine-controls-left .zine-controls-sep,\n.zine-controls-right .zine-controls-sep { width: auto; height: 1px; margin: 3px 3px; }\n\n.zine-controls-page {\n display: inline-flex;\n align-items: center;\n gap: 3px;\n padding: 0 3px;\n pointer-events: auto;\n white-space: nowrap;\n}\n.zine-controls-page input {\n width: 2.2em;\n padding: 2px 3px;\n border: 1px solid var(--zine-controls-hover, light-dark(rgba(0,0,0,0.14), rgba(255,255,255,0.2)));\n border-radius: 4px;\n background: light-dark(rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.25));\n color: inherit;\n font: inherit;\n text-align: center;\n -moz-appearance: textfield;\n}\n.zine-controls-page input::-webkit-outer-spin-button,\n.zine-controls-page input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }\n.zine-controls-page input:focus-visible {\n outline: 2px solid var(--zine-controls-accent, light-dark(#0284c7, #7dd3fc));\n outline-offset: 0;\n}\n\n.zine-controls-menu {\n position: absolute;\n display: flex;\n flex-direction: column;\n gap: 1px;\n min-width: 152px;\n padding: 4px;\n border-radius: 8px;\n pointer-events: auto;\n background: var(--zine-controls-bg, light-dark(rgba(255, 255, 255, 0.94), rgba(24, 24, 27, 0.94)));\n box-shadow: 0 6px 22px rgba(0, 0, 0, 0.36);\n backdrop-filter: blur(8px);\n}\n.zine-controls-menu .zine-controls-btn {\n width: 100%;\n height: auto;\n gap: 8px;\n padding: 6px 8px;\n justify-content: flex-start;\n}\n\n/* The right-click menu opens in a fixed, viewport-spanning layer so the menu's absolute left/top\n are plain client coordinates — it lands under the cursor without the container having to be a\n positioning context. The layer itself is click-through; only the menu inside it takes events. */\n.zine-context-layer {\n position: fixed;\n inset: 0;\n z-index: 5;\n pointer-events: none;\n font: 500 12px/1.2 system-ui, sans-serif;\n color: var(--zine-controls-fg, light-dark(#18181b, #f4f4f5));\n -webkit-user-select: none;\n user-select: none;\n}\n\n/* Search shares the rail: a query field pinned at the top over a scrolling list of hits. */\n.zine-search { gap: 0; overflow: hidden; }\n.zine-search-input {\n flex: 0 0 auto;\n width: 100%;\n padding: 6px 8px;\n border: 1px solid var(--zine-controls-hover, light-dark(rgba(0,0,0,0.14), rgba(255,255,255,0.2)));\n border-radius: 6px;\n background: light-dark(rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.25));\n color: inherit;\n font: inherit;\n box-sizing: border-box;\n}\n/* Replace the browser's default (white) focus ring with the accent ring the other controls use. */\n.zine-search-input:focus-visible {\n outline: none;\n border-color: var(--zine-controls-accent, light-dark(#0284c7, #7dd3fc));\n box-shadow: 0 0 0 1px var(--zine-controls-accent, light-dark(#0284c7, #7dd3fc));\n}\n.zine-search-hits {\n flex: 1 1 auto;\n overflow-y: auto;\n /* Chain overscroll up to the page: once the list is at its end (or too short to scroll at all),\n a further wheel scrolls the page behind, the same as scrolling over the book itself does. */\n overscroll-behavior: auto;\n margin-top: 5px;\n scrollbar-width: thin;\n}\n.zine-search-hit {\n display: block;\n width: 100%;\n padding: 6px 8px;\n border: 0;\n border-radius: 5px;\n background: transparent;\n color: inherit;\n font: inherit;\n text-align: left;\n cursor: pointer;\n box-sizing: border-box;\n}\n.zine-search-hit:hover { background: var(--zine-controls-hover, light-dark(rgba(0,0,0,0.08), rgba(255,255,255,0.14))); }\n.zine-search-hit:focus-visible {\n outline: 2px solid var(--zine-controls-accent, light-dark(#0284c7, #7dd3fc));\n outline-offset: -2px;\n}\n.zine-search-page { display: block; font-size: 0.9em; opacity: 0.85; }\n.zine-search-hit small {\n display: block;\n margin-top: 2px;\n opacity: 0.62;\n line-height: 1.35;\n overflow-wrap: anywhere;\n}\n\n/* Page-turn arrows flanking the book. Like the docked toolbar and the side panels, they sit\n outside the container so they never shrink the book or intercept its gestures. */\n.zine-arrows-wrap {\n display: flex;\n align-items: center;\n gap: 4px;\n min-width: 0;\n}\n.zine-arrows-wrap > *:not(.zine-arrow) { flex: 1 1 auto; min-width: 0; min-height: 0; }\n.zine-arrow {\n flex: 0 0 auto;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 44px;\n height: 44px;\n padding: 0;\n border: 0;\n background: none;\n /* Light on a light page, dark on a dark one. The shadow below carries the contrast. */\n color: var(--zine-controls-fg, light-dark(#f4f4f5, #18181b));\n cursor: pointer;\n -webkit-user-select: none;\n user-select: none;\n /* No grey tap box on touch; the :active tint below is the press feedback instead. */\n -webkit-tap-highlight-color: transparent;\n}\n.zine-arrow svg {\n width: 36px;\n height: 36px;\n stroke-width: 2.25;\n /* No backing disc, so a subtle offset shadow keeps the chevron from disappearing into a\n matching background. It contrasts the fill: a light chevron (light mode) casts a soft dark\n shadow, a dark chevron (dark mode) a soft light one, offset rather than a glowing halo. */\n filter: drop-shadow(0 1px 2px light-dark(rgba(0, 0, 0, 0.5), rgba(255, 255, 255, 0.35)));\n}\n/* Feedback intensifies the chevron toward its extreme (white in light mode, black in dark)\n rather than tinting it, so no new colour is introduced. Not tied to --zine-controls-fg: that\n sets the resting fill, and hover has to differ from it to read as feedback. :active clears on\n release, so tap and click feel the same; :hover is behind (hover: hover) so it never sticks on\n touch. */\n.zine-arrow:active:not(:disabled) { color: light-dark(#ffffff, #000000); }\n@media (hover: hover) {\n .zine-arrow:hover:not(:disabled) { color: light-dark(#ffffff, #000000); }\n}\n.zine-arrow:focus-visible {\n outline: 2px solid var(--zine-controls-accent, light-dark(#0284c7, #7dd3fc));\n outline-offset: 2px;\n border-radius: 8px;\n}\n/* Nothing to turn to: invisible, but still occupying its slot so the book does not slide across\n as the reader reaches a cover. */\n.zine-arrow-hidden { visibility: hidden; }\n/* Device-scoped arrows (controls.arrows: 'desktop' | 'mobile'). The split follows the same 640px\n breakpoint as the flank/overlay layouts below, so the arrows appear and disappear live as the\n window crosses it. */\n@media (max-width: 640px) {\n .zine-arrows-desktop > .zine-arrow { display: none; }\n}\n@media (min-width: 641px) {\n .zine-arrows-mobile > .zine-arrow { display: none; }\n}\n/* Too narrow to flank without squeezing the pages, so overlay the arrows on the book's edges\n instead. The wrap sits outside the container, so painting them over it leaves the container's\n measured box (which sizes the book and maps taps) untouched. */\n@media (max-width: 640px) {\n .zine-arrows-wrap { position: relative; }\n .zine-arrow {\n position: absolute;\n top: 50%;\n transform: translateY(-50%);\n z-index: 2;\n /* Sit back over the page so the reader can look at the book. */\n opacity: 0.7;\n }\n .zine-arrow:first-child { left: -2px; }\n .zine-arrow:last-child { right: -2px; }\n}\n@media (prefers-reduced-motion: no-preference) {\n .zine-arrow { transition: background 120ms ease; }\n}\n\n/* Side panels (thumbnails, outline, search). The rail wraps the book and its docked toolbar so it\n sits beside them as a flex sibling. On a wide screen the book shrinks to make room (below); on a\n narrow one the rail becomes a drawer over the book.\n align-items:flex-start, not stretch: the book sizes its height from its width through its own\n aspect-ratio, and that height is 'auto' from flex's point of view, so a stretch would override it\n and blow the book (and the rail matched to it) up to the flex line's cross size. Left as auto, the\n book keeps its aspect height and the holder alone stretches down to meet it (below). */\n.zine-panel-wrap { display: flex; align-items: flex-start; gap: 8px; position: relative; }\n\n/* The book-side slot (the bare container, or the docked-toolbar wrapper around it) yields space to\n the fixed-width rail instead of overflowing: flex-shrink lets it fall below its own width, and\n min-width:0 removes the automatic content floor that would otherwise stop it. The book only\n actually shrinks if its width is elastic (a %, max-width, or the demo's min(...) with a % term);\n a hard-coded pixel width has nothing for the % to resolve smaller against. */\n.zine-panel-wrap > *:not(.zine-panel-holder):not(.zine-panel-scrim) {\n flex: 0 1 auto;\n min-width: 0;\n}\n/* With a docked toolbar the book lives inside .zine-controls-wrap, whose children are pinned\n flex:0 0 auto (so the bar keeps its size). Inside a panel wrap the book child — everything but\n the bar itself — must instead be allowed to shrink, so left/right docking flanks like the rest.\n Top/bottom docking already shrinks through the container's own % width. */\n.zine-panel-wrap .zine-controls-wrap > *:not(.zine-controls) {\n flex: 0 1 auto;\n min-width: 0;\n min-height: 0;\n}\n@media (prefers-reduced-motion: no-preference) {\n .zine-panel-wrap > *:not(.zine-panel-holder):not(.zine-panel-scrim) {\n transition: flex-basis 160ms ease, width 160ms ease;\n }\n}\n\n/* The holder stretches to the book's height; the rail is absolutely positioned inside it so a\n long list scrolls rather than growing the row and running past the bottom of the book. */\n.zine-panel-holder {\n position: relative;\n flex: 0 0 auto;\n align-self: stretch;\n min-height: 0;\n}\n/* The scrim dims the book behind the narrow-screen drawer. It exists on every screen but only\n shows under the breakpoint below, so on a wide screen it takes no flex slot and never intercepts\n a click. */\n.zine-panel-scrim { display: none; }\n.zine-panel {\n position: absolute;\n inset: 0;\n display: flex;\n flex-direction: column;\n gap: 6px;\n overflow-y: auto;\n /* Chain overscroll up to the page: once the list is at its end (or too short to scroll at all),\n a further wheel scrolls the page behind, the same as scrolling over the book itself does. */\n overscroll-behavior: auto;\n /* Reserve the scrollbar's lane whether or not it is showing, so rows never sit under the bar\n and the close button (inset past this lane below) has a fixed edge to clear. */\n scrollbar-gutter: stable;\n padding: 6px;\n box-sizing: border-box;\n border-radius: 8px;\n background: var(--zine-controls-bg, light-dark(rgba(255, 255, 255, 0.94), rgba(24, 24, 27, 0.82)));\n color: var(--zine-controls-fg, light-dark(#18181b, #f4f4f5));\n font: 500 11px/1.2 system-ui, sans-serif;\n scrollbar-width: thin;\n touch-action: auto;\n -webkit-user-select: none;\n user-select: none;\n}\n/* The rail sits flush against the book's reading-start edge (left, or right in RTL), so its outer\n corners there would round away from that edge and leave a gap. Square them; keep the interior\n corners, which face the page, rounded. */\n.zine-panel { border-radius: 0 8px 8px 0; }\n.zine-panel-wrap-rtl .zine-panel { border-radius: 8px 0 0 8px; }\n/* Overlay fallback, used when the container has no parent to wrap. */\n.zine-panel-overlay { position: absolute; inset: 0 auto 0 0; z-index: 3; }\n.zine-panel-active { background: var(--zine-controls-hover, light-dark(rgba(0,0,0,0.08), rgba(255,255,255,0.18))); }\n.zine-panel-note { padding: 8px; opacity: 0.66; line-height: 1.4; }\n/* Discoverable dismiss, floated in the rail's trailing-top (interior) corner, away from the book.\n It sits in the holder above the scrolling list so the list scrolls under it. A translucent\n backdrop keeps the glyph legible over a thumbnail or a line of text. */\n.zine-panel-close {\n position: absolute;\n top: 6px;\n /* Inset past the reserved scrollbar gutter so the button never overlaps the bar. */\n right: 12px;\n z-index: 1;\n display: inline-flex;\n width: 24px;\n height: 24px;\n align-items: center;\n justify-content: center;\n padding: 0;\n border: 0;\n border-radius: 6px;\n /* Opaque so a light thumbnail underneath never shows through and dims the glyph. */\n background: light-dark(#f4f4f5, #27272a);\n color: inherit;\n cursor: pointer;\n}\n.zine-panel-close svg { width: 15px; height: 15px; }\n/* Opaque hover, not the translucent hover token: a see-through backdrop over a light thumbnail\n would wash the glyph out. Solid neutrals keep the X readable in both themes over anything. */\n.zine-panel-close:hover { background: light-dark(#e4e4e7, #3f3f46); }\n.zine-panel-close:focus-visible {\n outline: 2px solid var(--zine-controls-accent, light-dark(#0284c7, #7dd3fc));\n outline-offset: -2px;\n}\n.zine-panel-wrap-rtl .zine-panel-close { right: auto; left: 12px; }\n/* End the search field just before the close button rather than running under it, so the two sit\n side by side and the field's border reads cleanly. The button's inner edge is ~30px from the\n panel's content edge (24px wide, its right edge 6px in once the reserved scrollbar gutter is\n accounted for), so 30px leaves them flush with a hair of breathing room. width:auto lets the\n column's stretch fill the rest, so the margin shortens the field instead of overflowing 100%. */\n.zine-search .zine-search-input { width: auto; align-self: stretch; margin-right: 30px; }\n.zine-panel-wrap-rtl .zine-search .zine-search-input { margin-right: 0; margin-left: 30px; }\n\n/* Overlay panels (outline, search) on a wide screen: float over the book's start edge instead of\n flanking it, so the book never resizes. The wrap drops to a block, laying the book out exactly as\n it was before the panel opened; the holder sits absolute over it at full book height, with a\n shadow so it reads as lifted off the page. thumbnails is not overlaid — it stays a flex flank and\n shrinks the book only when the two will not otherwise both fit. */\n@media (min-width: 641px) {\n .zine-panel-wrap-overlay { display: block; }\n .zine-panel-wrap-overlay .zine-panel-holder {\n position: absolute;\n inset: 0 auto 0 0;\n z-index: 4;\n box-shadow: 0 0 24px light-dark(rgba(0, 0, 0, 0.22), rgba(0, 0, 0, 0.55));\n }\n .zine-panel-wrap-overlay.zine-panel-wrap-rtl .zine-panel-holder { inset: 0 0 0 auto; }\n /* Opaque over the page it floats on, unlike a flanking rail that sits against empty margin. */\n .zine-panel-wrap-overlay .zine-panel {\n background: var(--zine-controls-bg, light-dark(#ffffff, #18181b));\n }\n /* A click-away layer over the book, so pressing the page behind the floating rail closes it the\n way tapping the scrim does on a narrow screen. Transparent here — the wide-screen rail only\n covers an edge, so there is no need to dim the page the reader is still looking at. It sits\n under the holder (z-index) so the rail's own rows still take their clicks. */\n .zine-panel-wrap-overlay .zine-panel-scrim {\n display: block;\n position: absolute;\n inset: 0;\n z-index: 3;\n background: transparent;\n }\n}\n@media (min-width: 641px) and (prefers-reduced-motion: no-preference) {\n .zine-panel-wrap-overlay .zine-panel-holder { animation: zine-drawer-in 180ms ease; }\n .zine-panel-wrap-overlay.zine-panel-wrap-rtl .zine-panel-holder { animation-name: zine-drawer-in-rtl; }\n}\n\n/* Narrow screen: no room to flank, so the rail becomes a drawer over the book and the scrim dims\n the page behind it. The wrap is position:relative, so the absolute holder and scrim below are\n measured against it — i.e. against the book's own box. */\n@media (max-width: 640px) {\n /* No flanking here — the rail and scrim both overlay absolutely — so drop the flex row entirely.\n As a plain block wrap the book keeps the exact box it had before a panel opened (its own width,\n margin and aspect-ratio height); a flex row would instead re-resolve the book's width and, with\n it, its aspect-derived height, enlarging the canvas and pushing the page down. */\n .zine-panel-wrap {\n display: block;\n }\n .zine-panel-holder {\n position: absolute;\n inset: 0 auto 0 0;\n z-index: 4;\n /* Cap the drawer so it never eats the whole book; its inline width still applies under this. */\n max-width: 80%;\n box-shadow: 0 0 24px light-dark(rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.6));\n }\n /* RTL reads from the right, so the drawer enters from there. */\n .zine-panel-wrap-rtl .zine-panel-holder { inset: 0 0 0 auto; }\n /* Opaque over the page, unlike the wide-screen rail which can sit against empty margin. */\n .zine-panel {\n background: var(--zine-controls-bg, light-dark(#ffffff, #18181b));\n }\n .zine-panel-scrim {\n display: block;\n position: absolute;\n inset: 0;\n z-index: 3;\n background: rgba(0, 0, 0, 0.4);\n }\n}\n@media (max-width: 640px) and (prefers-reduced-motion: no-preference) {\n .zine-panel-holder { animation: zine-drawer-in 180ms ease; }\n .zine-panel-wrap-rtl .zine-panel-holder { animation-name: zine-drawer-in-rtl; }\n .zine-panel-scrim { animation: zine-scrim-in 180ms ease; }\n}\n@keyframes zine-drawer-in { from { transform: translateX(-100%); } to { transform: translateX(0); } }\n@keyframes zine-drawer-in-rtl { from { transform: translateX(100%); } to { transform: translateX(0); } }\n@keyframes zine-scrim-in { from { opacity: 0; } to { opacity: 1; } }\n\n.zine-thumbs { align-items: center; }\n\n.zine-thumbs-row {\n display: flex;\n flex-wrap: wrap;\n justify-content: center;\n gap: 2px;\n width: 100%;\n padding: 4px;\n border: 0;\n border-radius: 6px;\n background: transparent;\n color: inherit;\n font: inherit;\n cursor: pointer;\n box-sizing: border-box;\n}\n.zine-thumbs-row:hover { background: var(--zine-controls-hover, light-dark(rgba(0,0,0,0.08), rgba(255,255,255,0.14))); }\n.zine-thumbs-row:focus-visible {\n outline: 2px solid var(--zine-controls-accent, light-dark(#0284c7, #7dd3fc));\n outline-offset: -2px;\n}\n.zine-thumbs-row.zine-panel-active .zine-thumbs-cell {\n outline: 1px solid var(--zine-controls-accent, light-dark(#0284c7, #7dd3fc));\n}\n\n/* Outline: a single column of headings, indented by depth. */\n/* No gap: a hairline between rows is the separator instead, so a title that wraps to two lines\n still reads as one entry rather than blending into its neighbours. */\n.zine-outline { gap: 0; }\n.zine-outline-row {\n display: block;\n width: 100%;\n padding: 6px 8px;\n border: 0;\n border-bottom: 1px solid var(--zine-controls-divider, light-dark(rgba(0, 0, 0, 0.08), rgba(255, 255, 255, 0.1)));\n border-radius: 5px;\n background: transparent;\n color: inherit;\n font: inherit;\n text-align: left;\n line-height: 1.35;\n cursor: pointer;\n box-sizing: border-box;\n overflow-wrap: anywhere;\n}\n.zine-outline-row:last-child { border-bottom: 0; }\n.zine-outline-row:hover:not(:disabled) {\n background: var(--zine-controls-hover, light-dark(rgba(0,0,0,0.08), rgba(255,255,255,0.14)));\n}\n.zine-outline-row:focus-visible {\n outline: 2px solid var(--zine-controls-accent, light-dark(#0284c7, #7dd3fc));\n outline-offset: -2px;\n}\n/* A heading whose destination could not be resolved: shown, but nothing to click through to. */\n.zine-outline-row:disabled { opacity: 0.5; cursor: default; }\n\n.zine-thumbs-cell {\n flex: 1 1 0;\n min-width: 0;\n min-height: 24px;\n display: flex;\n background: light-dark(rgba(0, 0, 0, 0.08), rgba(0, 0, 0, 0.3));\n border-radius: 2px;\n overflow: hidden;\n}\n/* A lone page in a book that pairs elsewhere (a cover, a back page) keeps one page's width and\n centres, instead of stretching across both columns. */\n.zine-thumbs-row-lone .zine-thumbs-cell { flex: 0 0 calc(50% - 1px); }\n.zine-thumbs-img { display: block; width: 100%; height: auto; }\n.zine-thumbs-caption { flex: 1 0 100%; text-align: center; opacity: 0.7; }\n\n/* Share dialog. Centred over the page rather than in the side rail: a QR and six destinations\n need more room than the rail gives, and sharing is a brief interruption, not a browsing mode. */\n.zine-share-backdrop {\n position: fixed;\n inset: 0;\n z-index: 2147483000;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 16px;\n background: rgba(0, 0, 0, 0.55);\n font: 500 13px/1.4 system-ui, sans-serif;\n color: var(--zine-controls-fg, light-dark(#18181b, #f4f4f5));\n}\n.zine-share {\n width: min(320px, 100%);\n max-height: 100%;\n overflow-y: auto;\n display: flex;\n flex-direction: column;\n gap: 12px;\n padding: 14px;\n border-radius: 12px;\n background: var(--zine-share-bg, light-dark(#ffffff, #1c1c20));\n box-shadow: 0 18px 50px rgba(0, 0, 0, 0.45);\n box-sizing: border-box;\n}\n.zine-share-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }\n.zine-share-head strong { font-size: 15px; }\n.zine-share-close {\n display: inline-flex;\n width: 26px;\n height: 26px;\n align-items: center;\n justify-content: center;\n padding: 0;\n border: 0;\n border-radius: 6px;\n background: transparent;\n color: inherit;\n cursor: pointer;\n}\n.zine-share-close svg { width: 16px; height: 16px; }\n.zine-share-close:hover { background: var(--zine-controls-hover, light-dark(rgba(0,0,0,0.08), rgba(255,255,255,0.14))); }\n\n.zine-share-qr {\n align-self: center;\n line-height: 0;\n padding: 8px;\n border-radius: 8px;\n background: #fff;\n}\n.zine-share-qr svg { display: block; }\n\n.zine-share-link { display: flex; gap: 6px; }\n.zine-share-link input {\n flex: 1 1 auto;\n min-width: 0;\n padding: 7px 9px;\n border: 1px solid var(--zine-controls-hover, light-dark(rgba(0,0,0,0.14), rgba(255,255,255,0.2)));\n border-radius: 7px;\n background: light-dark(rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.3));\n color: inherit;\n font: inherit;\n}\n.zine-share-copy {\n flex: 0 0 auto;\n min-width: 74px;\n padding: 7px 12px;\n border: 0;\n border-radius: 7px;\n background: var(--zine-controls-accent, light-dark(#0284c7, #7dd3fc));\n color: light-dark(#ffffff, #06202c);\n font: inherit;\n font-weight: 600;\n cursor: pointer;\n}\n\n.zine-share-socials { display: flex; flex-wrap: wrap; gap: 6px; justify-content: center; }\n.zine-share-social {\n display: inline-flex;\n width: 38px;\n height: 38px;\n align-items: center;\n justify-content: center;\n border-radius: 8px;\n background: light-dark(rgba(0, 0, 0, 0.06), rgba(255, 255, 255, 0.08));\n color: inherit;\n text-decoration: none;\n}\n.zine-share-social svg { width: 19px; height: 19px; }\n.zine-share-social:hover { background: var(--zine-controls-hover, light-dark(rgba(0,0,0,0.12), rgba(255,255,255,0.18))); }\n.zine-share-close:focus-visible,\n.zine-share-copy:focus-visible,\n.zine-share-social:focus-visible,\n.zine-share-link input:focus-visible {\n outline: 2px solid var(--zine-controls-accent, light-dark(#0284c7, #7dd3fc));\n outline-offset: 2px;\n}\n\n@media (prefers-reduced-motion: no-preference) {\n .zine-controls-btn { transition: background 120ms ease; }\n}\n`;\n\n/** Add the stylesheet to `doc` if it is not already there. */\nexport function ensureStyles(doc: Document): void {\n if (doc.getElementById(STYLE_ID)) return;\n const style = doc.createElement('style');\n style.id = STYLE_ID;\n style.textContent = CSS;\n (doc.head ?? doc.documentElement)?.appendChild(style);\n}\n\n/**\n * Pin an element's colour scheme so the `light-dark()` fallbacks resolve to one palette.\n *\n * `'auto'` does nothing: `color-scheme` inherits, so the element keeps the page's used scheme and\n * `light-dark()` follows it. `'light'` / `'dark'` set the property on the element itself, which its\n * themed descendants (buttons, menu, dialog) inherit in turn. An unset `--zine-controls-*` reads\n * the forced palette; a set one still overrides it, since the tokens sit outside `light-dark()`.\n */\nexport function applyColorScheme(el: HTMLElement, scheme: 'light' | 'dark' | 'auto'): void {\n if (scheme !== 'auto') el.style.colorScheme = scheme;\n}\n","import { createIcon, ICONS } from './icons';\nimport { SOCIALS } from './socials';\nimport { qrSvg } from './qr';\nimport { applyColorScheme } from './styles';\nimport type { ControlsColorScheme } from './types';\nimport type { Zine } from '../zine';\n\n/** QR edge length in CSS px. */\nconst QR_SIZE = 132;\n/** How long the copy button confirms for. */\nconst COPIED_MS = 1600;\n\n/**\n * The share dialog: a QR code, the link, a copy button and the usual social destinations.\n *\n * Loaded on demand — the whole module, its icons and the QR encoder arrive only when a reader\n * presses Share, so a book nobody shares pays nothing for it.\n */\nexport class ShareDialog {\n #doc: Document;\n #backdrop: HTMLElement;\n #dialog: HTMLElement;\n #lastFocus: Element | null;\n #timer: ReturnType<typeof setTimeout> | null = null;\n #onKey: (e: KeyboardEvent) => void;\n\n constructor(zine: Zine, container: HTMLElement, colorScheme: ControlsColorScheme = 'auto') {\n const doc = container.ownerDocument!;\n this.#doc = doc;\n this.#lastFocus = doc.activeElement;\n\n const url = zine.pageLink();\n const title = doc.title || 'Flipbook';\n\n this.#backdrop = doc.createElement('div');\n this.#backdrop.className = 'zine-share-backdrop';\n // The dialog is mounted on <body>, outside the toolbar, so it does not inherit the stamp;\n // apply it here. color-scheme then inherits down to the dialog and its fields.\n applyColorScheme(this.#backdrop, colorScheme);\n this.#backdrop.addEventListener('pointerdown', (e) => {\n if (e.target === this.#backdrop) this.close();\n });\n\n this.#dialog = doc.createElement('div');\n this.#dialog.className = 'zine-share';\n this.#dialog.setAttribute('role', 'dialog');\n this.#dialog.setAttribute('aria-modal', 'true');\n this.#dialog.setAttribute('aria-label', 'Share');\n this.#backdrop.appendChild(this.#dialog);\n\n const head = doc.createElement('div');\n head.className = 'zine-share-head';\n const heading = doc.createElement('strong');\n heading.textContent = 'Share';\n const close = doc.createElement('button');\n close.type = 'button';\n close.className = 'zine-share-close';\n close.setAttribute('aria-label', 'Close');\n close.appendChild(createIcon(doc, ICONS.close));\n close.addEventListener('click', () => this.close());\n head.append(heading, close);\n\n this.#dialog.append(head, this.#linkRow(doc, url), this.#socialRow(doc, url, title));\n const qr = this.#qr(doc, url);\n if (qr) this.#dialog.insertBefore(qr, this.#dialog.children[1]!);\n\n container.ownerDocument.body.appendChild(this.#backdrop);\n\n this.#onKey = (e): void => {\n if (e.key === 'Escape') this.close();\n if (e.key === 'Tab') this.#trapFocus(e);\n };\n this.#backdrop.addEventListener('keydown', this.#onKey);\n close.focus();\n }\n\n /** The QR, or nothing if the URL is too long to encode — better no code than a broken one. */\n #qr(doc: Document, url: string): HTMLElement | null {\n if (!url) return null;\n try {\n const wrap = doc.createElement('div');\n wrap.className = 'zine-share-qr';\n wrap.innerHTML = qrSvg(url, QR_SIZE);\n wrap.setAttribute('aria-label', 'QR code for this page');\n wrap.setAttribute('role', 'img');\n return wrap;\n } catch {\n return null;\n }\n }\n\n #linkRow(doc: Document, url: string): HTMLElement {\n const row = doc.createElement('div');\n row.className = 'zine-share-link';\n const input = doc.createElement('input');\n input.type = 'text';\n input.readOnly = true;\n input.value = url;\n input.setAttribute('aria-label', 'Link to this page');\n input.addEventListener('focus', () => input.select());\n\n const copy = doc.createElement('button');\n copy.type = 'button';\n copy.className = 'zine-share-copy';\n copy.textContent = 'Copy';\n copy.addEventListener('click', () => {\n void this.#copy(url, copy);\n });\n row.append(input, copy);\n return row;\n }\n\n async #copy(url: string, button: HTMLButtonElement): Promise<void> {\n try {\n await navigator.clipboard?.writeText(url);\n button.textContent = 'Copied';\n } catch {\n button.textContent = 'Press Ctrl+C';\n }\n if (this.#timer) clearTimeout(this.#timer);\n this.#timer = setTimeout(() => {\n button.textContent = 'Copy';\n }, COPIED_MS);\n }\n\n #socialRow(doc: Document, url: string, title: string): HTMLElement {\n const row = doc.createElement('div');\n row.className = 'zine-share-socials';\n for (const social of SOCIALS) {\n const link = doc.createElement('a');\n link.className = 'zine-share-social';\n link.href = social.href(url, title);\n link.title = social.label;\n link.setAttribute('aria-label', `Share on ${social.label}`);\n if (social.id !== 'email') {\n link.target = '_blank';\n link.rel = 'noopener noreferrer';\n }\n link.appendChild(createIcon(doc, social.icon));\n row.appendChild(link);\n }\n return row;\n }\n\n /** Keep Tab inside the dialog while it is open. */\n #trapFocus(event: KeyboardEvent): void {\n const focusable = [\n ...this.#dialog.querySelectorAll<HTMLElement>('button, a[href], input'),\n ].filter((el) => !el.hasAttribute('disabled'));\n if (focusable.length === 0) return;\n const first = focusable[0]!;\n const last = focusable.at(-1)!;\n const active = this.#doc.activeElement;\n if (event.shiftKey && active === first) {\n last.focus();\n event.preventDefault();\n } else if (!event.shiftKey && active === last) {\n first.focus();\n event.preventDefault();\n }\n }\n\n close(): void {\n this.destroy();\n }\n\n destroy(): void {\n if (this.#timer) clearTimeout(this.#timer);\n this.#timer = null;\n this.#backdrop.removeEventListener('keydown', this.#onKey);\n this.#backdrop.remove();\n // Hand focus back to whatever opened the dialog.\n (this.#lastFocus as HTMLElement | null)?.focus?.();\n }\n}\n","import { ICONS } from './icons';\nimport { defineControl } from './registry';\nimport type { ControlContext } from './types';\n\n\n/** Zoom step per click: two clicks double the scale. */\nconst ZOOM_STEP = Math.SQRT2;\n\nconst isFullscreen = (ctx: ControlContext): boolean => {\n const doc = ctx.zine.container.ownerDocument;\n return doc?.fullscreenElement === ctx.zine.container;\n};\n\n/**\n * Register the stock controls. Called once as the toolbar chunk loads, so the icon set and these\n * definitions stay out of the base bundle.\n *\n * `pageInput` and `search` are not here: both need the toolbar instance itself (one builds an\n * input, the other opens an anchored panel), so the toolbar registers them.\n */\nexport function registerBuiltins(): void {\n defineControl({\n id: 'prev',\n title: 'Previous page',\n icon: ICONS.prev,\n isDisabled: (ctx) => !ctx.zine.canFlipPrev(),\n action: (ctx) => ctx.zine.flipPrev(),\n });\n\n defineControl({\n id: 'next',\n title: 'Next page',\n icon: ICONS.next,\n isDisabled: (ctx) => !ctx.zine.canFlipNext(),\n action: (ctx) => ctx.zine.flipNext(),\n });\n\n // The chevrons point the way the book travels, which reverses in RTL — same as the page\n // arrows flanking the book.\n const rtl = (ctx: ControlContext): boolean => ctx.zine.getDirection() === 'rtl';\n\n defineControl({\n id: 'first',\n title: 'First page',\n icon: (ctx) => (rtl(ctx) ? ICONS.last : ICONS.first),\n isDisabled: (ctx) => !ctx.zine.canFlipPrev(),\n action: (ctx) => {\n ctx.zine.flipTo(0);\n ctx.close();\n },\n });\n\n defineControl({\n id: 'last',\n title: 'Last page',\n icon: (ctx) => (rtl(ctx) ? ICONS.first : ICONS.last),\n isDisabled: (ctx) => !ctx.zine.canFlipNext(),\n action: (ctx) => {\n ctx.zine.flipTo(ctx.zine.getPageCount() - 1);\n ctx.close();\n },\n });\n\n defineControl({\n id: 'zoomIn',\n title: 'Zoom in',\n icon: ICONS.zoomIn,\n isDisabled: (ctx) => ctx.zine.getZoom() >= ctx.zine.getMaxZoom() - 1e-6,\n action: (ctx) => ctx.zine.setZoom(ctx.zine.getZoom() * ZOOM_STEP),\n });\n\n defineControl({\n id: 'zoomOut',\n title: 'Zoom out',\n icon: ICONS.zoomOut,\n isDisabled: (ctx) => ctx.zine.getZoom() <= 1 + 1e-6,\n action: (ctx) => ctx.zine.setZoom(ctx.zine.getZoom() / ZOOM_STEP),\n });\n\n defineControl({\n id: 'fullscreen',\n title: 'Fullscreen',\n icon: ICONS.fullscreen,\n isVisible: (ctx) => typeof ctx.zine.container.requestFullscreen === 'function',\n isActive: isFullscreen,\n action: (ctx) => {\n const doc = ctx.zine.container.ownerDocument;\n if (isFullscreen(ctx)) void doc?.exitFullscreen?.();\n else void ctx.zine.container.requestFullscreen?.();\n ctx.close();\n },\n });\n\n defineControl({\n id: 'share',\n title: 'Share',\n icon: ICONS.share,\n action: (ctx) => {\n ctx.close();\n // The dialog, its brand marks and the QR encoder are a chunk of their own: a book nobody\n // shares never downloads any of it.\n void import('./share')\n .then(({ ShareDialog }) => new ShareDialog(ctx.zine, ctx.zine.container, ctx.colorScheme))\n .catch(() => {\n // No dialog, so fall back to the plain behaviour rather than doing nothing.\n void navigator.clipboard?.writeText(ctx.zine.pageLink()).catch(() => {});\n });\n },\n });\n\n defineControl({\n id: 'download',\n title: 'Download PDF',\n icon: ICONS.download,\n // Hidden for image books and for PDFs opened from a caller-owned pdf.js document, which\n // have no file of their own to save.\n isVisible: (ctx) => ctx.zine.canDownload(),\n // Async: a cross-origin PDF is fetched into a blob first, which takes a beat. Returning the\n // promise lets the toolbar hold the menu open with a spinner until the save starts, then close.\n action: async (ctx) => {\n await ctx.zine.download();\n ctx.close();\n },\n });\n\n defineControl({\n id: 'spread',\n // Says what pressing it will do, not what the book is doing now, and in the reader's terms —\n // \"spread mode\" is the library's word for it, not theirs.\n title: (ctx) => (ctx.zine.isSinglePage() ? 'Show two pages' : 'Show one page'),\n icon: (ctx) => (ctx.zine.isSinglePage() ? ICONS.twoPages : ICONS.onePage),\n // Hidden while a narrow container is forcing one page: pressing it could not honour two.\n isVisible: (ctx) => !ctx.zine.isResponsiveSingle(),\n action: (ctx) => {\n ctx.zine.toggleSpreadMode();\n ctx.close();\n },\n });\n\n defineControl({\n id: 'print',\n title: 'Print',\n icon: ICONS.print,\n // Same reach as download: there has to be an original document to print.\n isVisible: (ctx) => ctx.zine.canPrint(),\n // Async like download: the print frame needs a same-origin blob for a cross-origin file, so\n // hold the menu open with a spinner until the frame is ready, then close.\n action: async (ctx) => {\n await ctx.zine.print();\n ctx.close();\n },\n });\n\n defineControl({\n id: 'menu',\n title: 'More',\n icon: ICONS.menu,\n // Share and fullscreen sit on the bar itself by default, so the overflow holds what is left.\n children: ['first', 'last', 'spread', 'thumbnails', 'outline', 'print', 'download'],\n });\n}\n","import { createIcon } from './icons';\nimport { resolveControl } from './registry';\nimport type { ControlContext, ControlDef, ControlItem } from './types';\n\n/** Shared empty set, so a menu built without a hide-list allocates nothing. */\nconst NONE_HIDDEN: ReadonlySet<string> = new Set();\n\n/** The id a layout item resolves to, or null for a separator (which has none). */\nfunction itemId(item: ControlItem): string | null {\n if (item === '|') return null;\n return typeof item === 'string' ? item : item.id;\n}\n\n/**\n * Drop every control whose id is in `hidden`, the one filter behind the top-level `hideControls`\n * option. Applied at each build point so a named control disappears from the toolbar, its overflow\n * menu and the right-click menu at once. Separators are left in place; menus drop them anyway and\n * the toolbar tidies its own runs after filtering.\n */\nexport function filterHidden(\n items: readonly ControlItem[],\n hidden: ReadonlySet<string>,\n): readonly ControlItem[] {\n if (!hidden.size) return items;\n return items.filter((item) => {\n const id = itemId(item);\n return id === null || !hidden.has(id);\n });\n}\n\n/**\n * The shared menu machinery.\n *\n * A `.zine-controls-menu` is built the same way in two places: the toolbar's overflow (the ⋮\n * button) and the right-click context menu. Rather than let those drift, both build their items,\n * read their state, and run their actions through the helpers here, so a menu looks and behaves\n * identically wherever it is anchored. Placement and dismissal stay with each owner, because a\n * popover hanging off a toolbar button and one opening at the cursor differ only there.\n */\n\n/** A control's label, which may depend on the current state. */\nexport function titleOf(def: ControlDef, ctx: ControlContext): string {\n return typeof def.title === 'function' ? def.title(ctx) : def.title;\n}\n\n/** A control's glyph, which may likewise depend on state (an RTL mirror, say). */\nexport function iconOf(def: ControlDef, ctx: ControlContext): string | undefined {\n return typeof def.icon === 'function' ? def.icon(ctx) : def.icon;\n}\n\n/** The element itself if it can be disabled, else any inputs/buttons it wraps. */\nexport function formFields(el: HTMLElement): (HTMLButtonElement | HTMLInputElement)[] {\n if (el instanceof HTMLButtonElement || el instanceof HTMLInputElement) return [el];\n return [...el.querySelectorAll<HTMLButtonElement | HTMLInputElement>('button, input')];\n}\n\n/** Toggle the pending-action state on a button: a trailing spinner, disabled while it spins. */\nexport function setBusy(btn: HTMLButtonElement, busy: boolean): void {\n btn.classList.toggle('zine-controls-busy', busy);\n btn.setAttribute('aria-busy', String(busy));\n btn.disabled = busy;\n}\n\n/**\n * Run a control's action, holding the button busy while a slow one (a cross-origin file fetched to\n * save or print) settles. `onAfter` re-reads state once the action has run, and again when a\n * pending one finishes. A disabled or already-busy button does nothing.\n */\nexport function activateControl(\n def: ControlDef,\n trigger: HTMLButtonElement,\n ctx: ControlContext,\n onAfter: () => void,\n): void {\n if (def.isDisabled?.(ctx)) return;\n if (trigger.classList.contains('zine-controls-busy')) return;\n const result = def.action?.(ctx);\n if (result && typeof result.then === 'function') {\n setBusy(trigger, true);\n void result.finally(() => {\n setBusy(trigger, false);\n onAfter();\n });\n }\n onAfter();\n}\n\n/**\n * A control button. Bar buttons are icon-only with the title as tooltip; menu items and any\n * control without an icon also carry a visible label. `onActivate` is left to the caller so the\n * toolbar can route a press through its submenu logic while a menu item just acts.\n */\nexport function buildControlButton(\n doc: Document,\n def: ControlDef,\n className: string,\n ctx: ControlContext,\n onActivate: (def: ControlDef, btn: HTMLButtonElement) => void,\n withLabel = false,\n): HTMLButtonElement {\n const btn = doc.createElement('button');\n const title = titleOf(def, ctx);\n const icon = iconOf(def, ctx);\n btn.type = 'button';\n btn.className = className;\n btn.title = title;\n btn.setAttribute('aria-label', title);\n if (icon) btn.appendChild(createIcon(doc, icon));\n if (withLabel || !icon) {\n const label = doc.createElement('span');\n label.className = 'zine-controls-label';\n label.textContent = title;\n btn.appendChild(label);\n }\n btn.addEventListener('click', () => onActivate(def, btn));\n return btn;\n}\n\n/**\n * Re-read one control's state after the book changes and reflect it on its element.\n *\n * Applies to custom `render` widgets as well as plain buttons: the predicates live on `ControlDef`,\n * so a widget may be any element and each piece of state is applied however that element supports\n * it. `visible` overrides the `isVisible` predicate for a control whose visibility is decided\n * elsewhere (a submenu shown only when it has visible children).\n */\nexport function applyControlItemState(\n el: HTMLElement,\n def: ControlDef,\n ctx: ControlContext,\n doc: Document,\n visible?: boolean,\n): void {\n const applies = visible ?? def.isVisible?.(ctx);\n if (applies !== undefined) el.style.display = applies ? '' : 'none';\n\n // A button running a slow action stays disabled until it settles, whatever its predicate says.\n const busy = el.classList.contains('zine-controls-busy');\n const disabled = (def.isDisabled?.(ctx) ?? false) || busy;\n const fields = formFields(el);\n // A wrapper element cannot be disabled, so mark it and disable the fields inside.\n for (const field of fields) field.disabled = disabled;\n if (!fields.length) el.setAttribute('aria-disabled', String(disabled));\n el.classList.toggle('zine-controls-off', disabled);\n\n if (def.isActive) el.setAttribute('aria-pressed', String(def.isActive(ctx)));\n if (typeof def.title === 'function') {\n const title = def.title(ctx);\n el.title = title;\n el.setAttribute('aria-label', title);\n const label = el.querySelector('.zine-controls-label');\n if (label) label.textContent = title;\n }\n if (typeof def.icon === 'function') {\n el.querySelector('svg')?.replaceWith(createIcon(doc, def.icon(ctx)));\n }\n}\n\n/**\n * A `.zine-controls-menu`: a column of control items with `role=menu`. Owns the items it builds so\n * it can re-read their state and clean up after itself. The owner supplies the context (whose\n * `close()` dismisses this menu) and drives when to build, refresh, and destroy; where the menu is\n * placed and how it is dismissed are the owner's business.\n */\nexport class ControlMenu {\n readonly el: HTMLElement;\n #doc: Document;\n #context: () => ControlContext;\n #onAfterAction: () => void;\n #hidden: ReadonlySet<string>;\n /** Every item on screen, buttons and `render` widgets alike, so {@link refresh} can reach them. */\n #items: { el: HTMLElement; def: ControlDef }[] = [];\n\n constructor(\n doc: Document,\n context: () => ControlContext,\n onAfterAction: () => void,\n hidden: ReadonlySet<string> = NONE_HIDDEN,\n ) {\n this.#doc = doc;\n this.#context = context;\n this.#onAfterAction = onAfterAction;\n this.#hidden = hidden;\n this.el = doc.createElement('div');\n this.el.className = 'zine-controls-menu';\n this.el.setAttribute('role', 'menu');\n }\n\n /** Fill the menu from a list of controls, dropping separators and anything not applicable. */\n build(children: readonly ControlItem[]): void {\n this.el.replaceChildren();\n this.#items = [];\n const ctx = this.#context();\n for (const child of filterHidden(children, this.#hidden)) {\n if (child === '|') continue;\n const def = resolveControl(child);\n if (def.isVisible && !def.isVisible(ctx)) continue;\n let item: HTMLElement;\n if (def.render) {\n item = def.render(this.#context());\n } else {\n item = buildControlButton(\n this.#doc,\n def,\n 'zine-controls-btn',\n ctx,\n (d, btn) => activateControl(d, btn, this.#context(), this.#onAfterAction),\n true,\n );\n }\n item.setAttribute('role', 'menuitem');\n this.el.appendChild(item);\n this.#items.push({ el: item, def });\n }\n }\n\n /** Re-read every item's state. */\n refresh(): void {\n const ctx = this.#context();\n for (const { el, def } of this.#items) applyControlItemState(el, def, ctx, this.#doc);\n }\n\n /** Whether the menu (or something inside it) contains `node`. */\n contains(node: Node | null): boolean {\n return node !== null && this.el.contains(node);\n }\n\n focusFirst(): void {\n this.el.querySelector('button')?.focus();\n }\n\n destroy(): void {\n this.el.remove();\n this.#items = [];\n }\n}\n\n/** A submenu is only worth showing if something inside it is. Keeps a menu trigger from opening\n * onto nothing when every entry has hidden itself. */\nexport function hasVisibleChildren(\n def: ControlDef,\n ctx: ControlContext,\n hidden: ReadonlySet<string> = NONE_HIDDEN,\n): boolean {\n if (def.isVisible && !def.isVisible(ctx)) return false;\n return filterHidden(def.children ?? [], hidden).some((child) => {\n if (child === '|') return false;\n const childDef = resolveControl(child);\n return childDef.isVisible ? childDef.isVisible(ctx) : true;\n });\n}\n","import { createIcon, ICONS } from './icons';\n\n/**\n * The rail beside the book that the thumbnail and outline lists both sit in.\n *\n * It exists to solve one problem shared by every such panel: the container's measured box is what\n * the renderer sizes the book from and what pointer hit-testing maps into, so a panel must not\n * live inside it. Instead the book (and its docked toolbar, if any) is wrapped in a flex row with\n * the panel as its sibling, and `destroy` unwinds that again.\n *\n * The rail is absolutely positioned inside a holder that stretches to the book's height, so a\n * long list scrolls internally instead of running past the bottom of the book.\n *\n * On a wide screen the rail flanks the book and the book shrinks to make room (see styles.ts). On\n * a narrow one there is no room to flank, so the rail becomes a drawer over the book and a scrim\n * dims the page behind it; tapping the scrim or pressing Escape dismisses it through `onDismiss`.\n */\n/** Shared options a toolbar threads into every panel constructor. */\nexport interface PanelOptions {\n /** Called when the reader dismisses the narrow-screen drawer (scrim tap or Escape). */\n onDismiss?: () => void;\n}\n\n/** The book's painted box inside the container, in container px (see Zine.getPageBox). */\ntype PageBox = { x: number; y: number; width: number; height: number };\n\nexport class Sidebar {\n /** The scrolling list; panels append their rows here. */\n readonly root: HTMLElement;\n #holder: HTMLElement;\n #wrap: HTMLElement | null = null;\n #scrim: HTMLElement | null = null;\n #close: HTMLElement | null = null;\n #onKeydown: ((e: KeyboardEvent) => void) | null = null;\n #doc: Document;\n #pageBox: (() => PageBox | null) | null = null;\n #resizeObserver: ResizeObserver | null = null;\n\n constructor(\n doc: Document,\n container: HTMLElement,\n options: {\n className: string;\n label: string;\n width: number;\n /** Called when the reader dismisses the drawer (scrim tap or Escape). Omit to disable both. */\n onDismiss?: () => void;\n /** Reading direction; the drawer slides in from the right instead of the left when true. */\n rtl?: boolean;\n /** Float over the book's edge instead of flanking it (so the book never resizes). Panels\n * whose width is fixed and modest — the outline and search rails — prefer this; the wider\n * thumbnail rail flanks so both stay fully visible. Only affects wide screens: below the\n * breakpoint every panel is a drawer regardless. */\n overlay?: boolean;\n /** The book's painted box in container px. The rail stands exactly as tall as the page rather\n * than the whole container, which is taller whenever the page is letterboxed (its aspect not\n * matching the container's) or a docked toolbar padded the wrap. Re-read on every resize. */\n pageBox?: () => PageBox | null;\n },\n ) {\n this.#doc = doc;\n this.#holder = doc.createElement('div');\n this.#holder.className = 'zine-panel-holder';\n this.#holder.style.width = `${options.width}px`;\n this.#pageBox = options.pageBox ?? null;\n\n this.root = doc.createElement('div');\n this.root.className = `zine-panel ${options.className}`;\n this.root.setAttribute('role', 'listbox');\n this.root.setAttribute('aria-label', options.label);\n this.#holder.appendChild(this.root);\n\n // In the overlay fallback the rail sits inside the book, where the book's own gesture\n // handlers would otherwise read a click on a row as a page tap.\n for (const type of ['pointerdown', 'pointerup', 'pointermove', 'click', 'dblclick', 'wheel']) {\n this.root.addEventListener(type, (e) => e.stopPropagation());\n }\n\n this.#place(doc, container, options.rtl ?? false, options.overlay ?? false);\n\n if (options.onDismiss) this.#armDismiss(doc, options.onDismiss);\n\n this.#trackPage(container);\n }\n\n /**\n * Keep the rail sized to the painted page. The page box changes whenever the container resizes\n * (window, flanking shrink) or its aspect-ratio does (spread-mode toggle), and a ResizeObserver\n * on the container fires on all of those, so re-fitting there catches every case with no event\n * wiring. Falls back to the full-height CSS when there is no observer or no box yet (e.g. tests).\n */\n #trackPage(container: HTMLElement): void {\n if (!this.#pageBox) return;\n this.#fitToPage();\n if (typeof ResizeObserver === 'undefined') return;\n this.#resizeObserver = new ResizeObserver(() => this.#fitToPage());\n this.#resizeObserver.observe(container);\n }\n\n #fitToPage(): void {\n const box = this.#pageBox?.();\n if (!box) return;\n // top+height (and bottom:auto so an absolute drawer is not over-constrained back to full height)\n // work whether the holder is a relative flex item (flank) or absolutely placed (overlay/drawer).\n this.#holder.style.top = `${box.y}px`;\n this.#holder.style.bottom = 'auto';\n this.#holder.style.height = `${box.height}px`;\n }\n\n /** Resize the rail, e.g. once a thumbnail list knows whether it needs one column or two. */\n setWidth(px: number): void {\n this.#holder.style.width = `${px}px`;\n }\n\n #place(doc: Document, container: HTMLElement, rtl: boolean, overlay: boolean): void {\n const parent = container.parentNode;\n if (!parent) {\n // Not in a document yet: overlay rather than lose the panel.\n this.#holder.classList.add('zine-panel-overlay');\n container.appendChild(this.#holder);\n return;\n }\n // A docked toolbar already wrapped the book; wrap that whole assembly so the rail sits\n // beside book *and* toolbar rather than between them.\n const existing = container.closest('.zine-controls-wrap');\n const target = (existing ?? container) as HTMLElement;\n const wrap = doc.createElement('div');\n wrap.className = 'zine-panel-wrap';\n if (rtl) wrap.classList.add('zine-panel-wrap-rtl');\n // An overlay panel floats over the book's edge rather than flanking it, so the book keeps its\n // size on a wide screen too — the flex-row shrink is off for this wrap.\n if (overlay) wrap.classList.add('zine-panel-wrap-overlay');\n target.parentNode!.insertBefore(wrap, target);\n wrap.append(this.#holder, target);\n this.#wrap = wrap;\n }\n\n /**\n * Wire every way to dismiss the rail. A close button in the rail's trailing-top corner is the\n * discoverable one, present on every screen — the toolbar button and Escape always toggle too,\n * but neither is obvious, and the flanking thumbnail rail has no scrim to click away on. The\n * scrim (drawer only, under the narrow-screen media query) and the Escape key are the other two.\n *\n * The scrim is a sibling of the holder so it can dim the book area without the flex row reserving\n * a column for it. When there is no wrap (the parentless overlay fallback), there is no book\n * beside it to dim, so the scrim is skipped and only the button and Escape are armed.\n */\n #armDismiss(doc: Document, onDismiss: () => void): void {\n const close = doc.createElement('button');\n close.type = 'button';\n close.className = 'zine-panel-close';\n close.setAttribute('aria-label', 'Close');\n close.appendChild(createIcon(doc, ICONS.close));\n // Floats over the scrolling list (see styles.ts); stop the click reaching the book's tap handler\n // in the overlay fallback, where the rail sits inside the container.\n close.addEventListener('click', (e) => {\n e.stopPropagation();\n onDismiss();\n });\n this.#holder.appendChild(close);\n this.#close = close;\n\n if (this.#wrap) {\n const scrim = doc.createElement('div');\n scrim.className = 'zine-panel-scrim';\n // Before the holder so the drawer paints on top of the dim; z-index in CSS makes it explicit.\n this.#wrap.insertBefore(scrim, this.#holder);\n for (const type of ['pointerdown', 'click']) {\n scrim.addEventListener(type, (e) => {\n e.stopPropagation();\n onDismiss();\n });\n }\n this.#scrim = scrim;\n }\n this.#onKeydown = (e: KeyboardEvent): void => {\n if (e.key === 'Escape') onDismiss();\n };\n doc.addEventListener('keydown', this.#onKeydown);\n }\n\n destroy(): void {\n this.#resizeObserver?.disconnect();\n this.#resizeObserver = null;\n if (this.#onKeydown) {\n this.#doc.removeEventListener('keydown', this.#onKeydown);\n this.#onKeydown = null;\n }\n this.#scrim?.remove();\n this.#scrim = null;\n this.#close = null; // lives inside #holder, removed with it below\n this.#holder.remove();\n const wrap = this.#wrap;\n if (wrap?.parentNode) {\n // The rail is gone, so what is left is the book; put it back where the wrapper was.\n while (wrap.firstChild) wrap.parentNode.insertBefore(wrap.firstChild, wrap);\n wrap.remove();\n }\n this.#wrap = null;\n }\n}\n","import { Sidebar, type PanelOptions } from './sidebar';\nimport type { Zine } from '../zine';\n\n/** Width of one page thumbnail, in px. A paired rail fits two of these side by side. */\nconst THUMB_WIDTH = 78;\n/** Padding inside the rail, per side. */\nconst RAIL_PAD = 6;\n/** Gap between the two pages of a spread. */\nconst CELL_GAP = 2;\n\n/**\n * A rail of page thumbnails beside the book.\n *\n * Rows mirror the book's own spread model, so a `double` book shows two pages per row and a\n * `single` book one — including the responsive fallback that forces single-page on a narrow\n * container. A book that mixes the two (`cover`, `book`) keeps two columns so pairs line up, and\n * centres its lone first/last page between them. Clicking a row turns to that spread.\n *\n * Pages are drawn only as they scroll into view: a long PDF would otherwise decode every page at\n * once the moment the rail opened.\n */\nexport class Thumbnails {\n /** The panel's themed root, so the toolbar can stamp its colour scheme. */\n readonly root: HTMLElement;\n #zine: Zine;\n #doc: Document;\n #bar: Sidebar;\n #rows: { el: HTMLElement; spread: number; pages: number[] }[] = [];\n #observer: IntersectionObserver | null = null;\n #unsubscribe: (() => void)[] = [];\n\n constructor(zine: Zine, container: HTMLElement, options: PanelOptions = {}) {\n this.#zine = zine;\n const doc = container.ownerDocument!;\n this.#doc = doc;\n this.#bar = new Sidebar(doc, container, {\n className: 'zine-thumbs',\n label: 'Pages',\n width: THUMB_WIDTH + RAIL_PAD * 2,\n onDismiss: options.onDismiss,\n rtl: zine.getDirection() === 'rtl',\n pageBox: () => zine.getPageBox(),\n });\n this.root = this.#bar.root;\n\n this.#build();\n\n const onPage = (): void => this.#markCurrent();\n this.#unsubscribe.push(zine.on('pageChanged', onPage));\n this.#markCurrent();\n }\n\n #build(): void {\n const spreads = this.#zine.getSpreads();\n const rtl = this.#zine.getDirection() === 'rtl';\n // Two columns only if the book actually pairs pages somewhere. A wholly single-page book\n // gets a one-page-wide rail rather than a column of half-empty rows.\n const paired = spreads.some((s) => s.left !== null && s.right !== null);\n this.#bar.root.classList.toggle('zine-thumbs-solo', !paired);\n if (paired) this.#bar.setWidth(THUMB_WIDTH * 2 + CELL_GAP + RAIL_PAD * 2);\n\n for (const [index, spread] of spreads.entries()) {\n const row = this.#doc.createElement('button');\n row.type = 'button';\n row.className = 'zine-thumbs-row';\n row.setAttribute('role', 'option');\n if (rtl) row.style.flexDirection = 'row-reverse';\n\n const pages = [spread.left, spread.right].filter((p): p is number => p !== null);\n // A lone page in a book that pairs elsewhere keeps a single page's width and sits in the\n // middle, rather than stretching across both columns or hugging one side.\n if (paired && pages.length === 1) row.classList.add('zine-thumbs-row-lone');\n\n for (const page of pages) {\n const cell = this.#doc.createElement('span');\n cell.className = 'zine-thumbs-cell';\n cell.dataset.page = String(page);\n row.appendChild(cell);\n }\n\n const labels = [...pages].sort((a, b) => a - b).map((p) => p + 1);\n row.setAttribute(\n 'aria-label',\n labels.length > 1 ? `Pages ${labels[0]}–${labels.at(-1)}` : `Page ${labels[0] ?? ''}`,\n );\n\n const caption = this.#doc.createElement('span');\n caption.className = 'zine-thumbs-caption';\n caption.textContent = labels.join('–');\n row.appendChild(caption);\n\n row.addEventListener('click', () => {\n if (pages.length > 0) this.#zine.flipTo(Math.min(...pages));\n });\n this.#bar.root.appendChild(row);\n this.#rows.push({ el: row, spread: index, pages });\n }\n this.#observeRows();\n }\n\n /** Decode a row's pages the first time it comes into view. */\n #observeRows(): void {\n const cells = [...this.#bar.root.querySelectorAll<HTMLElement>('.zine-thumbs-cell[data-page]')];\n if (typeof IntersectionObserver === 'undefined') {\n for (const cell of cells) void this.#paint(cell);\n return;\n }\n this.#observer = new IntersectionObserver(\n (entries, obs) => {\n for (const entry of entries) {\n if (!entry.isIntersecting) continue;\n obs.unobserve(entry.target);\n void this.#paint(entry.target as HTMLElement);\n }\n },\n { root: this.#bar.root, rootMargin: '200px' },\n );\n for (const cell of cells) this.#observer.observe(cell);\n }\n\n async #paint(cell: HTMLElement): Promise<void> {\n const page = Number(cell.dataset.page);\n const content = await this.#zine.getPageImage(page);\n if (!content || !cell.isConnected) return;\n const w = (content as { width: number }).width;\n const h = (content as { height: number }).height;\n if (!w || !h) return;\n // One page's worth of pixels, whatever the rail's column count, so thumbs stay a consistent\n // size between modes. Scaled by dpr so they are not soft on a retina screen.\n const dpr = Math.min(2, globalThis.devicePixelRatio || 1);\n const canvas = this.#doc.createElement('canvas');\n canvas.width = Math.max(1, Math.round(THUMB_WIDTH * dpr));\n canvas.height = Math.max(1, Math.round(((THUMB_WIDTH * h) / w) * dpr));\n canvas.className = 'zine-thumbs-img';\n const ctx = canvas.getContext('2d');\n if (!ctx) return;\n try {\n ctx.drawImage(content as CanvasImageSource, 0, 0, canvas.width, canvas.height);\n } catch {\n return; // an unusable raster is not worth breaking the rail over\n }\n cell.replaceChildren(canvas);\n }\n\n /** Highlight the spread on screen and keep it scrolled into view. */\n #markCurrent(): void {\n // Key off the current page, not getSpreadIndex(): a `pageChanged` fires during the flip's\n // lead, a beat before the landing updates the book's spread index, so reading that index here\n // would highlight the spread we are leaving until the next flip. The page number is already\n // fresh at lead time, so the row that contains it is always the right one.\n const page = this.#zine.getPage();\n for (const { el, pages } of this.#rows) {\n const active = pages.includes(page);\n el.classList.toggle('zine-panel-active', active);\n el.setAttribute('aria-selected', String(active));\n if (active) el.scrollIntoView?.({ block: 'nearest' });\n }\n }\n\n destroy(): void {\n for (const off of this.#unsubscribe) off();\n this.#unsubscribe = [];\n this.#observer?.disconnect();\n this.#observer = null;\n this.#bar.destroy();\n }\n}\n","import { Sidebar, type PanelOptions } from './sidebar';\nimport type { OutlineItem } from '../source/types';\nimport type { Zine } from '../zine';\n\n/** Rail width in px. Wider than the thumbnail rail because it holds headings, not pictures. */\nconst RAIL_WIDTH = 208;\n/** Indent per nesting level, in px. */\nconst INDENT = 12;\n\n/**\n * The document's table of contents beside the book.\n *\n * One row per heading, nested entries indented under their parent. Clicking a row turns to the\n * page it points at; a heading whose destination could not be resolved is still listed, just not\n * clickable. Unlike the thumbnail rail this is a single column — headings are text, and a second\n * column would only make them narrower.\n */\nexport class Outline {\n /** The panel's themed root, so the toolbar can stamp its colour scheme. */\n readonly root: HTMLElement;\n #zine: Zine;\n #doc: Document;\n #bar: Sidebar;\n #rows: { el: HTMLElement; page: number }[] = [];\n #unsubscribe: (() => void)[] = [];\n #onDismiss?: () => void;\n\n constructor(zine: Zine, container: HTMLElement, options: PanelOptions = {}) {\n this.#zine = zine;\n const doc = container.ownerDocument!;\n this.#doc = doc;\n this.#onDismiss = options.onDismiss;\n this.#bar = new Sidebar(doc, container, {\n className: 'zine-outline',\n label: 'Outline',\n width: RAIL_WIDTH,\n onDismiss: options.onDismiss,\n rtl: zine.getDirection() === 'rtl',\n overlay: true, // floats over the book's edge on a wide screen rather than shrinking it\n pageBox: () => zine.getPageBox(),\n });\n this.root = this.#bar.root;\n\n this.#note('Loading…');\n void this.#build();\n\n this.#unsubscribe.push(zine.on('pageChanged', () => this.#markCurrent()));\n }\n\n #note(text: string): void {\n const note = this.#doc.createElement('div');\n note.className = 'zine-panel-note';\n note.textContent = text;\n this.#bar.root.replaceChildren(note);\n }\n\n async #build(): Promise<void> {\n const items = await this.#zine.getOutline();\n if (!this.#bar.root.isConnected) return; // closed while the outline was loading\n if (items.length === 0) {\n // The toolbar hides the control when there is nothing to list, so this is only reachable\n // through a hand-written layout that names 'outline' regardless.\n this.#note('This document has no outline.');\n return;\n }\n this.#bar.root.replaceChildren();\n this.#addLevel(items, 0);\n this.#markCurrent();\n }\n\n #addLevel(items: OutlineItem[], depth: number): void {\n for (const item of items) {\n const row = this.#doc.createElement('button');\n row.type = 'button';\n row.className = 'zine-outline-row';\n row.setAttribute('role', 'option');\n row.style.paddingLeft = `${8 + depth * INDENT}px`;\n row.textContent = item.title;\n row.title = item.title;\n if (item.page === null) {\n // Keep the heading visible but inert: it still tells the reader what is in the document.\n row.disabled = true;\n } else {\n const page = item.page;\n row.setAttribute('aria-label', `${item.title}, page ${page + 1}`);\n row.addEventListener('click', () => {\n this.#zine.flipTo(page);\n this.#onDismiss?.(); // clicking a heading has served its purpose; get the rail out of the way\n });\n this.#rows.push({ el: row, page });\n }\n this.#bar.root.appendChild(row);\n if (item.children.length > 0) this.#addLevel(item.children, depth + 1);\n }\n }\n\n /**\n * Mark the entry the reader is inside — the last one at or before the current page, rather\n * than an exact match, since a section spans many pages and rarely starts on the one shown.\n */\n #markCurrent(): void {\n const current = this.#zine.getPage();\n let best: HTMLElement | null = null;\n let bestPage = -1;\n for (const { el, page } of this.#rows) {\n el.classList.remove('zine-panel-active');\n el.removeAttribute('aria-selected');\n if (page <= current && page >= bestPage) {\n best = el;\n bestPage = page;\n }\n }\n if (!best) return;\n best.classList.add('zine-panel-active');\n best.setAttribute('aria-selected', 'true');\n best.scrollIntoView?.({ block: 'nearest' });\n }\n\n destroy(): void {\n for (const off of this.#unsubscribe) off();\n this.#unsubscribe = [];\n this.#bar.destroy();\n }\n}\n","import { Sidebar, type PanelOptions } from './sidebar';\nimport type { SearchHit, Zine } from '../zine';\n\n/** Rail width in px. Matches the outline: both hold text, not pictures. */\nconst RAIL_WIDTH = 208;\n/** Wait after the last keystroke before searching, so typing does not scan per character. */\nconst DEBOUNCE_MS = 180;\n/** Shorter queries match too much to be useful. */\nconst MIN_QUERY = 2;\n\n/** The query and its results, so a closed panel can be reopened where the reader left it rather\n * than blank. Held by the toolbar (which outlives the panel) and handed back on reopen. */\nexport interface SearchState {\n query: string;\n results: SearchHit[];\n}\n\n/** Panel options plus the optional state to reopen with. */\nexport interface SearchOptions extends PanelOptions {\n /** Query and results from a previous open, put back so the reader continues where they left off. */\n state?: SearchState;\n}\n\n/**\n * Full-text search beside the book: a query field over its results.\n *\n * It lives in the same rail as the thumbnails and outline rather than a popover, because results\n * are an open-ended list — a floating panel over the book would either cover the page being\n * searched or clip the very results it found.\n */\nexport class Search {\n /** The panel's themed root, so the toolbar can stamp its colour scheme. */\n readonly root: HTMLElement;\n #zine: Zine;\n #doc: Document;\n #bar: Sidebar;\n #input: HTMLInputElement;\n #hits: HTMLElement;\n /** Increments per query so a slow search that resolves late cannot overwrite a newer one. */\n #run = 0;\n #timer: ReturnType<typeof setTimeout> | null = null;\n #onDismiss?: () => void;\n /** The last query and results, kept so the toolbar can restore them on reopen. */\n #results: SearchHit[] = [];\n\n constructor(zine: Zine, container: HTMLElement, options: SearchOptions = {}) {\n this.#zine = zine;\n const doc = container.ownerDocument!;\n this.#doc = doc;\n this.#onDismiss = options.onDismiss;\n this.#bar = new Sidebar(doc, container, {\n className: 'zine-search',\n label: 'Search results',\n width: RAIL_WIDTH,\n onDismiss: options.onDismiss,\n rtl: zine.getDirection() === 'rtl',\n overlay: true, // floats over the book's edge on a wide screen rather than shrinking it\n pageBox: () => zine.getPageBox(),\n });\n this.root = this.#bar.root;\n\n this.#input = doc.createElement('input');\n this.#input.type = 'search';\n this.#input.className = 'zine-search-input';\n this.#input.placeholder = 'Search…';\n this.#input.setAttribute('aria-label', 'Search the document');\n this.#input.addEventListener('input', () => {\n if (this.#timer) clearTimeout(this.#timer);\n this.#timer = setTimeout(() => void this.#search(), DEBOUNCE_MS);\n });\n // Enter searches at once rather than waiting out the debounce.\n this.#input.addEventListener('keydown', (e) => {\n if ((e as KeyboardEvent).key !== 'Enter') return;\n if (this.#timer) clearTimeout(this.#timer);\n void this.#search();\n });\n\n this.#hits = doc.createElement('div');\n this.#hits.className = 'zine-search-hits';\n\n this.#bar.root.append(this.#input, this.#hits);\n\n // Reopened where the reader left off: put the query and its results straight back, without\n // re-scanning the document. A later keystroke supersedes them the usual way.\n const initial = options.state;\n if (initial && initial.query) {\n this.#input.value = initial.query;\n this.#run++; // any restored render belongs to this run, not a stale one\n this.#renderResults(initial.query, initial.results);\n }\n\n this.#input.focus();\n }\n\n /** The query and results as they stand, for the toolbar to stash across a close. */\n getState(): SearchState {\n return { query: this.#input.value, results: this.#results };\n }\n\n #note(text: string): void {\n const note = this.#doc.createElement('div');\n note.className = 'zine-panel-note';\n note.textContent = text;\n this.#hits.replaceChildren(note);\n }\n\n async #search(): Promise<void> {\n const query = this.#input.value.trim();\n const mine = ++this.#run;\n if (query.length < MIN_QUERY) {\n this.#results = [];\n this.#hits.replaceChildren();\n return;\n }\n this.#note('Searching…');\n const results = await this.#zine.search(query);\n if (mine !== this.#run || !this.#bar.root.isConnected) return; // superseded, or closed\n this.#renderResults(query, results);\n }\n\n /** Paint a completed search: a note when empty, one clickable row per hit otherwise. Shared by\n * a live search and the restore of a reopened panel, so both render identically. */\n #renderResults(query: string, results: SearchHit[]): void {\n this.#results = results;\n if (results.length === 0) {\n this.#note(`No matches for “${query}”`);\n return;\n }\n this.#hits.replaceChildren(\n ...results.map((hit) => {\n const row = this.#doc.createElement('button');\n row.type = 'button';\n row.className = 'zine-search-hit';\n row.setAttribute('role', 'option');\n row.setAttribute('aria-label', `Page ${hit.page + 1}: ${hit.excerpt}`);\n const label = this.#doc.createElement('span');\n label.className = 'zine-search-page';\n label.textContent = `Page ${hit.page + 1}`;\n const excerpt = this.#doc.createElement('small');\n excerpt.textContent = hit.excerpt;\n row.append(label, excerpt);\n row.addEventListener('click', () => {\n this.#zine.flipTo(hit.page);\n this.#onDismiss?.(); // jump to the hit, then get the rail out of the way\n });\n return row;\n }),\n );\n }\n\n destroy(): void {\n if (this.#timer) clearTimeout(this.#timer);\n this.#timer = null;\n this.#run++; // any search still in flight is now stale\n this.#bar.destroy();\n }\n}\n","import { createIcon, ICONS } from './icons';\nimport { applyColorScheme } from './styles';\nimport type { ControlsColorScheme } from './types';\nimport type { Zine } from '../zine';\n\n/**\n * Big page-turn arrows flanking the book.\n *\n * They sit *beside* the book rather than over it, for the same reason the docked toolbar does:\n * the container's measured box is what the renderer sizes pages from and what pointer\n * hit-testing maps into, so anything inside it would shrink the book and skew every tap. The\n * book (with whatever the toolbar already wrapped around it) goes in a flex row between them.\n *\n * An arrow disables itself at the end of the book rather than disappearing, so the layout does\n * not jump as the reader reaches the covers.\n */\nexport class Arrows {\n #zine: Zine;\n #prev: HTMLButtonElement;\n #next: HTMLButtonElement;\n #wrap: HTMLElement | null = null;\n #device: 'desktop' | 'mobile' | null;\n #unsubscribe: (() => void)[] = [];\n\n constructor(\n zine: Zine,\n container: HTMLElement,\n colorScheme: ControlsColorScheme = 'auto',\n // null: show everywhere. 'desktop'/'mobile': show only on that device; a class on the wrap\n // lets CSS hide the arrows on the other side of the layout breakpoint (see styles.ts).\n device: 'desktop' | 'mobile' | null = null,\n ) {\n this.#zine = zine;\n this.#device = device;\n const doc = container.ownerDocument!;\n\n const rtl = zine.getDirection() === 'rtl';\n // The arrows point where the page goes, so in RTL they swap: \"back\" is to the right.\n this.#prev = this.#button(doc, rtl ? ICONS.next : ICONS.prev, 'Previous page', () =>\n zine.flipPrev(),\n );\n this.#next = this.#button(doc, rtl ? ICONS.prev : ICONS.next, 'Next page', () =>\n zine.flipNext(),\n );\n\n this.#place(doc, container);\n // The wrap holds both arrow buttons; color-scheme inherits, so stamping it themes both. In\n // the parentless fallback there is no wrap, so stamp the buttons directly instead.\n if (this.#wrap) applyColorScheme(this.#wrap, colorScheme);\n else for (const btn of [this.#prev, this.#next]) applyColorScheme(btn, colorScheme);\n\n // pageChanged fires at the animation lead and carries the landed page; flipEnd is the safety\n // net for a landing that never led (reduced motion, a canceled lead). See #refresh for why the\n // page matters.\n this.#unsubscribe.push(zine.on('pageChanged', ({ page }) => this.#refresh(page)));\n this.#unsubscribe.push(zine.on('flipEnd', () => this.#refresh()));\n this.#refresh();\n }\n\n #button(\n doc: Document,\n icon: string,\n label: string,\n onClick: () => void,\n ): HTMLButtonElement {\n const btn = doc.createElement('button');\n btn.type = 'button';\n btn.className = 'zine-arrow';\n btn.title = label;\n btn.setAttribute('aria-label', label);\n btn.appendChild(createIcon(doc, icon));\n btn.addEventListener('click', onClick);\n // The book's gesture handlers sit on the container; an arrow press is not a page tap.\n for (const type of ['pointerdown', 'pointerup', 'click']) {\n btn.addEventListener(type, (e) => e.stopPropagation());\n }\n return btn;\n }\n\n #place(doc: Document, container: HTMLElement): void {\n // Wrap the outermost thing the library has already built around the book — a docked toolbar\n // or a side panel — so the arrows flank the whole assembly, not just the pages.\n const outer =\n container.closest('.zine-panel-wrap') ??\n container.closest('.zine-controls-wrap') ??\n container;\n const parent = outer.parentNode;\n if (!parent) return; // not in a document; nothing sensible to flank\n const wrap = doc.createElement('div');\n wrap.className = 'zine-arrows-wrap';\n if (this.#device) wrap.classList.add(`zine-arrows-${this.#device}`);\n parent.insertBefore(wrap, outer);\n wrap.append(this.#prev, outer, this.#next);\n this.#wrap = wrap;\n }\n\n /**\n * Hide an arrow that has nowhere to go — no back arrow on the first page.\n *\n * Hidden rather than removed so its slot stays reserved: dropping it from the flow would slide\n * the book sideways every time the reader reached a cover. `disabled` comes along for the ride\n * so the button is inert even if a consumer's own CSS makes it visible again.\n */\n #refresh(page?: number): void {\n const { prev, next } = this.#edges(page);\n for (const [btn, canTurn] of [\n [this.#prev, prev],\n [this.#next, next],\n ] as const) {\n btn.disabled = !canTurn;\n btn.classList.toggle('zine-arrow-hidden', !canTurn);\n }\n }\n\n /**\n * Which arrows have somewhere to go.\n *\n * `canFlipPrev`/`canFlipNext` read the committed spread index, which only updates once a flip\n * lands — so refreshing off them at the animation lead (when `pageChanged` fires, a few ms\n * before the commit) leaves the arrow a step behind until the fold fully settles. When the\n * event hands us the page that just landed, resolve its spread from the public spread model so\n * the arrow corrects the instant the fold reads as landed. Fall back to the committed edge state\n * when there is no page in hand (initial paint, `flipEnd`) or the page is not in any spread.\n */\n #edges(page?: number): { prev: boolean; next: boolean } {\n if (page !== undefined) {\n const spreads = this.#zine.getSpreads();\n const index = spreads.findIndex((s) => s.left === page || s.right === page);\n if (index >= 0) return { prev: index > 0, next: index + 1 < spreads.length };\n }\n return { prev: this.#zine.canFlipPrev(), next: this.#zine.canFlipNext() };\n }\n\n destroy(): void {\n for (const off of this.#unsubscribe) off();\n this.#unsubscribe = [];\n this.#prev.remove();\n this.#next.remove();\n const wrap = this.#wrap;\n if (wrap?.parentNode) {\n // The arrows are gone, so what is left is the book; put it back where the wrapper was.\n while (wrap.firstChild) wrap.parentNode.insertBefore(wrap.firstChild, wrap);\n wrap.remove();\n }\n this.#wrap = null;\n }\n}\n","import { ICONS } from './icons';\nimport { DEFAULT_ITEMS, defineControl, resolveControl } from './registry';\nimport { registerBuiltins } from './builtins';\nimport {\n activateControl,\n applyControlItemState,\n buildControlButton,\n ControlMenu,\n filterHidden,\n hasVisibleChildren,\n} from './menu';\nimport { applyColorScheme, ensureStyles } from './styles';\nimport { Thumbnails } from './thumbnails';\nimport { Outline } from './outline';\nimport { Search, type SearchState } from './search';\nimport { Arrows } from './arrows';\n\n/** The side panels, all of which share the one rail beside the book. */\nconst PANELS = { thumbnails: Thumbnails, outline: Outline, search: Search };\ntype PanelKind = keyof typeof PANELS;\n/** What every panel has in common: build itself on construction, and clean up after itself. */\ntype Panel = InstanceType<(typeof PANELS)[PanelKind]>;\nimport type {\n ControlContext,\n ControlDef,\n ControlItem,\n ControlsColorScheme,\n ControlsOptions,\n ControlsPosition,\n} from './types';\nimport type { Zine } from '../zine';\n\n/**\n * Events the container listens to for flipping, zooming and dragging. The gesture layer binds\n * these on the container and never checks `event.target`, so anything the toolbar handles has to\n * be stopped here or a button press would also register as a page tap.\n */\nconst SWALLOWED = ['pointerdown', 'pointerup', 'pointermove', 'click', 'dblclick', 'wheel', 'keydown'];\n\ninterface Popover {\n menu: ControlMenu;\n trigger: HTMLElement;\n}\n\n/**\n * Tidy the separator runs a hide-list can leave behind on the bar: drop leading and trailing\n * separators, and collapse consecutive ones to a single divider. The overflow menu drops all\n * separators anyway, so this only matters for the bar itself.\n */\nfunction collapseSeparators(items: readonly ControlItem[]): ControlItem[] {\n const out: ControlItem[] = [];\n for (const item of items) {\n if (item === '|' && (out.length === 0 || out[out.length - 1] === '|')) continue;\n out.push(item);\n }\n if (out[out.length - 1] === '|') out.pop();\n return out;\n}\n\nexport class Toolbar {\n #zine: Zine;\n #doc: Document;\n #root: HTMLElement;\n #bar: HTMLElement;\n /** Every control on screen, buttons and `render` widgets alike, so #refresh can reach them. */\n #buttons: { el: HTMLElement; def: ControlDef }[] = [];\n #popover: Popover | null = null;\n #unsubscribe: (() => void)[] = [];\n #pageInput: HTMLInputElement | null = null;\n /** The flex wrapper docked mode inserts around the container; unwound on destroy. */\n #wrap: HTMLElement | null = null;\n #container: HTMLElement;\n /** The side panel currently in the rail. Only one at a time — they share the space. */\n #panel: Panel | null = null;\n #panelKind: PanelKind | null = null;\n /** The last search's query and results, so reopening the search rail restores them rather than\n * starting blank. Panels are rebuilt on every open, so this outlives the panel that holds it. */\n #searchState: SearchState | null = null;\n /** Whether the document turned out to have any outline entries. Null until known: the answer\n * is async, and the outline control stays hidden rather than flash in and out. */\n #hasOutline: boolean | null = null;\n #arrows: Arrows | null = null;\n /** Which palette the built-in colours resolve to. Passed on to the arrows, panels and share\n * dialog, each of which mounts its own root outside this one and so must be stamped too. */\n #colorScheme: ControlsColorScheme;\n /** The edge the bar sits on, kept so a popover can open away from it. */\n #position: ControlsPosition;\n /** Control ids hidden everywhere by the top-level `hideControls` option: off the bar, out of the\n * overflow menu. Empty when nothing is hidden. */\n #hidden: ReadonlySet<string>;\n\n constructor(\n zine: Zine,\n container: HTMLElement,\n options: ControlsOptions,\n hidden: ReadonlySet<string> = new Set(),\n ) {\n this.#zine = zine;\n this.#container = container;\n this.#hidden = hidden;\n const doc = container.ownerDocument!;\n this.#doc = doc;\n ensureStyles(doc);\n\n this.#colorScheme = options.colorScheme ?? 'auto';\n const position = options.position ?? 'bottom';\n this.#position = position;\n const docked = options.docked ?? true;\n this.#root = doc.createElement('div');\n this.#root.className = [\n 'zine-controls',\n `zine-controls-${position}`,\n docked ? 'zine-controls-docked' : 'zine-controls-floating',\n options.className,\n ]\n .filter(Boolean)\n .join(' ');\n this.#root.setAttribute('role', 'toolbar');\n this.#root.setAttribute('aria-label', 'Flipbook controls');\n applyColorScheme(this.#root, this.#colorScheme);\n\n this.#bar = doc.createElement('div');\n this.#bar.className = 'zine-controls-bar';\n this.#root.appendChild(this.#bar);\n\n // Toolbar keys are handled first, then every listed event is stopped so none of it reaches\n // the book's gesture, zoom or keyboard handlers on the container.\n this.#root.addEventListener('keydown', (e) => this.#onKeyDown(e as KeyboardEvent));\n for (const event of SWALLOWED) {\n this.#root.addEventListener(event, (e) => this.#isolate(e));\n }\n this.#place(container, position, docked);\n\n const refresh = (): void => this.#refresh();\n this.#unsubscribe.push(zine.on('pageChanged', refresh));\n this.#unsubscribe.push(zine.on('zoomChanged', refresh));\n this.#unsubscribe.push(zine.on('flipEnd', refresh));\n const onDocPointer = (e: Event): void => {\n if (!this.#popover) return;\n const target = e.target as Node;\n // Leave a press on the trigger to its own click handler, which toggles the menu shut —\n // closing here first would let that click reopen it, so it would never close.\n if (this.#popover.menu.contains(target) || this.#popover.trigger.contains(target)) return;\n this.#closePopover();\n };\n doc.addEventListener('pointerdown', onDocPointer, true);\n this.#unsubscribe.push(() => doc.removeEventListener('pointerdown', onDocPointer, true));\n }\n\n /**\n * Put the toolbar in the page.\n *\n * Floating is simple: a child of the container, absolutely positioned over the book.\n *\n * Docked has to sit *outside* the container, because the container's measured box is what the\n * renderer sizes the book from and what pointer hit-testing maps into — a bar inside it would\n * shrink the book and skew every tap. So the container is wrapped in a flex column (or row),\n * with the toolbar as its sibling. The wrapper inherits the container's own layout box so the\n * arrangement the consumer's CSS set up is preserved.\n */\n #place(container: HTMLElement, position: ControlsPosition, docked: boolean): void {\n if (!docked) {\n container.appendChild(this.#root);\n return;\n }\n const parent = container.parentNode;\n if (!parent) {\n // Not in a document yet: fall back to overlaying rather than losing the toolbar.\n this.#root.classList.replace('zine-controls-docked', 'zine-controls-floating');\n container.appendChild(this.#root);\n return;\n }\n const wrap = this.#doc.createElement('div');\n wrap.className = `zine-controls-wrap zine-controls-wrap-${position}`;\n parent.insertBefore(wrap, container);\n wrap.appendChild(container);\n // 'top'/'left' put the bar first; flex-direction (set in CSS) handles the axis.\n if (position === 'top' || position === 'left') wrap.insertBefore(this.#root, container);\n else wrap.appendChild(this.#root);\n this.#wrap = wrap;\n }\n\n /** Build the layout. Separate from construction so instance-bound widgets (the page field, the\n * search panel) can be registered against this toolbar first. */\n mount(options: ControlsOptions): void {\n this.#build(options.items ?? DEFAULT_ITEMS);\n // After #place, so the arrows can wrap whatever it built around the book. A string value picks\n // the device the arrows show on; only an explicit false suppresses them entirely.\n const arrows = options.arrows ?? true;\n if (arrows !== false)\n this.#arrows = new Arrows(\n this.#zine,\n this.#container,\n this.#colorScheme,\n arrows === true ? null : arrows,\n );\n this.#refresh();\n this.#probeOutline();\n }\n\n destroy(): void {\n for (const off of this.#unsubscribe) off();\n this.#unsubscribe = [];\n this.#closePopover();\n this.#panel?.destroy();\n this.#panel = null;\n this.#panelKind = null;\n this.#root.remove();\n // Unwrap from the inside out: the arrows enclose the toolbar's wrapper, so removing theirs\n // first would leave this one orphaned outside the document.\n const wrap = this.#wrap;\n if (wrap?.parentNode) {\n wrap.parentNode.insertBefore(wrap.firstChild!, wrap);\n wrap.remove();\n }\n this.#wrap = null;\n this.#arrows?.destroy();\n this.#arrows = null;\n }\n\n /** Keep toolbar interaction from reaching the book's own gesture/zoom/keyboard handlers. */\n #isolate(event: Event): void {\n event.stopPropagation();\n }\n\n #context(close: () => void = () => this.#closePopover()): ControlContext {\n return { zine: this.#zine, close, colorScheme: this.#colorScheme };\n }\n\n #build(items: readonly ControlItem[]): void {\n for (const item of collapseSeparators(filterHidden(items, this.#hidden))) {\n if (item === '|') {\n const sep = this.#doc.createElement('div');\n sep.className = 'zine-controls-sep';\n this.#bar.appendChild(sep);\n continue;\n }\n const def = resolveControl(item);\n let el: HTMLElement;\n if (def.render) {\n el = def.render(this.#context());\n } else {\n el = buildControlButton(this.#doc, def, 'zine-controls-btn', this.#context(), (d, btn) =>\n this.#activate(d, btn),\n );\n }\n // Registered so its predicates are re-read on every change, whichever way it draws itself.\n this.#buttons.push({ el, def });\n this.#bar.appendChild(el);\n }\n }\n\n #activate(def: ControlDef, trigger: HTMLButtonElement): void {\n if (def.isDisabled?.(this.#context())) return;\n // A submenu toggles its popover; a second press on the same trigger closes it.\n if (def.children) {\n if (this.#popover?.trigger === trigger) this.#closePopover();\n else this.#openMenu(def, trigger);\n return;\n }\n activateControl(def, trigger, this.#context(), () => this.#refresh());\n }\n\n #openMenu(def: ControlDef, trigger: HTMLButtonElement): void {\n this.#closePopover();\n const menu = new ControlMenu(this.#doc, () => this.#context(), () => this.#refresh(), this.#hidden);\n menu.build(def.children ?? []);\n this.#root.appendChild(menu.el);\n this.#popover = { menu, trigger };\n trigger.setAttribute('aria-expanded', 'true');\n this.#placePopover(menu.el, trigger);\n // The entries were only just built, so nothing has evaluated their state yet: without this\n // a menu opens with every entry enabled until the next page turn happens to refresh it.\n menu.refresh();\n menu.focusFirst();\n }\n\n /** Anchor a popover to its trigger, flipped to stay inside the book. */\n #placePopover(el: HTMLElement, trigger: HTMLElement): void {\n const root = this.#root.getBoundingClientRect();\n const anchor = trigger.getBoundingClientRect();\n const box = el.getBoundingClientRect();\n if (!root.width || !box.width) return; // not laid out (e.g. jsdom): leave to CSS\n let left = anchor.left - root.left + anchor.width / 2 - box.width / 2;\n left = Math.max(4, Math.min(left, root.width - box.width - 4));\n el.style.left = `${left}px`;\n const below = anchor.bottom - root.top + 6;\n const above = anchor.top - root.top - box.height - 6;\n // A bottom bar opens its menu upward (above the button), so it never spills off the bottom\n // edge — the book above always has the room. Every other edge keeps the pick-whichever-fits\n // behaviour, preferring upward only when there is space for it.\n el.style.top = `${this.#position === 'bottom' ? above : above >= 0 ? above : below}px`;\n }\n\n #closePopover(): void {\n if (!this.#popover) return;\n const { menu, trigger } = this.#popover;\n this.#popover = null;\n trigger.removeAttribute('aria-expanded');\n const hadFocus = this.#doc.activeElement !== null && menu.contains(this.#doc.activeElement);\n // The menu owns its own items, so tearing it down keeps #refresh off detached nodes.\n menu.destroy();\n if (hadFocus) trigger.focus();\n }\n\n /** Arrow keys move between controls; Escape dismisses an open popover. */\n #onKeyDown(event: KeyboardEvent): void {\n if (event.key === 'Escape' && this.#popover) {\n const { trigger } = this.#popover;\n this.#closePopover();\n trigger.focus();\n event.preventDefault();\n return;\n }\n const focusables = [...this.#root.querySelectorAll<HTMLElement>('button, input')];\n const at = focusables.indexOf(this.#doc.activeElement as HTMLElement);\n if (at < 0) return;\n // Let the page-number field use its own arrows for text editing.\n if (this.#doc.activeElement instanceof HTMLInputElement) return;\n const step = event.key === 'ArrowRight' || event.key === 'ArrowDown' ? 1 : event.key === 'ArrowLeft' || event.key === 'ArrowUp' ? -1 : 0;\n if (step === 0) return;\n const next = focusables[(at + step + focusables.length) % focusables.length];\n next?.focus();\n event.preventDefault();\n }\n\n /**\n * Re-read every control's state after the book changes.\n *\n * Applies to custom widgets built with `render` as well as to plain buttons: the predicates\n * are on `ControlDef`, so honouring them only for button-shaped controls would make the same\n * field mean different things depending on how a control chose to draw itself. A widget may be\n * any element, so each piece of state is applied in whatever way that element supports.\n */\n #refresh(): void {\n const ctx = this.#context();\n for (const { el, def } of this.#buttons) {\n // A submenu trigger shows only while something inside it does; every other control follows\n // its own isVisible. Undefined leaves the element's display alone.\n const visible = def.children ? hasVisibleChildren(def, ctx, this.#hidden) : def.isVisible?.(ctx);\n applyControlItemState(el, def, ctx, this.#doc, visible);\n }\n // The open overflow menu owns its items, so refresh them through it.\n this.#popover?.menu.refresh();\n if (this.#pageInput && this.#doc.activeElement !== this.#pageInput) {\n this.#pageInput.value = String(this.#zine.getPage() + 1);\n }\n }\n\n /** Registered lazily by {@link registerWidgets} so the widget can reach this instance. */\n makePageInput(): HTMLElement {\n const wrap = this.#doc.createElement('div');\n wrap.className = 'zine-controls-page';\n const input = this.#doc.createElement('input');\n input.type = 'number';\n input.min = '1';\n input.max = String(this.#zine.getPageCount());\n input.value = String(this.#zine.getPage() + 1);\n input.setAttribute('aria-label', 'Page number');\n const total = this.#doc.createElement('span');\n total.textContent = `/ ${this.#zine.getPageCount()}`;\n\n // Enter commits and then blurs, and the blur commits again; a click-away blur commits on its\n // own. Guard so only the first call per editing session navigates: flipTo animates, so a\n // second commit right after would read the still-old getPage() and turn the book back to it —\n // the page that is (still) in the URL hash. Re-armed on focus for the next edit.\n let committed = false;\n const commit = (): void => {\n if (committed) return;\n committed = true;\n const n = Number(input.value);\n const last = Math.max(0, this.#zine.getPageCount() - 1);\n const target = Number.isFinite(n)\n ? Math.min(Math.max(Math.round(n) - 1, 0), last)\n : this.#zine.getPage();\n if (target !== this.#zine.getPage()) {\n this.#zine.flipTo(target); // #refresh (pageChanged/flipEnd) resyncs the field once it lands\n } else {\n input.value = String(this.#zine.getPage() + 1); // no navigation: restore the display now\n }\n };\n input.addEventListener('focus', () => {\n committed = false;\n });\n input.addEventListener('keydown', (e) => {\n if (e.key === 'Enter') {\n commit();\n input.blur();\n }\n });\n input.addEventListener('blur', commit);\n wrap.append(input, total);\n this.#pageInput = input;\n return wrap;\n }\n\n /** Which side panel is showing, if any. */\n openPanel(): PanelKind | null {\n return this.#panelKind;\n }\n\n /**\n * Whether this document has a table of contents worth offering.\n *\n * Many PDFs have none, and a control that can only report its own emptiness is a dead end, so\n * the outline button stays hidden until the outline is known to be non-empty. The first call\n * kicks off the lookup and refreshes the toolbar when it lands; `Zine.getOutline` memoizes, so\n * the panel does not pay for it twice.\n */\n hasOutline(): boolean {\n return this.#hasOutline === true;\n }\n\n /** Ask the document whether it has an outline, and refresh once the answer lands. Started as\n * the toolbar mounts so the button settles into place immediately rather than on first use. */\n #probeOutline(): void {\n if (this.#hasOutline !== null || !this.#zine.canOutline()) return;\n this.#hasOutline = false; // assume not, until we hear otherwise\n void this.#zine.getOutline().then((items) => {\n if (items.length === 0) return;\n this.#hasOutline = true;\n this.#refresh();\n // The menu may already be open with the entry missing; rebuild it so it appears.\n const popover = this.#popover;\n if (popover) {\n const def = this.#buttons.find((b) => b.el === popover.trigger)?.def;\n this.#closePopover();\n if (def) this.#openMenu(def, popover.trigger as HTMLButtonElement);\n }\n });\n }\n\n /**\n * Show one of the side panels, or close it if it is already up. They share the rail beside the\n * book, so opening one closes the others. Rebuilt each time, so the thumbnail rail always\n * matches the current spread grouping (which `spreadMode` and the responsive fallback change).\n */\n togglePanel(kind: PanelKind): void {\n const wasOpen = this.#panelKind;\n // Remember the query and results before tearing the search rail down, so its next open picks\n // up where the reader left it instead of blank. Only search carries state worth keeping;\n // thumbnails and the outline are rebuilt from the document each time by design.\n if (this.#panel instanceof Search) this.#searchState = this.#panel.getState();\n this.#panel?.destroy();\n this.#panel = null;\n this.#panelKind = null;\n if (wasOpen !== kind) {\n // Reopening the same kind toggles it shut, the same path the toolbar button takes, so the\n // scrim and Escape can dismiss the narrow-screen drawer without any new close machinery.\n this.#panel = new PANELS[kind](this.#zine, this.#container, {\n onDismiss: () => this.togglePanel(kind),\n ...(kind === 'search' && this.#searchState ? { state: this.#searchState } : {}),\n });\n applyColorScheme(this.#panel.root, this.#colorScheme);\n this.#panelKind = kind;\n }\n this.#refresh();\n }\n\n}\n\n/**\n * Controls that need the toolbar itself (a DOM widget, or a panel to open) are registered per\n * instance, since the registry is module-level and shared.\n */\nexport function registerWidgets(toolbar: Toolbar): void {\n registerBuiltins();\n defineControl({\n id: 'thumbnails',\n title: () => (toolbar.openPanel() === 'thumbnails' ? 'Hide thumbnails' : 'Show thumbnails'),\n icon: ICONS.thumbnails,\n // Documents only, for now: an image book is already a short, flat list of pictures.\n isVisible: (ctx) => ctx.zine.isDocument(),\n isActive: () => toolbar.openPanel() === 'thumbnails',\n action: (ctx) => {\n toolbar.togglePanel('thumbnails');\n ctx.close();\n },\n });\n defineControl({\n id: 'outline',\n title: () => (toolbar.openPanel() === 'outline' ? 'Hide outline' : 'Show outline'),\n icon: ICONS.outline,\n // Only when the document actually has entries to show.\n isVisible: () => toolbar.hasOutline(),\n isActive: () => toolbar.openPanel() === 'outline',\n action: (ctx) => {\n toolbar.togglePanel('outline');\n ctx.close();\n },\n });\n defineControl({\n id: 'pageInput',\n title: 'Page',\n render: () => toolbar.makePageInput(),\n });\n defineControl({\n id: 'search',\n title: () => (toolbar.openPanel() === 'search' ? 'Hide search' : 'Search'),\n icon: ICONS.search,\n isVisible: (ctx) => ctx.zine.canSearch(),\n isActive: () => toolbar.openPanel() === 'search',\n action: () => toolbar.togglePanel('search'),\n });\n}\n","import { getControl } from './registry';\nimport { registerBuiltins } from './builtins';\nimport { ControlMenu } from './menu';\nimport { applyColorScheme, ensureStyles } from './styles';\nimport type {\n ContextMenuOptions,\n ControlContext,\n ControlItem,\n ControlsColorScheme,\n} from './types';\nimport type { Zine } from '../zine';\n\n/** The controls a right-click offers by default: the reading and viewing actions that suit a\n * pointer, each hidden by its own `isVisible` when it does not apply to this book. */\nconst DEFAULT_ITEMS: readonly ControlItem[] = [\n 'zoomIn',\n 'zoomOut',\n '|',\n 'prev',\n 'next',\n '|',\n 'fullscreen',\n '|',\n 'print',\n 'download',\n 'share',\n];\n\n/** Toolbar-only controls: they need the Toolbar instance registered per-book, so a context menu\n * running without a toolbar cannot resolve them. Silently dropped rather than throwing. */\nconst TOOLBAR_ONLY = new Set(['pageInput', 'thumbnails', 'outline', 'search']);\n\n/**\n * A right-click menu of reading controls, opened at the cursor over the book.\n *\n * It reuses the toolbar's control definitions and its {@link ControlMenu} body, so the same icons\n * and actions appear whether a reader reaches them from the toolbar or a right-click. It is opt-in\n * (`contextMenu` off by default), because overriding the browser's own menu is a real cost to pay\n * only when asked. Works with `controls: false` too, which is the point: a bare book with no\n * toolbar can still offer a right-click menu.\n */\nexport class ContextMenu {\n #zine: Zine;\n #container: HTMLElement;\n #doc: Document;\n #items: readonly ControlItem[];\n #colorScheme: ControlsColorScheme;\n #hidden: ReadonlySet<string>;\n #menu: ControlMenu | null = null;\n #unbind: (() => void)[] = [];\n\n constructor(\n zine: Zine,\n container: HTMLElement,\n options: ContextMenuOptions = {},\n hidden: ReadonlySet<string> = new Set(),\n ) {\n this.#zine = zine;\n this.#container = container;\n this.#hidden = hidden;\n this.#doc = container.ownerDocument!;\n // The built-ins have to exist before the layout resolves ids; harmless if the toolbar has\n // already registered them (the registry replaces by id).\n registerBuiltins();\n ensureStyles(this.#doc);\n this.#colorScheme = options.colorScheme ?? 'auto';\n // Drop toolbar-only controls a bare book cannot resolve, so a custom list never throws for one.\n this.#items = (options.items ?? DEFAULT_ITEMS).filter(\n (item) => typeof item !== 'string' || !TOOLBAR_ONLY.has(item) || getControl(item) !== undefined,\n );\n\n const onContextMenu = (event: MouseEvent): void => {\n event.preventDefault(); // replace the browser's own menu with ours\n this.#open(event.clientX, event.clientY);\n };\n this.#container.addEventListener('contextmenu', onContextMenu);\n this.#unbind.push(() => this.#container.removeEventListener('contextmenu', onContextMenu));\n\n // Dismiss on a press anywhere outside the menu, and on Escape. Captured on the document so a\n // press on the book (which the gesture layer would otherwise swallow) still closes the menu.\n const onDocPointer = (event: Event): void => {\n if (this.#menu && !this.#menu.contains(event.target as Node)) this.#close();\n };\n const onKey = (event: KeyboardEvent): void => {\n if (event.key === 'Escape' && this.#menu) {\n this.#close();\n event.preventDefault();\n }\n };\n this.#doc.addEventListener('pointerdown', onDocPointer, true);\n this.#doc.addEventListener('keydown', onKey, true);\n this.#unbind.push(() => this.#doc.removeEventListener('pointerdown', onDocPointer, true));\n this.#unbind.push(() => this.#doc.removeEventListener('keydown', onKey, true));\n\n // Keep an open menu honest if the book changes under it (a keyboard turn, a programmatic zoom):\n // the disabled and active states are re-read, just as the toolbar does.\n const refresh = (): void => this.#menu?.refresh();\n this.#unbind.push(zine.on('pageChanged', refresh));\n this.#unbind.push(zine.on('zoomChanged', refresh));\n this.#unbind.push(zine.on('flipEnd', refresh));\n }\n\n #context(): ControlContext {\n return { zine: this.#zine, close: () => this.#close(), colorScheme: this.#colorScheme };\n }\n\n #open(clientX: number, clientY: number): void {\n this.#close();\n // On action, re-read the menu's state (a zoom-out greys once at 1x). Closing is the action's\n // own call: terminal ones (fullscreen, print, share, page ends) call ctx.close(); incremental\n // ones (page turn, zoom step) leave the menu up to be repeated, as the overflow menu does.\n const menu = new ControlMenu(\n this.#doc,\n () => this.#context(),\n () => this.#menu?.refresh(),\n this.#hidden,\n );\n menu.build(this.#items);\n // Nothing applies to this book (an image book with zoom off, say): no menu rather than an empty\n // box. The native menu is already suppressed, but an empty popup would be worse than none.\n if (!menu.el.querySelector('button')) return;\n // Fixed to the viewport so it lands under the cursor without depending on the container being a\n // positioning context; the layer isolates its own events from the book's gesture handlers.\n const layer = this.#doc.createElement('div');\n layer.className = 'zine-context-layer';\n applyColorScheme(layer, this.#colorScheme);\n layer.appendChild(menu.el);\n this.#container.appendChild(layer);\n this.#menu = menu;\n menu.refresh(); // read state before first paint, so a disabled entry shows disabled at once\n this.#place(menu.el, clientX, clientY);\n menu.focusFirst();\n }\n\n /** Pin the menu at the cursor, nudged to stay inside the book's box. */\n #place(el: HTMLElement, clientX: number, clientY: number): void {\n const rect = this.#container.getBoundingClientRect();\n const box = el.getBoundingClientRect();\n if (!box.width) {\n // Not laid out (e.g. happy-dom has no layout): just pin it at the cursor.\n el.style.left = `${clientX}px`;\n el.style.top = `${clientY}px`;\n return;\n }\n const left = Math.min(clientX, rect.right - box.width - 4);\n const top = Math.min(clientY, rect.bottom - box.height - 4);\n el.style.left = `${Math.max(rect.left + 4, left)}px`;\n el.style.top = `${Math.max(rect.top + 4, top)}px`;\n }\n\n #close(): void {\n if (!this.#menu) return;\n const layer = this.#menu.el.parentElement;\n const hadFocus = this.#doc.activeElement !== null && this.#menu.contains(this.#doc.activeElement);\n this.#menu.destroy();\n layer?.remove();\n this.#menu = null;\n // Return focus to the book so keyboard control is not stranded on a removed node.\n if (hadFocus && typeof this.#container.focus === 'function') this.#container.focus();\n }\n\n destroy(): void {\n this.#close();\n for (const off of this.#unbind) off();\n this.#unbind = [];\n }\n}\n","import { registerWidgets, Toolbar } from './toolbar';\nimport { ContextMenu } from './contextMenu';\nimport type { ContextMenuOptions, ControlsOptions } from './types';\nimport type { Zine } from '../zine';\n\nexport { defineControl, getControl, DEFAULT_ITEMS } from './registry';\nexport { ICONS, createIcon } from './icons';\nexport type {\n ContextMenuOptions,\n ControlContext,\n ControlDef,\n ControlItem,\n ControlsColorScheme,\n ControlsOptions,\n ControlsPosition,\n} from './types';\n\n/**\n * Build the toolbar inside `container` and return its teardown.\n *\n * This module is the lazy chunk `zine.ts` reaches through a dynamic `import()`, so a book built\n * with `controls: false` never downloads the toolbar, its icons or its stylesheet. The file is\n * named `controls.ts` rather than `index.ts` so the emitted chunk is `controls-<hash>.js`, which\n * is what the size-limit budget watches.\n */\nexport function mountControls(\n zine: Zine,\n container: HTMLElement,\n options: ControlsOptions = {},\n hidden: ReadonlySet<string> = new Set(),\n): () => void {\n const toolbar = new Toolbar(zine, container, options, hidden);\n // Built-ins and the instance-bound widgets have to exist before the layout resolves ids.\n registerWidgets(toolbar);\n toolbar.mount(options);\n return () => toolbar.destroy();\n}\n\n/**\n * Attach the right-click menu to `container` and return its teardown.\n *\n * Shares this lazy chunk with {@link mountControls}, so a book that wants only the context menu\n * (`controls: false`, `contextMenu: true`) still reuses the same icons and control definitions.\n */\nexport function mountContextMenu(\n zine: Zine,\n container: HTMLElement,\n options: ContextMenuOptions = {},\n hidden: ReadonlySet<string> = new Set(),\n): () => void {\n const menu = new ContextMenu(zine, container, options, hidden);\n return () => menu.destroy();\n}\n","import {\n buildSpreads,\n shouldSinglePage,\n type Direction,\n type Spread,\n type SpreadMode,\n} from './engine/spread';\nimport { Virtualizer } from './engine/virtualizer';\nimport { Emitter, type ZineEventMap } from './engine/emitter';\nimport { FlipMachine } from './engine/stateMachine';\nimport { PointerRecognizer, PinchRecognizer, bindGestures, type GestureEnd } from './engine/input';\nimport {\n bindDeepLink,\n claimHash,\n hashWithPage,\n pageFromHash,\n releaseHash,\n type DeepLinkHandle,\n} from './engine/deeplink';\nimport {\n CURL_TYPES,\n DEFAULT_CURL,\n IMPORTABLE_CURLS,\n type CurlSpec,\n type CurlType,\n} from './geometry/curls/types';\nimport { selectRenderer, type RendererOption } from './renderer/select';\nimport type { FlipDirection, PageContent, Renderer, SpreadContent } from './renderer/types';\nimport type { DownloadInfo, LoadProgress, OutlineItem, Source } from './source/types';\nimport { composeSource } from './source/compose';\nimport type { ContextMenuOptions, ControlsOptions } from './controls/types';\nimport type { LoaderHandle } from './loading/loading';\n\n/** Grab-zone size as a fraction of the smaller container dimension. */\nconst CORNER_FRACTION = 0.25;\n\n/** Wheel-zoom sensitivity: scale multiplies by exp(-deltaY * this) per wheel event. */\nconst WHEEL_ZOOM_SENSITIVITY = 0.0015;\n\n/** Pointer movement (px) beyond which a press becomes a drag rather than a tap. */\nconst DRAG_THRESHOLD = 6;\n\n/** Window (ms) a single click waits to rule out a double-click before flipping. Also the window\n * within which the library pairs two clicks into its own double-click (see #bindDoubleClickZoom). */\nconst DOUBLE_CLICK_MS = 250;\n\n/** How far (px) the second click of a pair may land from the first and still count as the same\n * spot. Forgiving enough for a wobble, tight enough that two different zones don't pair. */\nconst DOUBLE_CLICK_MOVE = 24;\n\n/** Grace period after a flip lands during which a paired double-click in a *dead* edge zone (the\n * first/last spread, where the turn can't happen) is read as the tail of a rapid flipping streak\n * and swallowed rather than zooming. Two streak clicks pair within DOUBLE_CLICK_MS of each other,\n * so two of them span this window; a deliberate zoom comes after the reader pauses on the end\n * page, well outside it. */\nconst FLIP_STREAK_MS = 2 * DOUBLE_CLICK_MS;\n\n/** A lone page fills the container, so it sweeps the full width where a spread leaf only covers\n * its half. `flipDuration` is the spread timing; lone pages stretch it slightly so twice the\n * travel still reads at a comparable speed instead of whipping across. */\nconst LONE_PAGE_FLIP_SCALE = 1.2;\n\n/** Characters of surrounding text shown on either side of a search match. */\nconst EXCERPT_PAD = 32;\n\n/** Quiet period before re-rasterizing at a new zoom, so a pinch does not rasterize every frame. */\nconst ZOOM_TILE_DELAY = 60;\n\n/** Ceiling on the zoomed page raster. Memory grows with the square of this, so a letter page at\n * 4x would run past 100 MB; 2.5x keeps a deep zoom far sharper than fit-to-screen for a\n * fraction of that. */\nconst MAX_PAGE_UPGRADE = 2.5;\n\n/** Raster texels we want per CSS px for crisp text (roughly retina density). A raster matched 1:1\n * to CSS px looks soft: glyph antialiasing is baked at exactly the on-screen size and then\n * bilinear-sampled under a fractional transform, with nothing to downsample. Rendering ~2x and\n * letting the GPU minify supersamples the edges. A retina display already paints its raster at\n * ~2x CSS px, so this floor is scaled by 1/dpr and only lifts low-dpr screens. */\nconst TEXT_SUPERSAMPLE = 1.5;\n\n/** Flip time under `prefers-reduced-motion`. Short enough not to read as animation, long enough\n * to show which way the page went — an instant swap leaves the reader guessing. */\nconst REDUCED_MOTION_DURATION = 120;\n\n/** Eased fold pose (0..1) at which the page number is announced, while the fold keeps animating to\n * its full duration. The eases decelerate into the finish, so by this pose the turn already reads\n * as landed; announcing here updates the number on time without shortening the visible flip. This\n * is a pure timing lead for the number: it does not touch the animation clock or the end pose. */\nconst PAGE_LEAD = 0.85;\n\n/** Lone-page turns dissolve rather than land on a facing half; announce a touch earlier than\n * a spread so the number updates as the under-page takes over, not after the fade tail. */\nconst PAGE_LEAD_LONE = 0.82;\n\n/** How far the corner peek hint lifts the leading page (fold progress 0..1) before settling back —\n * enough to read as a liftable page, well short of committing a turn. */\nconst PEEK_PROGRESS = 0.12;\n\n/** Duration of each leg (out, then back) of the corner peek, in ms. */\nconst PEEK_LEG_MS = 360;\n\n/** How far the whole spread slides toward the edge on a dead boundary tap, in px, before it springs\n * back — enough to read as \"nothing past here\", well short of looking like a turn. */\nconst EDGE_NUDGE_PX = 28;\n\n/** Duration of the boundary rubber-band nudge (out and back together), in ms. */\nconst EDGE_NUDGE_MS = 260;\n\n/** How long the reader may sit on the opening spread without turning a page before the peek replays\n * once as a nudge. Long enough not to interrupt someone who is simply reading the first page. */\nconst IDLE_NUDGE_MS = 7000;\n\n/** How long the first-zoom \"drag to move\" caption stays up before fading on its own. */\nconst CAPTION_MS = 2200;\n\n/** localStorage key for learned gestures under `hints.persist`. Shared across books on a site so a\n * gesture learned on one is not re-taught on the next. */\nconst HINTS_STORAGE_KEY = 'zine:hints-learned';\n\n/** Which gestures the reader has demonstrated, so the matching discoverability hint stays quiet. */\ninterface Learned {\n turn: boolean;\n zoom: boolean;\n pan: boolean;\n}\n\n/** One page that matched a {@link Zine.search} query. */\nexport interface SearchHit {\n /** Zero-based page index; pass straight to `flipTo`. */\n page: number;\n /** The match with a little text either side, ellipsised where it was cut. */\n excerpt: string;\n}\n\n/** Pull a readable snippet around a match, collapsing the whitespace PDFs are full of. */\nfunction excerptAround(text: string, at: number, length: number): string {\n const start = Math.max(0, at - EXCERPT_PAD);\n const end = Math.min(text.length, at + length + EXCERPT_PAD);\n const slice = text.slice(start, end).replace(/\\s+/g, ' ').trim();\n return `${start > 0 ? '…' : ''}${slice}${end < text.length ? '…' : ''}`;\n}\n\ninterface DragState {\n direction: FlipDirection;\n targetIndex: number;\n toPage: number;\n toContent: SpreadContent;\n width: number;\n t: number;\n}\n\nexport interface ZoomOptions {\n /** Whether zooming is allowed at all; default true. */\n enabled?: boolean;\n /** Maximum zoom scale; default 4. */\n max?: number;\n /** Zoom on Ctrl/⌘ + wheel (also how trackpad pinch arrives on desktop); default true. */\n wheel?: boolean;\n /** Zoom levels cycled by double-click (wraps to the first); `false` disables. Default [1, 2, 4]. */\n doubleClick?: number[] | false;\n /**\n * Listen for double-click zoom inside the click-to-flip zones. Default: off in 'edge' mode\n * (edges flip instantly, no delay), on in 'half' mode. Turning it on in 'edge' mode makes\n * clicks wait out `clickFlipDelay` so a double-click can preempt them with a zoom.\n */\n doubleClickInFlipZone?: boolean;\n}\n\nexport interface ZineOptions {\n /** Content source (e.g. an ImageSource). Required. */\n source: Source;\n /** Renderer selection; default 'auto'. */\n renderer?: RendererOption;\n /**\n * Page-curl model for the WebGL2 renderer. Default `'cone'`.\n *\n * `'cone'` and `'simple'` are bundled and can be named directly. The others are imported and\n * passed as models, so a book that never uses them never carries their math:\n *\n * ```ts\n * import { silk } from '@zinejs/core/curls';\n * new Zine(el, { source, curl: silk });\n * ```\n *\n * Any object of the same shape works, so a custom curl needs no registration.\n */\n curl?: CurlSpec;\n /** Fixed container width in px; omit to let the container/CSS drive the size. */\n width?: number;\n /** Fixed container height in px; omit to let the container/CSS drive the size. */\n height?: number;\n /** Reading direction; default 'ltr'. */\n direction?: Direction;\n /** How pages group into spreads: 'double' | 'single' | 'cover' | 'book'. Default 'cover'. */\n spreadMode?: SpreadMode;\n /** Image URL prepended as a lone front cover (adds a page). */\n frontCover?: string;\n /** Image URL appended as a lone back cover (adds a page). */\n backCover?: string;\n /** Replace source pages with image URLs, keyed by 0-based source index (negative = from the end). */\n pages?: Record<number, string>;\n /** Page to open on; default 0. */\n startPage?: number;\n /** Flip animation duration in ms; default 500. */\n flipDuration?: number;\n /** How a tap/click turns pages: near an edge ('edge'), by page half ('half'), or 'off'. Default 'edge'. */\n clickToFlip?: 'edge' | 'half' | 'off';\n /** Edge-zone size in px per side, used when clickToFlip is 'edge'. Default 64. */\n clickZoneSize?: number;\n /**\n * On a mouse, change the cursor over the book to hint what a press would do: `pointer` where a\n * click turns the page, `zoom-in` where a double-click zooms (including the edge peel band, so a\n * sweep stays calm), `grab` over the whole page once zoomed (a drag pans), and `grabbing` while a\n * peel or pan is actually in flight. Default true. Set false when a changing cursor would be noise\n * in the embedding design. No effect on touch.\n */\n cursorHints?: boolean;\n /**\n * Subtle, one-shot discoverability hints shown on the book itself (so they reach touch readers,\n * unlike {@link cursorHints}): the leading page corner peeks and settles on first open, nudges\n * once more if the reader sits idle without turning a page, a small \"drag to move\" caption appears\n * the first time they zoom in, and (on a mouse) a \"double-click or Ctrl/⌘-scroll to zoom\" caption\n * appears if a lone click lands in a dead zone where only a double-click would do anything. Each\n * hint stops for good the moment the reader performs the gesture it teaches. Honors\n * `prefers-reduced-motion` (the motion cues are skipped). Default true. Pass an object to keep what\n * the reader has learned across visits: `{ persist: true }` records it in localStorage so a\n * returning reader is not re-taught (default is in-memory, per page load).\n */\n hints?: boolean | { enabled?: boolean; persist?: boolean };\n /**\n * Delay (ms) a click waits before flipping, so a double-click can preempt it with a zoom.\n * Omit for auto: 0 when double-click zoom is inactive, 250 when it's active.\n */\n clickFlipDelay?: number;\n /** Zoom behavior. */\n zoom?: ZoomOptions;\n /** Container widths below this (px) switch to one page per spread; default 640. */\n singlePageThreshold?: number;\n /**\n * Whether a narrow container may override `spreadMode` and show one page at a time. Default\n * true.\n *\n * Set false to hold the configured mode at every width — a two-page spread stays two pages on\n * a phone, however small. (`singlePageThreshold: 0` has the same effect; this says it out\n * loud, and can be changed at runtime with {@link Zine.setResponsiveSpread}.)\n */\n responsiveSpread?: boolean;\n /**\n * The built-in toolbar. Shown by default; pass `false` to render none, or an object to choose\n * its position and which controls appear.\n */\n controls?: boolean | ControlsOptions;\n /**\n * The built-in loading indicator, shown while a source that reports download progress (a PDF\n * from a URL) fetches and prepares its first spread. On by default; pass `false` to render none\n * and drive your own from the `progress` event and the `ready` promise. Retheme the default with\n * the `--zine-loading-bg`, `--zine-loading-fg`, `--zine-loading-accent`, and `--zine-loading-track`\n * CSS variables.\n */\n loading?: boolean;\n /**\n * Keep the current page in the URL hash (`#page=12`), so a link opens where the reader was and\n * back/forward move through the book. Default true.\n *\n * Only a `page` key is read or written — anything else in the hash is left alone — and updates\n * use `replaceState`, so turning pages does not fill the back button. Pass `false` if your app\n * owns the hash.\n */\n deepLink?: boolean;\n /**\n * Suppress the browser's right-click menu over the book. Default false.\n *\n * A mild deterrent for published documents, not protection: the pages are still in the DOM and\n * reachable by anyone who wants them. It also takes away Inspect and \"Open image in new tab\"\n * for everyone, so it is off unless asked for.\n *\n * Mutually exclusive with {@link contextMenu}: one replaces the native menu, the other only\n * removes it, so enabling both is a contradiction and throws.\n */\n disableContextMenu?: boolean;\n /**\n * A right-click menu of reading controls (zoom, page turns, fullscreen, print, download, share),\n * opened at the cursor over the book. On by default. Over a canvas the native menu offers only\n * the generic page actions (Back, Reload, View source), so replacing it with the book's own\n * controls is the better trade; pass `false` to keep the browser's menu.\n *\n * Reuses the toolbar's icons and actions, so the same controls appear whether reached from the\n * toolbar or a right-click, and it works even with `controls: false` — a bare book can still\n * offer one. `true` uses the default set; an object with `items` replaces the layout. Mouse\n * affordance only; touch never raises it.\n *\n * Turned off automatically when {@link disableContextMenu} is set (that option removes the native\n * menu with nothing in its place); setting both explicitly throws.\n */\n contextMenu?: boolean | ContextMenuOptions;\n /**\n * Control ids to hide from both surfaces at once: off the toolbar (bar and overflow menu) and out\n * of the right-click menu. `['print', 'download']` removes those two everywhere, wherever they are\n * nested, without having to edit `controls.items` and `contextMenu.items` separately.\n *\n * A blunt cross-cutting filter: it only removes, and it applies to whatever layout each surface\n * ends up with (default or custom). To rearrange or add controls, set the per-surface `items`\n * instead.\n */\n hideControls?: readonly string[];\n}\n\n/**\n * The public flipbook. Composes the engine (spreads, virtualizer, events), a\n * lazily-selected renderer, and a content source. Construction is synchronous\n * but setup is async — await `ready` (or listen for the `ready` event) before\n * driving it.\n */\nexport class Zine {\n #container: HTMLElement;\n #source: Source;\n #emitter = new Emitter<ZineEventMap>();\n #virtualizer = new Virtualizer(2);\n #machine = new FlipMachine();\n #direction: Direction;\n #spreadMode: SpreadMode;\n /** The mode the book was built with, so toggling away from one page can return to it — a\n * `cover` book comes back as `cover`, keeping its lone first page, not as plain `double`. */\n #configuredMode: SpreadMode;\n #curl: CurlSpec;\n #anchorY = 1; // where the last tap/drag grabbed (0=top, 1=bottom); drives anchored curls\n #clickToFlip: 'edge' | 'half' | 'off';\n #clickZoneSize: number;\n #cursorHints: boolean;\n /** Last mouse position over the container in client px, so the cursor can be re-derived after a\n * page turn, zoom, or resize changes what a press there would do — without the pointer moving. */\n #pointerClient: { x: number; y: number } | null = null;\n #unbindCursor: (() => void) | null = null;\n #hintsEnabled: boolean;\n #hintsPersist: boolean;\n /** What the reader has demonstrated they already know, so a hint teaching it stays quiet. Set by\n * the gesture itself (a turn, a zoom, a pan); persisted to localStorage when `hints.persist`. */\n #learned: Learned = { turn: false, zoom: false, pan: false };\n /** The one-shot idle nudge timer (armed on ready, cleared by any real gesture). */\n #idleTimer: ReturnType<typeof setTimeout> | null = null;\n /** Tags the peek's own rAF loop so a real flip (or teardown) can cancel a peek mid-play without\n * the stale frame repainting a half-folded page. Distinct from #activeAnim, which is flip-only. */\n #peekAnim: object | null = null;\n /** Tags the boundary rubber-band's rAF loop, so a real flip/drag/zoom (or teardown) cancels a nudge\n * in flight without a stale frame leaving the spread shifted off-center. Shares #raf, like #peekAnim. */\n #nudgeAnim: object | null = null;\n /** The lazily-built hint caption element (null until a caption shows it). */\n #captionEl: HTMLElement | null = null;\n #captionTimer: ReturnType<typeof setTimeout> | null = null;\n /** Delays the \"double-click to zoom\" caption past the pairing window, so a real double-click zooms\n * (which teaches zoom) instead of flashing the hint first. Cleared if the click pairs or zoom is learned. */\n #zoomHintTimer: ReturnType<typeof setTimeout> | null = null;\n /** The zoom caption teaches at most once a session; a reader who ignores it is not nagged on every\n * dead click. (The peek and pan captions are one-shot for the same reason.) */\n #zoomHintShown = false;\n /** pointerType of the last press, so the zoom caption stays mouse-only: touch has no \"double-click\"\n * and double-tap/pinch are already well known (this mirrors why cursorHints is mouse-only). */\n #lastPointerType = '';\n #honorDoubleClickInFlipZone: boolean;\n #clickFlipDelayValue: number;\n #singlePageThreshold: number;\n #singlePage = false;\n /** Last layout announced via `spreadChanged`, so a rebuild that changes nothing stays quiet. */\n #effectiveModeShown: SpreadMode | null = null;\n #narrow = false; // responsive: container currently below singlePageThreshold\n #responsiveSpread: boolean;\n #lastAspect = ''; // last container aspect-ratio written (avoids redundant style writes)\n #startPageOption: number | undefined;\n #deepLinkEnabled: boolean;\n #disableContextMenu: boolean;\n #deepLink: DeepLinkHandle | null = null;\n #unsubscribeDeepLink: (() => void) | null = null;\n #spreads: Spread[] = [];\n #current = 0;\n #currentPage = 0;\n #currentContent: SpreadContent = { left: null, right: null };\n #flipDuration: number;\n #zoomEnabled: boolean;\n #wheelZoom: boolean;\n #doubleClickLevels: number[] | null;\n #maxZoom: number;\n #resizeObserver: ResizeObserver | null = null;\n #updateScheduled = false;\n #scale = 1;\n #tx = 0;\n #ty = 0;\n #renderer: Renderer | null = null;\n #fellBack = false;\n #destroyed = false;\n #raf: number | null = null;\n #drag: DragState | null = null;\n #pendingGrab: { direction: FlipDirection; targetIndex: number; toPage: number; width: number } | null = null;\n #press: { x: number; y: number } | null = null;\n #pendingClickTimer: ReturnType<typeof setTimeout> | null = null;\n // Latest flip intent requested while one was already animating; replayed on settle so\n // clicks/keys during a turn aren't dropped and continuous flipping keeps advancing.\n #queuedFlip: (() => void) | null = null;\n // The fold animation currently on screen, so a new flip request can land it instantly\n // (snap to its end pose + commit) and start immediately instead of waiting it out.\n #activeAnim: { toT: number; direction: FlipDirection; onDone: () => void } | null = null;\n // Bumped per flip so an async #runFlip that resumes after being superseded can bail.\n #flipGeneration = 0;\n #pan: { baseTx: number; baseTy: number } | null = null;\n #pinching = false;\n #pinchBaseScale = 1;\n #reducedMotion = false;\n #liveRegion: HTMLElement | null = null;\n #unbindInput: (() => void) | null = null;\n #unbindWheel: (() => void) | null = null;\n #unbindContextMenu: (() => void) | null = null;\n #unbindDblClick: (() => void) | null = null;\n /** First click of a possible library-detected double-click, and when/where it landed. Kept\n * because the browser's own `dblclick` misfires on rapid streaks (the counter resets). */\n #lastClick: { t: number; x: number; y: number } | null = null;\n /** When the last flip committed (performance.now()). A dead-edge double-click landing soon after\n * is the tail of a flipping streak, not a zoom request; see #zoomAt and FLIP_STREAK_MS. */\n #lastFlipAt = Number.NEGATIVE_INFINITY;\n #a11yCleanup: (() => void) | null = null;\n /** Resolved once and reused; see {@link getOutline}. */\n #outline: Promise<OutlineItem[]> | null = null;\n /** Pages whose content improved while a flip was running, to repaint once it settles. */\n #pendingUpgrades = new Set<number>();\n #controlsOption: boolean | ControlsOptions;\n #controlsCleanup: (() => void) | null = null;\n #contextMenuOption: boolean | ContextMenuOptions;\n #contextMenuCleanup: (() => void) | null = null;\n /** Control ids hidden from every surface by {@link ZineOptions.hideControls}. Empty when unset. */\n #hiddenControls: ReadonlySet<string>;\n #loadingOption: boolean;\n #loader: LoaderHandle | null = null;\n #lastProgress: LoadProgress | null = null;\n /** 'download' while bytes arrive, 'preparing' while the first spread rasterizes, 'done' after\n * it paints. Guards a late-arriving loader chunk from mounting once the book is already up. */\n #loaderPhase: 'download' | 'preparing' | 'done' = 'download';\n /** Resolution multiplier the current spread is rasterized at; 1 while not zoomed. */\n #zoomedAt = 1;\n /** Guards against an out-of-date tile landing after the view has already moved on. */\n #tileGeneration = 0;\n #tileTimer: ReturnType<typeof setTimeout> | null = null;\n #ready: Promise<void>;\n\n /** Stable seek API: drive the fold to a fixed progress without animating (visual regression). */\n readonly debug = {\n setFlipProgress: (t: number, direction: FlipDirection): void => {\n this.#renderer?.setFlipProgress(t, direction);\n },\n };\n\n constructor(container: HTMLElement, options: ZineOptions) {\n validateOptions(container, options);\n this.#container = container;\n if (options.width !== undefined) container.style.width = `${options.width}px`;\n if (options.height !== undefined) container.style.height = `${options.height}px`;\n this.#source = composeSource(options.source, {\n frontCover: options.frontCover,\n backCover: options.backCover,\n pages: options.pages,\n });\n const direction = options.direction ?? 'ltr';\n this.#direction = direction;\n // Set before the spread model is built: that is where a page in the URL is honoured.\n // Claimed before the spread model is built, which is where the hash is read: a second book\n // on the same page must not open on the first one's page.\n this.#deepLinkEnabled = (options.deepLink ?? true) && claimHash();\n this.#disableContextMenu = options.disableContextMenu ?? false;\n this.#spreadMode = options.spreadMode ?? 'cover';\n this.#configuredMode = this.#spreadMode;\n this.#curl = options.curl ?? DEFAULT_CURL;\n this.#clickToFlip = options.clickToFlip ?? 'edge';\n this.#clickZoneSize = options.clickZoneSize ?? 64;\n this.#cursorHints = options.cursorHints ?? true;\n const hints = options.hints ?? true;\n this.#hintsEnabled = hints === true || (hints !== false && (hints.enabled ?? true));\n this.#hintsPersist = hints !== true && hints !== false && (hints.persist ?? false);\n if (this.#hintsPersist) this.#loadLearned();\n this.#singlePageThreshold = options.singlePageThreshold ?? 640;\n this.#responsiveSpread = options.responsiveSpread ?? true;\n this.#controlsOption = options.controls ?? true;\n // On by default, but a book that asked to suppress the native menu with nothing in its place\n // (disableContextMenu) means it: don't hand it our menu unasked. Setting both explicitly is a\n // contradiction and throws in validateOptions.\n this.#contextMenuOption = options.contextMenu ?? (options.disableContextMenu ? false : true);\n this.#hiddenControls = new Set(options.hideControls ?? []);\n this.#loadingOption = options.loading ?? true;\n this.#startPageOption = options.startPage;\n // Sync sources (a known page count) build spreads now — so bad pageCount/startPage\n // throw immediately from `new Zine`. Async sources (an `open()`) defer to #init.\n if (typeof this.#source.open !== 'function') {\n this.#buildSpreadModel();\n }\n this.#flipDuration = options.flipDuration ?? 800;\n this.#zoomEnabled = options.zoom?.enabled ?? true;\n this.#wheelZoom = options.zoom?.wheel ?? true;\n const dbl = options.zoom?.doubleClick;\n const levels = dbl === false ? [] : [...(dbl ?? [1, 2, 4])].sort((a, b) => a - b);\n this.#doubleClickLevels = levels.length > 0 ? levels : null;\n this.#maxZoom = options.zoom?.max ?? 4;\n\n // Arbitrate click-to-flip vs double-click zoom inside the flip zones.\n // Listen for double-click zoom in flip zones: default off in 'edge', on in 'half'.\n // clickFlipDelay === 0 hard-disables it. When honored, a delay lets a click wait\n // out a possible double-click; auto default 250 ms, overridable.\n const doubleClickZoomActive = this.#zoomEnabled && this.#doubleClickLevels !== null;\n const listenInFlipZone = options.zoom?.doubleClickInFlipZone ?? (this.#clickToFlip === 'half');\n this.#honorDoubleClickInFlipZone =\n this.#clickToFlip !== 'off' &&\n doubleClickZoomActive &&\n listenInFlipZone &&\n options.clickFlipDelay !== 0;\n this.#clickFlipDelayValue = this.#honorDoubleClickInFlipZone\n ? (options.clickFlipDelay ?? DOUBLE_CLICK_MS)\n : 0;\n\n this.#ready = this.#init(options.renderer ?? 'auto');\n }\n\n /** Resolves once the renderer is mounted and the first spread is painted. */\n get ready(): Promise<void> {\n return this.#ready;\n }\n\n getPageCount(): number {\n return this.#source.pageCount;\n }\n\n getPage(): number {\n return this.#currentPage;\n }\n\n /** The element this flipbook was mounted into. */\n get container(): HTMLElement {\n return this.#container;\n }\n\n /**\n * Where the book is actually painted inside the container, in container pixels: the\n * aspect-fitted, centred region, which is shorter (or narrower) than the container whenever the\n * two aspect-ratios do not match. A side rail uses it to stand exactly as tall as the page rather\n * than the whole container. Null before the first paint, when there is nothing to measure yet.\n */\n getPageBox(): { x: number; y: number; width: number; height: number } | null {\n return this.#renderer?.measure().book ?? null;\n }\n\n /**\n * How pages are currently grouped, one entry per spread — the same model the renderer paints,\n * so it already reflects `spreadMode` and the responsive single-page fallback. Useful for\n * building a page list or thumbnail rail that matches the book.\n */\n getSpreads(): readonly Spread[] {\n return this.#spreads;\n }\n\n /** The spread index currently on screen; indexes into {@link getSpreads}. */\n getSpreadIndex(): number {\n return this.#current;\n }\n\n /** Reading direction, which mirrors each spread's left/right sides. */\n getDirection(): Direction {\n return this.#direction;\n }\n\n /** How pages are grouped: the configured mode, not what a narrow container may be forcing. */\n getSpreadMode(): SpreadMode {\n return this.#spreadMode;\n }\n\n /** Whether the book is showing one page at a time, for whatever reason. */\n isSinglePage(): boolean {\n return this.#singlePage;\n }\n\n /**\n * Regroup the pages, keeping the reader on the page they are looking at.\n *\n * A narrow container may still override this with one page at a time; see\n * {@link setResponsiveSpread}.\n */\n setSpreadMode(mode: SpreadMode): void {\n if (mode === this.#spreadMode) return;\n this.#spreadMode = mode;\n // The narrow measurement was taken under the layout we are leaving, and a book showing one\n // page is narrower than the same book showing two. Holding on to it would let a container\n // that only shrank *because* of one-page mode veto the way back out. Drop it and let the\n // next measurement decide afresh.\n this.#narrow = false;\n this.#rebuildSpreads();\n void this.#renderCurrent();\n }\n\n /**\n * Switch between one page and two, and back again.\n *\n * Returning to two pages restores the mode the book was built with, so a `cover` book gets its\n * lone first page back rather than becoming a plain `double`.\n */\n toggleSpreadMode(): void {\n this.setSpreadMode(\n this.#spreadMode === 'single'\n ? this.#configuredMode === 'single'\n ? 'double' // built as single: there is no other mode to return to\n : this.#configuredMode\n : 'single',\n );\n }\n\n /** Whether a narrow container is allowed to override `spreadMode` with one page at a time. */\n getResponsiveSpread(): boolean {\n return this.#responsiveSpread;\n }\n\n /**\n * Whether the book is showing one page at a time *because the container is narrow*, rather\n * than because it was asked to. False when `responsiveSpread` is off.\n *\n * A book already set to `single` is not being overridden, however narrow it is: asking for one\n * page makes the book itself narrower, so reporting that as an override would strand the reader\n * in a layout they could no longer switch out of.\n */\n isResponsiveSingle(): boolean {\n return this.#narrow && this.#spreadMode !== 'single';\n }\n\n /**\n * Allow or forbid the narrow-container override, and re-lay out at once.\n *\n * Turning it off holds the configured `spreadMode` at every width; turning it back on\n * re-measures, so a book on a narrow screen collapses to single again straight away.\n */\n setResponsiveSpread(enabled: boolean): void {\n if (enabled === this.#responsiveSpread) return;\n this.#responsiveSpread = enabled;\n if (!enabled) {\n if (!this.#narrow) return; // nothing was being overridden\n this.#narrow = false;\n this.#rebuildSpreads();\n } else {\n const width = this.#renderer?.measure().containerWidth;\n if (width === undefined) return;\n this.#narrow = false; // let #applySinglePage see a real change and rebuild\n this.#applySinglePage(width);\n }\n void this.#renderCurrent();\n }\n\n /**\n * Resolve one page's raster, for drawing a thumbnail or exporting. Returns null if the page\n * cannot be decoded — the failure is reported through `sourceError` rather than thrown, as it\n * is for the pages the renderer paints.\n */\n getPageImage(index: number): Promise<PageContent | null> {\n return this.#getPage(index);\n }\n\n /** Whether there is a spread after the current one (false on the last). */\n canFlipNext(): boolean {\n return this.#current + 1 < this.#spreads.length;\n }\n\n /** Whether there is a spread before the current one (false on the first). */\n canFlipPrev(): boolean {\n return this.#current > 0;\n }\n\n /** The ceiling `setZoom` clamps to. */\n getMaxZoom(): number {\n return this.#maxZoom;\n }\n\n /** Whether this book's source can produce text — false for image books. */\n canSearch(): boolean {\n return typeof this.#source.getText === 'function';\n }\n\n /** Whether the original document can be downloaded — true for a PDF from a URL or bytes. */\n canDownload(): boolean {\n return typeof this.#source.getDownload === 'function';\n }\n\n /**\n * Whether the book is a document rather than a set of loose images — true for a PDF, false for\n * an `ImageSource`. Document-shaped features (the page rail, the outline) key off this.\n *\n * Inferred from the source implementing document-only capabilities, so a custom source opts in\n * simply by implementing them.\n */\n isDocument(): boolean {\n return typeof this.#source.getOutline === 'function' || typeof this.#source.getText === 'function';\n }\n\n /**\n * Whether this book can be printed: the source must have an original document, and the browser\n * must render PDFs inline. {@link print} loads the file into an offscreen iframe and drives it\n * with `window.print()`, which needs a built-in PDF viewer; Android Chrome has none, so the\n * frame never lays out and the print silently does nothing. `pdfViewerEnabled` reports exactly\n * that capability, so hide the control when it is explicitly false. It is only hidden on that\n * explicit false — a browser too old to report the property (undefined) keeps the button.\n */\n canPrint(): boolean {\n if (typeof this.#source.getDownload !== 'function') return false;\n const nav = this.#container.ownerDocument?.defaultView?.navigator;\n return nav?.pdfViewerEnabled !== false;\n }\n\n /**\n * Print the document.\n *\n * The original file is handed to the browser in an offscreen frame rather than printing the\n * host page: printing the page would capture the toolbar and whatever single spread happens to\n * be on screen, while the browser paginates a PDF properly on its own. Resolves false when\n * there is no original to print — an image book, or a caller-owned pdf.js document.\n */\n async print(): Promise<boolean> {\n const info = await this.getSourceFile();\n const doc = this.#container.ownerDocument;\n if (!info || !doc) return false;\n\n // A cross-origin PDF loads into the frame but the frame is then cross-origin, so calling\n // print() on its window throws and the dialog never opens. Pull it into a same-origin blob so\n // the frame is scriptable — the same fetch the download path uses.\n const { url, revoke } = await this.#sameOriginFile(info, doc);\n\n const frame = doc.createElement('iframe');\n // Offscreen rather than display:none: a hidden frame does not always lay out its document,\n // and an unlaid-out frame has nothing to print.\n frame.style.cssText =\n 'position:fixed;right:0;bottom:0;width:1px;height:1px;opacity:0;border:0;';\n frame.setAttribute('aria-hidden', 'true');\n frame.src = url;\n\n return new Promise<boolean>((resolve) => {\n let settled = false;\n const finish = (ok: boolean): void => {\n if (settled) return;\n settled = true;\n // Outlive the print dialog, which is modal and synchronous in some browsers; removing\n // the frame while it is open cancels the job.\n setTimeout(() => {\n frame.remove();\n if (revoke) URL.revokeObjectURL(url);\n }, 60_000);\n resolve(ok);\n };\n frame.addEventListener('load', () => {\n try {\n const win = frame.contentWindow;\n if (!win) return finish(false);\n win.focus();\n win.print();\n finish(true);\n } catch {\n finish(false); // a cross-origin PDF cannot be driven from here\n }\n });\n frame.addEventListener('error', () => finish(false));\n doc.body.appendChild(frame);\n });\n }\n\n /** Whether the source can supply a table of contents. True for PDFs; the document may still\n * turn out to have no outline, in which case {@link getOutline} resolves empty. */\n canOutline(): boolean {\n return typeof this.#source.getOutline === 'function';\n }\n\n /**\n * The document's table of contents, or an empty list when it has none.\n *\n * Memoized: an outline does not change for the life of the document, and resolving one means\n * following every entry's destination to a page. The toolbar asks for it up front to decide\n * whether to offer the outline panel at all, and the panel itself then gets it for free.\n */\n getOutline(): Promise<OutlineItem[]> {\n if (!this.#outline) {\n const getOutline = this.#source.getOutline;\n this.#outline =\n typeof getOutline === 'function'\n ? Promise.resolve(getOutline.call(this.#source)).catch(() => [])\n : Promise.resolve([]);\n }\n return this.#outline;\n }\n\n /**\n * Save the original document. Resolves to false when the source has nothing to hand over\n * (an image book, or a PDF opened from a caller-owned pdf.js document).\n */\n /**\n * Where the original document lives, if the source has one — a PDF's URL or a blob of its\n * bytes. Null for an image book, or a PDF opened from a caller-owned pdf.js document.\n *\n * Saving and printing both start here: the original is better than anything reassembled from\n * page rasters, and the browser already knows how to paginate it.\n */\n async getSourceFile(): Promise<DownloadInfo | null> {\n const getDownload = this.#source.getDownload;\n if (typeof getDownload !== 'function') return null;\n return (await getDownload.call(this.#source)) ?? null;\n }\n\n async download(): Promise<boolean> {\n const info = await this.getSourceFile();\n if (!info) return false;\n const doc = this.#container.ownerDocument;\n if (!doc) return false;\n // The `download` attribute is honoured only for same-origin (and blob/data) URLs; for a\n // cross-origin one the browser ignores it and just navigates to the file, opening the PDF in\n // a tab instead of saving it. Pulling the file into a same-origin blob forces the save with\n // its filename.\n const { url, revoke } = await this.#sameOriginFile(info, doc);\n const link = doc.createElement('a');\n link.href = url;\n link.download = info.filename;\n link.rel = 'noopener';\n doc.body.appendChild(link);\n link.click();\n link.remove();\n // An object URL is ours to clean up; give the click a tick to start first.\n if (revoke) setTimeout(() => URL.revokeObjectURL(url), 10_000);\n return true;\n }\n\n /**\n * A same-origin URL for the source file, so an `<a download>` saves it and a print frame can be\n * scripted. A file that is already same-origin, or a blob we made ourselves (`revoke`), is\n * returned unchanged; a cross-origin one is fetched into a blob. The server must allow CORS,\n * which it already does — the viewer fetched the same PDF to render it. On a fetch failure the\n * raw URL is returned (opens in a tab, no worse than before).\n */\n async #sameOriginFile(info: DownloadInfo, doc: Document): Promise<{ url: string; revoke: boolean }> {\n if (info.revoke || !this.#isCrossOrigin(info.url, doc)) {\n return { url: info.url, revoke: info.revoke ?? false };\n }\n try {\n const blob = await (await fetch(info.url)).blob();\n return { url: URL.createObjectURL(blob), revoke: true };\n } catch {\n return { url: info.url, revoke: false };\n }\n }\n\n /** Whether `url` resolves to a different origin than the page, so an `<a download>` would be\n * ignored for it. A relative or same-origin URL, or one that fails to parse, counts as same. */\n #isCrossOrigin(url: string, doc: Document): boolean {\n const here = doc.defaultView?.location?.href;\n if (!here) return false;\n try {\n return new URL(url, here).origin !== new URL(here).origin;\n } catch {\n return false;\n }\n }\n\n /**\n * Find `query` in the book's text, case-insensitively.\n *\n * Pages are read on demand and cached by the source, so the first search over a long document\n * costs one text extraction per page and later ones are cheap. Returns at most one hit per\n * page, in page order. Always empty when {@link canSearch} is false.\n */\n async search(query: string, options: { limit?: number } = {}): Promise<SearchHit[]> {\n const getText = this.#source.getText;\n const needle = query.trim().toLowerCase();\n if (typeof getText !== 'function' || needle === '') return [];\n const limit = options.limit ?? 50;\n const hits: SearchHit[] = [];\n for (let page = 0; page < this.#source.pageCount && hits.length < limit; page++) {\n let text: string;\n try {\n text = await getText.call(this.#source, page);\n } catch (error) {\n this.#emitter.emit('sourceError', { index: page, error });\n continue;\n }\n const at = text.toLowerCase().indexOf(needle);\n if (at < 0) continue;\n hits.push({ page, excerpt: excerptAround(text, at, needle.length) });\n }\n return hits;\n }\n\n flipNext(): void {\n this.#stepFlip(1);\n }\n\n flipPrev(): void {\n this.#stepFlip(-1);\n }\n\n /** Turn one spread in `step`'s direction, counted from wherever the book lands. Interrupting\n * commits the running turn first, so this steps on from there — repeat taps keep advancing,\n * and a reversal goes to the neighbour of the spread that just landed. */\n #stepFlip(step: 1 | -1): void {\n this.#requestFlip(() => this.#startFlip(this.#current + step));\n }\n\n flipTo(page: number): void {\n const target = clamp(page, 0, Math.max(0, this.#source.pageCount - 1));\n this.#requestFlip(() => this.#startFlip(this.#spreadIndexForPage(target)));\n }\n\n /** Run a flip now. If a fold is animating, land it instantly first so the new turn starts\n * immediately on this click (thunks read `#current` lazily, so it steps on from where the\n * interrupted turn landed). A drag in flight can't be interrupted this way, so its request\n * is queued and replayed on settle instead. */\n #requestFlip(run: () => void): void {\n if (this.#machine.state === 'idle') {\n run();\n } else if (this.#activeAnim) {\n // This request supersedes anything still waiting: landing the current turn commits it,\n // and that commit drains the queue. Without clearing it first, an older tap would fire\n // from inside the commit and take the book somewhere the reader has since changed their\n // mind about — a reversal would be undone by the forward tap it was meant to replace.\n this.#queuedFlip = null;\n this.#finishActiveAnim(); // lands the current turn → machine back to idle\n run();\n } else {\n this.#queuedFlip = run; // dragging: no animation to cut short, so wait for release\n }\n }\n\n /** Snap the in-flight fold to its end pose and commit it now, cancelling its RAF loop. */\n #finishActiveAnim(): void {\n const anim = this.#activeAnim;\n if (!anim) return;\n this.#activeAnim = null;\n if (this.#raf !== null) {\n cancelAnimationFrame(this.#raf);\n this.#raf = null;\n }\n this.#renderer?.setFlipProgress(anim.toT, anim.direction);\n anim.onDone();\n }\n\n /** After an animation settles, fire whatever flip was requested mid-turn. */\n #drainQueuedFlip(): void {\n const queued = this.#queuedFlip;\n if (!queued) {\n // Nothing further to turn to, so this is where the book comes to rest: a good moment to\n // apply any page content that improved while it was moving.\n this.#flushPendingUpgrades();\n return;\n }\n this.#queuedFlip = null;\n queued();\n }\n\n getZoom(): number {\n return this.#scale;\n }\n\n /** Zoom to `scale` (clamped to [1, zoom.max]), keeping `center` (container-local) fixed. */\n setZoom(scale: number, center?: { x: number; y: number }): void {\n if (!this.#renderer || !this.#zoomEnabled) return;\n this.#cancelPeek(); // a zoom takes over the view; don't leave a peek folding under it\n this.#cancelNudge(); // and don't let a boundary rubber-band fight the zoom's own transform\n const s2 = clamp(scale, 1, this.#maxZoom);\n const m = this.#renderer.measure();\n // Default to the middle of the pages, not of the container: on a lone page those differ, and\n // zooming toward the container centre would drift the page off toward the gutter.\n const box = m.screenAt?.(this.#scale);\n const focal = center ??\n (box\n ? { x: box.x + this.#tx + box.width / 2, y: box.y + this.#ty + box.height / 2 }\n : null) ?? { x: m.containerWidth / 2, y: m.containerHeight / 2 };\n const s1 = this.#scale;\n // Solve for the translate that keeps the focal screen point fixed as s1→s2.\n let tx = s2 === 1 ? 0 : focal.x - (s2 / s1) * (focal.x - this.#tx);\n let ty = s2 === 1 ? 0 : focal.y - (s2 / s1) * (focal.y - this.#ty);\n [tx, ty] = this.#clampPan(tx, ty, s2, m.containerWidth, m.containerHeight);\n this.#scale = s2;\n this.#tx = tx;\n this.#ty = ty;\n this.#renderer.setViewTransform(s2, tx, ty);\n // Crossing in/out of zoom flips who owns vertical panning: browser scroll at rest, us when zoomed.\n if ((s1 > 1) !== (s2 > 1)) this.#applyTouchAction();\n this.#refreshZoomTiles();\n this.#updateCursor(); // crossing in/out of zoom swaps the flip hint for the zoom hint\n // First zoom in: teach panning, since a zoomed page has no visual cue that it can be dragged.\n if (s1 <= 1 && s2 > 1) {\n this.#markLearned('zoom');\n this.#showPanCaption();\n }\n this.#emitter.emit('zoomChanged', { scale: s2 });\n }\n\n resetZoom(): void {\n this.setZoom(1);\n }\n\n /** Hand the browser the gestures we don't use, so a flipbook filling the viewport doesn't trap\n * the page. At rest we only claim horizontal turns, so vertical panning (page scroll) stays the\n * browser's — `pan-y`. Zoomed in, one finger pans the image in both axes, so we take it all. */\n #applyTouchAction(): void {\n this.#container.style.touchAction = this.#scale > 1 ? 'none' : 'pan-y';\n }\n\n #clampPan(tx: number, ty: number, scale: number, w: number, h: number): [number, number] {\n // Bound the pan by where the pages actually are, not by the container. A lone page (a cover,\n // or the back of a book-mode spread) is painted in the middle half, and letterboxing insets\n // the book on any container whose aspect differs; clamping to the container would let the\n // reader drag the page off to one side and stare at the space beside it.\n //\n // `screen` is where the renderer says the pages are under the *current* view, so the offset\n // between it and the translate that produced it converts a wanted translate into a page\n // position. The renderers place a lone page differently (one shifts outside the view scale,\n // one inside), which is why this asks rather than deriving it.\n // Land on whole device pixels. A fractional offset makes every screen pixel a different\n // bilinear blend of the same texels, so as the page slides the strokes of each glyph thicken\n // and thin: the text appears to shimmer and change weight rather than simply move.\n const dpr = typeof devicePixelRatio === 'number' && devicePixelRatio > 0 ? devicePixelRatio : 1;\n // `|| 0` normalises -0, which Object.is and strict deep-equality treat as distinct from 0.\n const snap = (v: number): number => Math.round(v * dpr) / dpr || 0;\n\n // Asked for `scale` rather than read from the current view, because setZoom clamps before it\n // commits and would otherwise be bounded by the scale it is leaving.\n // Falls back to the container, which is what the pages fill on a renderer that does not\n // report a box: keeps a bare Renderer implementation clamped rather than free to pan away.\n const box = this.#renderer?.measure().screenAt?.(scale) ?? {\n x: 0,\n y: 0,\n width: w * scale,\n height: h * scale,\n };\n const [minX, maxX] = axisPanRange(box.x, box.width, w);\n const [minY, maxY] = axisPanRange(box.y, box.height, h);\n return [snap(clamp(tx, minX, maxX)), snap(clamp(ty, minY, maxY))];\n }\n\n on<K extends keyof ZineEventMap>(\n event: K,\n listener: (payload: ZineEventMap[K]) => void,\n ): () => void {\n return this.#emitter.on(event, listener);\n }\n\n destroy(): void {\n this.#destroyed = true;\n if (this.#raf !== null) cancelAnimationFrame(this.#raf);\n this.#activeAnim = null;\n this.#queuedFlip = null;\n this.#clearPendingClickFlip();\n this.#clearIdleNudge();\n this.#cancelPeek();\n this.#cancelNudge();\n this.#clearZoomHint();\n this.#hideCaption();\n this.#resizeObserver?.disconnect();\n this.#deepLink?.stop();\n this.#deepLink = null;\n this.#unsubscribeDeepLink?.();\n if (this.#deepLinkEnabled) releaseHash(); // a later book may own it now\n this.#controlsCleanup?.();\n this.#contextMenuCleanup?.();\n this.#dismissLoader();\n this.#a11yCleanup?.();\n this.#unbindWheel?.();\n this.#unbindDblClick?.();\n this.#unbindContextMenu?.();\n this.#unbindCursor?.();\n this.#unbindInput?.();\n if (this.#tileTimer !== null) clearTimeout(this.#tileTimer);\n this.#tileGeneration++; // strand any raster still resolving\n this.#renderer?.destroy();\n this.#renderer = null;\n this.#source.destroy();\n this.#emitter.clear();\n }\n\n #startFlip(targetIndex: number): void {\n if (targetIndex < 0 || targetIndex >= this.#spreads.length || targetIndex === this.#current) {\n return;\n }\n this.#cancelPeek(); // a real turn is starting; drop any peek folding the same leaf\n this.#cancelNudge(); // and any boundary rubber-band still springing\n // `flip` is legal only from idle; otherwise a flip/drag is already in flight.\n if (this.#machine.send('flip') === null) return;\n\n const direction: FlipDirection = targetIndex > this.#current ? 'forward' : 'backward';\n const toPage = this.#leadPage(targetIndex);\n // State is already locked (send('flip') above), so re-entrant flips are rejected\n // even though staging the destination content below is async.\n this.#emitter.emit('flipStart', { from: this.#currentPage, to: toPage });\n // Claimed here, synchronously, so this flip owns the run before #runFlip's first await.\n // Bumping inside #runFlip instead let a superseded flip's continuation claim the newest\n // generation and cancel the flip that replaced it.\n void this.#runFlip(targetIndex, direction, toPage, ++this.#flipGeneration);\n }\n\n async #runFlip(\n targetIndex: number,\n direction: FlipDirection,\n toPage: number,\n generation: number,\n ): Promise<void> {\n // Staging the destination is async, so this can resume after a later flip has already\n // superseded it (interrupting mid-turn does exactly that). Without this check the stale\n // continuation would begin animating its own, now-abandoned target and commit it, undoing\n // the turn the reader actually asked for.\n const toSpread = this.#spreads[targetIndex];\n const toContent: SpreadContent = toSpread\n ? await this.#resolveContent(toSpread)\n : { left: null, right: null };\n if (generation !== this.#flipGeneration) return;\n this.#renderer?.beginFlip(this.#currentContent, toContent, direction, {\n fill: this.#singlePage,\n curl: this.#effectiveCurl(),\n anchor: { y: this.#anchorY },\n });\n this.#animateProgress(\n 0,\n 1,\n direction,\n () => this.#commit(targetIndex, toPage, toContent),\n () => this.#announcePage(toPage),\n );\n }\n\n /** Animate the fold from `fromT` to `toT`, easing per-frame, then run `onDone`. */\n #animateProgress(\n fromT: number,\n toT: number,\n direction: FlipDirection,\n onDone: () => void,\n onLead?: () => void,\n ): void {\n const duration = this.#effectiveDuration() * Math.abs(toT - fromT);\n if (duration <= 0) {\n // Reduced motion (or zero-duration): swap without the curl sweep. onDone announces the\n // page itself, so there is no separate lead to fire.\n this.#renderer?.setFlipProgress(toT, direction);\n onDone();\n return;\n }\n // Remembered so a mid-turn flip request can #finishActiveAnim() to land it at once.\n // Object identity also tags this run: if #finishActiveAnim (or a new flip) replaces it,\n // a stale frame that still fires bails instead of re-committing or rescheduling itself.\n const anim = { toT, direction, onDone };\n this.#activeAnim = anim;\n const start = performance.now();\n let ledPage = false;\n const step = (now: number): void => {\n if (this.#activeAnim !== anim) return; // superseded/interrupted → this frame is void\n // Easing lives here; flipProgressToPose stays linear so seeks are deterministic.\n // Same ease for lone and spread so single-page turns feel like the same flipbook, not a\n // stretched dissolve. Curl geometry holds the landed pose through the decelerating tail.\n const raw = Math.min(1, (now - start) / duration);\n const eased = easeInOutQuad(raw);\n // The ease decelerates hard into the end, so once the pose crosses the lead threshold the\n // fold already reads as landed. Announce the page number there — once — while the fold plays\n // on to its full duration: the number leads the near-still tail without shortening the flip.\n if (onLead && !ledPage) {\n const lead = this.#singlePage ? PAGE_LEAD_LONE : PAGE_LEAD;\n if (eased >= lead) {\n ledPage = true;\n onLead();\n }\n }\n // `raw < 1` (not `raw >= 1`) so a finished — or NaN, under a rAF stub that omits the\n // timestamp — clock still lands rather than looping forever.\n if (raw < 1) {\n this.#renderer?.setFlipProgress(fromT + (toT - fromT) * eased, direction);\n this.#raf = requestAnimationFrame(step);\n } else {\n this.#renderer?.setFlipProgress(toT, direction);\n this.#raf = null;\n this.#activeAnim = null;\n onDone();\n }\n };\n this.#raf = requestAnimationFrame(step);\n }\n\n /** The page-facing half of a landing: current page, a11y text, and `pageChanged`. Fired from the\n * animation lead a little before the fold finishes so the number updates on time, then guarded\n * so the end-of-flip #commit does not repeat it. A jump with no animation just calls it inline. */\n #announcePage(toPage: number): void {\n if (this.#currentPage === toPage) return; // already led this turn (or nothing moved)\n this.#currentPage = toPage;\n this.#announce();\n this.#emitter.emit('pageChanged', { page: toPage });\n }\n\n #commit(targetIndex: number, toPage: number, toContent: SpreadContent): void {\n const spread = this.#spreads[targetIndex];\n this.#current = targetIndex;\n this.#currentContent = toContent;\n // renderSpread paints the landed spread and clears the turning leaf.\n if (spread) this.#paintSpread(spread, toContent);\n this.#prefetchWindow();\n this.#machine.send('settle');\n // Stamp the landing so a dead-edge double-click that arrives on its heels reads as the tail of\n // a flipping streak rather than a zoom (see #zoomAt).\n this.#lastFlipAt = performance.now();\n // No-op if the animation already led the number; otherwise (reduced motion, a canceled lead,\n // or an interrupt landing before the threshold) this is where it lands.\n this.#announcePage(toPage);\n this.#markLearned('turn'); // a page has turned — the peek/idle-nudge have taught their lesson\n this.#updateCursor(); // the new spread may have reached an end, killing a flip zone under the pointer\n this.#emitter.emit('flipEnd', { page: toPage });\n this.#drainQueuedFlip();\n }\n\n #leadPage(spreadIndex: number): number {\n const spread = this.#spreads[spreadIndex];\n if (!spread) return this.#currentPage;\n const pages = [spread.left, spread.right].filter((p): p is number => p !== null);\n return pages.length > 0 ? Math.min(...pages) : this.#currentPage;\n }\n\n #onDragStart(clientX: number, clientY: number): void {\n if (this.#pinching || !this.#renderer) return;\n this.#cancelNudge(); // a new press reclaims the view from a boundary rubber-band still springing\n this.#clearPendingClickFlip(); // a new press cancels a click-flip still waiting out its window\n // Zoomed in → a drag pans; at scale 1 → a corner drag flips (§9 mode switch).\n if (this.#scale > 1) {\n if (this.#machine.send('panStart') === null) return;\n this.#pan = { baseTx: this.#tx, baseTy: this.#ty };\n return;\n }\n const b = this.#contentRect();\n const point = this.#toBookPoint(clientX, clientY);\n // Recorded before the busy check below: a tap landing mid-flip is still a real tap, and\n // on release it decides which way to turn. Leaving the previous press in place made that\n // release repeat the last flip's direction instead of honouring the side just tapped.\n this.#press = point; // remembered for tap classification / click-to-flip zone\n // Grabbable along the whole outer band of each side edge, not just the corners: a drag\n // from the vertical middle should peel and follow the finger too, the way a corner does.\n // The fold then anchors at the grabbed height, so mid-edge grabs curl from the middle.\n const edgeBand = Math.min(b.width, b.height) * CORNER_FRACTION;\n const insidePage = point.x >= 0 && point.x <= b.width && point.y >= 0 && point.y <= b.height;\n const nearSideEdge = point.x <= edgeBand || point.x >= b.width - edgeBand;\n const wantsGrab = insidePage && nearSideEdge;\n // A fold may already be mid-flight when this press lands.\n if (this.#machine.state !== 'idle') {\n // Only an edge grab that can land the running turn takes it over. A live finger drag has\n // no animation to cut short (#activeAnim null), and a non-grab press (centre tap, or an\n // edge *tap* handled on release) must leave the animation to play out — so both bail here.\n // A flip's async content-staging also has #activeAnim null, so that window bails too.\n if (!this.#activeAnim || !wantsGrab) {\n this.#pendingGrab = null;\n return;\n }\n // Commit the running turn now so this grab peels straight on from where the book lands,\n // exactly the way repeated taps interrupt-and-advance. #current updates before we target.\n this.#finishActiveAnim();\n }\n if (!wantsGrab) {\n return; // not near a side edge — no drag; a tap here may still click-to-flip on release\n }\n this.#anchorY = clamp(point.y / b.height, 0, 1); // fold anchors at the grabbed point\n // Arm a potential drag; it only becomes a real flip once the pointer moves\n // (so an edge *tap* never fires a spurious flipStart). The right edge\n // turns the page forward in LTR; RTL mirrors it.\n const rightSide = point.x > b.width / 2;\n const forward = this.#direction === 'rtl' ? !rightSide : rightSide;\n const targetIndex = this.#current + (forward ? 1 : -1);\n if (targetIndex < 0 || targetIndex >= this.#spreads.length) return;\n this.#pendingGrab = {\n direction: forward ? 'forward' : 'backward',\n targetIndex,\n toPage: this.#leadPage(targetIndex),\n width: b.width,\n };\n }\n\n /** Turn an armed corner press into a live drag flip once the pointer has moved. */\n #promoteGrab(): void {\n const grab = this.#pendingGrab;\n if (!grab) return;\n this.#pendingGrab = null;\n if (this.#machine.send('grab') === null) return;\n this.#drag = {\n direction: grab.direction,\n targetIndex: grab.targetIndex,\n toPage: grab.toPage,\n toContent: { left: null, right: null },\n width: grab.width,\n t: 0,\n };\n this.#emitter.emit('flipStart', { from: this.#currentPage, to: grab.toPage });\n void this.#stageDrag(grab.targetIndex, grab.direction);\n }\n\n async #stageDrag(targetIndex: number, direction: FlipDirection): Promise<void> {\n const toSpread = this.#spreads[targetIndex];\n const toContent: SpreadContent = toSpread\n ? await this.#resolveContent(toSpread)\n : { left: null, right: null };\n // The drag may have ended (or retargeted) while content was resolving.\n if (this.#drag?.targetIndex !== targetIndex) return;\n this.#drag.toContent = toContent;\n this.#renderer?.beginFlip(this.#currentContent, toContent, direction, {\n fill: this.#singlePage,\n curl: this.#effectiveCurl(),\n anchor: { y: this.#anchorY },\n });\n this.#renderer?.setFlipProgress(this.#drag.t, direction);\n }\n\n #onDragMove(dx: number, dy: number): void {\n if (this.#pinching) return;\n if (this.#pan) {\n if (!this.#renderer) return;\n const m = this.#renderer.measure();\n const [tx, ty] = this.#clampPan(\n this.#pan.baseTx + dx,\n this.#pan.baseTy + dy,\n this.#scale,\n m.containerWidth,\n m.containerHeight,\n );\n this.#tx = tx;\n this.#ty = ty;\n this.#renderer.setViewTransform(this.#scale, tx, ty);\n this.#refreshZoomTiles();\n this.#markLearned('pan'); // the reader is panning — the caption has served its purpose\n this.#hideCaption();\n return;\n }\n // An armed corner press becomes a real flip once it moves past the threshold.\n if (this.#pendingGrab && Math.hypot(dx, dy) > DRAG_THRESHOLD) {\n this.#promoteGrab();\n }\n const drag = this.#drag;\n if (!drag) return;\n const signed = drag.direction === 'forward' ? -dx : dx;\n drag.t = clamp(signed / drag.width, 0, 1);\n this.#renderer?.setFlipProgress(drag.t, drag.direction);\n }\n\n #onDragEnd(gesture: GestureEnd): void {\n if (this.#pinching) return;\n if (this.#pan) {\n this.#pan = null;\n this.#machine.send('panEnd');\n this.#updateCursor(); // pan over → re-derive the resting hint (no move fires on release)\n return;\n }\n const drag = this.#drag;\n if (!drag) {\n // Never became a grab (the press was not on a side edge, so no live peel armed). A fast\n // horizontal flick from anywhere still turns the page; otherwise it was a tap.\n this.#pendingGrab = null;\n if (!this.#swipeFlip(gesture)) this.#maybeClickFlip(gesture);\n return;\n }\n this.#drag = null;\n this.#machine.send('release');\n\n // A flick at release throws the page, and its direction is the release velocity (vx), not the\n // net displacement: you can peel a page well past halfway and then flick it back toward the\n // edge to let go — the mobile counterpart of dragging back to cancel on desktop. An onward\n // throw commits even from short of halfway; a back-throw cancels even from past it. Position\n // (drag.t) decides only when the release is not a horizontal flick.\n const horizFlick = gesture.swipe && Math.abs(gesture.vx) > Math.abs(gesture.vy);\n const onward = drag.direction === 'forward' ? gesture.vx < 0 : gesture.vx > 0;\n const flungOnward = horizFlick && onward;\n const flungBack = horizFlick && !onward;\n if (flungOnward || (drag.t >= 0.5 && !flungBack)) {\n this.#animateProgress(\n drag.t,\n 1,\n drag.direction,\n () => this.#commit(drag.targetIndex, drag.toPage, drag.toContent),\n () => this.#announcePage(drag.toPage),\n );\n } else {\n this.#animateProgress(drag.t, 0, drag.direction, () => this.#cancelFlip());\n }\n }\n\n /** A fast horizontal flick that started away from the side edges (so no live peel was armed):\n * turn the page in the flick's direction. Returns whether it consumed the gesture, so the caller\n * skips click-to-flip. Zoomed in the same gesture pans, so this only applies at scale 1. */\n #swipeFlip(gesture: GestureEnd): boolean {\n if (this.#scale > 1) return false;\n // A flick, and predominantly horizontal — a vertical flick (page scroll intent) is not a turn.\n if (!gesture.swipe || Math.abs(gesture.dx) <= Math.abs(gesture.dy)) return false;\n // In LTR a leftward flick (dx < 0) turns forward; RTL and the reading direction mirror it.\n const forward = this.#direction === 'rtl' ? gesture.dx > 0 : gesture.dx < 0;\n const direction: FlipDirection = forward ? 'forward' : 'backward';\n if (!this.#canFlip(direction)) {\n this.#playEdgeNudge(direction); // swiped past the end: rubber-band toward the edge\n return false;\n }\n const step = forward ? 1 : -1;\n this.#requestFlip(() => this.#startFlip(this.#current + step));\n return true;\n }\n\n #maybeClickFlip(gesture: GestureEnd): void {\n if (this.#clickToFlip === 'off' || this.#scale > 1 || !this.#press) return;\n // A tap barely moves; anything more was a drag we already ignored.\n if (Math.abs(gesture.dx) > DRAG_THRESHOLD || Math.abs(gesture.dy) > DRAG_THRESHOLD) return;\n const direction = this.#clickFlipDirection(this.#press);\n if (!direction) return;\n if (!this.#canFlip(direction)) {\n this.#playEdgeNudge(direction); // nowhere to turn: rubber-band toward the edge instead\n return;\n }\n // Anchor the fold at the tapped height (cone/leaf/flick curl from where you tap).\n this.#anchorY = clamp(this.#press.y / this.#contentRect().height, 0, 1);\n // Read `#current` lazily: interrupting commits the running turn first, so this steps on\n // from wherever the book actually landed — one spread back from there is the neighbour\n // the reader is looking at, not the spread they tapped on two turns ago.\n const step = direction === 'forward' ? 1 : -1;\n const flip = (): void => this.#startFlip(this.#current + step);\n this.#clearPendingClickFlip();\n if (this.#clickFlipDelayValue <= 0) {\n // No double-click competing here → flip right away (or queue if mid-turn).\n this.#requestFlip(flip);\n return;\n }\n // Wait out the window; a double-click (zoom) cancels this.\n this.#pendingClickTimer = setTimeout(() => {\n this.#pendingClickTimer = null;\n this.#requestFlip(flip);\n }, this.#clickFlipDelayValue);\n }\n\n /** Where the current spread is actually drawn (lone pages centered), as the renderer reports\n * it — the region taps/drags map into. Falls back to book, then the full container. */\n #contentRect(): { x: number; y: number; width: number; height: number } {\n const m = this.#renderer!.measure();\n return m.content ?? m.book ?? { x: 0, y: 0, width: m.containerWidth, height: m.containerHeight };\n }\n\n /** A viewport point in book-local px: relative to the drawn page, not the container. The two\n * differ whenever the book is letterboxed, so every hit test against a zone or corner has to\n * come through here — measuring from the container edge puts the zones off by the bar width. */\n #toBookPoint(clientX: number, clientY: number): { x: number; y: number } {\n const rect = this.#container.getBoundingClientRect();\n const b = this.#contentRect();\n return { x: clientX - rect.left - b.x, y: clientY - rect.top - b.y };\n }\n\n /** Which way a tap at `point` (book-local, via {@link #toBookPoint}) turns the page, or null\n * for a dead zone. */\n #clickFlipDirection(point: { x: number; y: number }): FlipDirection | null {\n if (!this.#renderer) return null;\n const bookWidth = this.#contentRect().width;\n let side: 'left' | 'right' | null;\n if (this.#clickToFlip === 'half') {\n side = point.x > bookWidth / 2 ? 'right' : 'left';\n } else if (point.x <= this.#clickZoneSize) {\n side = 'left';\n } else if (point.x >= bookWidth - this.#clickZoneSize) {\n side = 'right';\n } else {\n side = null; // center dead zone in 'edge' mode\n }\n if (!side) return null;\n const forwardSide = this.#direction === 'rtl' ? 'left' : 'right';\n return side === forwardSide ? 'forward' : 'backward';\n }\n\n /** Whether a flip in `direction` has somewhere to land: false at the first spread going back\n * and the last spread going forward. A flip zone that fails this is dead, so a double-click\n * there should zoom rather than defer to a turn that can never happen. */\n #canFlip(direction: FlipDirection): boolean {\n const target = this.#current + (direction === 'forward' ? 1 : -1);\n return target >= 0 && target < this.#spreads.length;\n }\n\n /** Watch the pointer at rest and hint what a press there would do through the cursor (mouse only;\n * touch never fires these). A separate listener from the gesture recognizer so it can be removed\n * on its own; both read the same `pointermove` stream. */\n #bindCursorHints(): void {\n const move = (e: Event): void => {\n const p = e as PointerEvent;\n this.#pointerClient = { x: p.clientX, y: p.clientY };\n this.#updateCursor();\n };\n const leave = (): void => {\n this.#pointerClient = null;\n this.#updateCursor();\n };\n this.#container.addEventListener('pointermove', move);\n this.#container.addEventListener('pointerleave', leave);\n this.#unbindCursor = (): void => {\n this.#container.removeEventListener('pointermove', move);\n this.#container.removeEventListener('pointerleave', leave);\n };\n }\n\n /** Re-derive the cursor from the last pointer position. Called both on movement and after a page\n * turn, zoom, or resize changes what a press at the same spot would do without the mouse moving. */\n #updateCursor(): void {\n if (!this.#cursorHints || !this.#renderer) return;\n let cursor = '';\n if (this.#drag || this.#pan) {\n cursor = 'grabbing'; // a peel or pan is in flight — the hand has hold of the page\n } else if (this.#pointerClient) {\n cursor = this.#cursorFor(this.#pointerClient.x, this.#pointerClient.y);\n }\n this.#container.style.cursor = cursor;\n }\n\n /**\n * The cursor for a resting pointer at client (x, y): a hint for what a press would do, read from\n * the same zone classifiers the gestures use. Returns '' (the default arrow) off the drawn page\n * or where a press does nothing.\n *\n * Zoomed in, the whole page pans, so one steady 'grab' covers it — no flicker as the reader moves,\n * and it never lies about a double-click that would zoom back out (grab, not a stale zoom-in). At\n * rest there are two hints: 'pointer' over a live click-to-flip zone, and 'zoom-in' everywhere else\n * a double-click would zoom (including the edge peel band, so a sweep across the page stays calm).\n * A drag peels regardless; it just is not called out with its own resting cursor.\n */\n #cursorFor(clientX: number, clientY: number): string {\n if (!this.#renderer) return '';\n const b = this.#contentRect();\n const p = this.#toBookPoint(clientX, clientY);\n if (p.x < 0 || p.y < 0 || p.x > b.width || p.y > b.height) return ''; // a letterbox bar\n // Zoomed in the page pans everywhere: a drag is the standing gesture, so hint it with grab.\n if (this.#scale > 1) return 'grab';\n // At rest a click turns the page in the flip zones...\n const flipDir = this.#clickFlipDirection(p);\n if (flipDir && this.#canFlip(flipDir)) return 'pointer';\n // ...and elsewhere a double-click zooms in (from rest the next configured level is always up).\n if (this.#zoomEnabled && this.#doubleClickLevels !== null) return 'zoom-in';\n return '';\n }\n\n // --- Discoverability hints (corner peek, idle nudge, first-zoom caption) ---\n\n /** Read the learned flags from localStorage (persist mode). Storage can throw in private mode or a\n * sandboxed iframe, and a corrupt value is not worth crashing over — either way, learn nothing. */\n #loadLearned(): void {\n try {\n const raw = localStorage.getItem(HINTS_STORAGE_KEY);\n if (!raw) return;\n const saved = JSON.parse(raw) as Partial<Learned>;\n this.#learned = {\n turn: saved.turn === true,\n zoom: saved.zoom === true,\n pan: saved.pan === true,\n };\n } catch {\n // No stored history; the reader is simply taught fresh.\n }\n }\n\n /** Record that the reader has performed a gesture, so the hint teaching it stays quiet from now on\n * (and across visits under persist). Idempotent; tears down whatever that signal silences. */\n #markLearned(signal: 'turn' | 'zoom' | 'pan'): void {\n if (this.#learned[signal]) return;\n this.#learned[signal] = true;\n if (signal === 'turn') this.#clearIdleNudge(); // no more peeks once a page has been turned\n if (signal === 'zoom') this.#clearZoomHint(); // reader zoomed; a pending zoom caption is moot\n if (this.#hintsPersist) {\n try {\n localStorage.setItem(HINTS_STORAGE_KEY, JSON.stringify(this.#learned));\n } catch {\n // Persisting is best-effort; the in-memory flag still silences the hint this session.\n }\n }\n }\n\n /** Arm the one-shot idle nudge: if the reader turns no page within IDLE_NUDGE_MS, replay the peek\n * once. Any real turn clears the timer first (see #markLearned('turn')). */\n #armIdleNudge(): void {\n if (!this.#hintsEnabled || this.#reducedMotion || this.#learned.turn) return;\n this.#clearIdleNudge();\n this.#idleTimer = setTimeout(() => {\n this.#idleTimer = null;\n this.#playPeek();\n }, IDLE_NUDGE_MS);\n }\n\n #clearIdleNudge(): void {\n if (this.#idleTimer !== null) {\n clearTimeout(this.#idleTimer);\n this.#idleTimer = null;\n }\n }\n\n /**\n * The corner peek: lift the leading page's outer edge to PEEK_PROGRESS and settle it back, once,\n * without committing a turn. A wordless \"this page turns\" cue that reaches touch and mouse alike.\n *\n * Its own rAF loop, not #animateProgress — that one is wired to #commit and the flip generation.\n * Tagged by #peekAnim so a real flip (or destroy) cancels a peek in flight; the leaf is repainted\n * flat at the end so nothing is left half-folded.\n */\n async #playPeek(): Promise<void> {\n if (\n !this.#hintsEnabled ||\n this.#reducedMotion ||\n this.#learned.turn ||\n !this.#renderer ||\n typeof requestAnimationFrame !== 'function' || // no way to animate; skip the cosmetic cue\n this.#machine.state !== 'idle' // a real flip/drag/pan owns the leaf; don't fight it\n ) {\n return;\n }\n const targetIndex = this.#current + 1;\n const toSpread = this.#spreads[targetIndex];\n if (!toSpread) return; // nothing ahead to peek toward (single spread, or already at the end)\n const toContent = await this.#resolveContent(toSpread);\n // Guard the async gap: a turn, a zoom, a teardown, or a second peek may have intervened.\n if (this.#destroyed || this.#learned.turn || this.#machine.state !== 'idle' || this.#peekAnim) {\n return;\n }\n // Anchor the fold at the bottom-outer corner, the natural place a thumb lifts a page.\n this.#anchorY = 1;\n this.#renderer.beginFlip(this.#currentContent, toContent, 'forward', {\n fill: this.#singlePage,\n curl: this.#effectiveCurl(),\n anchor: { y: this.#anchorY },\n });\n const token = {};\n this.#peekAnim = token;\n const start = performance.now();\n const total = PEEK_LEG_MS * 2;\n // Read the clock from performance.now(), not the rAF timestamp arg: it is what drives the two\n // legs, and it keeps the loop working under a rAF stub that omits the timestamp.\n const step = (): void => {\n if (this.#peekAnim !== token) return; // superseded/cancelled → this frame is void\n const raw = Math.min(1, (performance.now() - start) / total);\n // Out to PEEK_PROGRESS by the midpoint, back to 0 by the end; a soft sine so it eases at both\n // the lift and the settle rather than snapping.\n const t = PEEK_PROGRESS * Math.sin(raw * Math.PI);\n this.#renderer?.setFlipProgress(Math.max(0, t), 'forward');\n if (raw < 1) {\n this.#raf = requestAnimationFrame(step);\n } else {\n this.#raf = null;\n this.#peekAnim = null;\n this.#repaintRestingSpread(); // clear the turning leaf, back to a flat spread\n }\n };\n this.#raf = requestAnimationFrame(step);\n }\n\n /** Stop a peek mid-play (a real gesture is taking over) and leave the spread flat. */\n #cancelPeek(): void {\n if (!this.#peekAnim) return;\n this.#peekAnim = null;\n if (this.#raf !== null) {\n cancelAnimationFrame(this.#raf);\n this.#raf = null;\n }\n this.#repaintRestingSpread();\n }\n\n /** Repaint the current spread flat (no turning leaf), used to land a finished/cancelled peek. */\n #repaintRestingSpread(): void {\n const spread = this.#spreads[this.#current];\n if (spread && this.#machine.state === 'idle') this.#paintSpread(spread, this.#currentContent);\n }\n\n /**\n * The boundary rubber-band: on a tap/swipe that would turn the page but has nowhere to land (the\n * last spread going forward, the first going back), slide the whole spread a little toward that\n * edge and spring it back. A wordless \"this is the end\" that reads on touch and mouse alike.\n *\n * A view-transform animation (setViewTransform), not a fold — there is no next page to curl toward\n * at a boundary. Its own rAF loop, tagged by #nudgeAnim so a real gesture cancels it mid-spring.\n */\n #playEdgeNudge(direction: FlipDirection): void {\n if (\n !this.#renderer ||\n typeof requestAnimationFrame !== 'function' || // no way to animate; skip the cosmetic cue\n this.#reducedMotion ||\n this.#machine.state !== 'idle' || // a flip/drag owns the view; don't fight it\n this.#scale > 1 || // zoomed: the transform is the reader's pan, not ours to move\n this.#nudgeAnim // one nudge at a time\n ) {\n return;\n }\n // Slide toward the edge the reader tried to reach: forward turns off the right in LTR, so the\n // spread travels left (negative x); RTL and backward mirror it.\n const forward = direction === 'forward';\n const toRightEdge = this.#direction === 'rtl' ? !forward : forward;\n const sign = toRightEdge ? -1 : 1;\n const token = {};\n this.#nudgeAnim = token;\n const start = performance.now();\n const step = (): void => {\n if (this.#nudgeAnim !== token) return; // superseded/cancelled → this frame is void\n const raw = Math.min(1, (performance.now() - start) / EDGE_NUDGE_MS);\n // Out to EDGE_NUDGE_PX at the midpoint, back to 0 by the end — a soft sine springs at both ends.\n const offset = EDGE_NUDGE_PX * Math.sin(raw * Math.PI);\n this.#renderer?.setViewTransform(1, sign * offset, 0);\n if (raw < 1) {\n this.#raf = requestAnimationFrame(step);\n } else {\n this.#raf = null;\n this.#nudgeAnim = null;\n this.#renderer?.setViewTransform(1, 0, 0); // land exactly on center\n }\n };\n this.#raf = requestAnimationFrame(step);\n }\n\n /** Stop a boundary nudge mid-spring (a real gesture is taking over) and reset the view to center. */\n #cancelNudge(): void {\n if (!this.#nudgeAnim) return;\n this.#nudgeAnim = null;\n if (this.#raf !== null) {\n cancelAnimationFrame(this.#raf);\n this.#raf = null;\n }\n this.#renderer?.setViewTransform(1, 0, 0);\n }\n\n /** Show the one-time \"drag to move\" caption over the book on the first zoom. Self-contained inline\n * styles: the controls stylesheet is absent when `controls: false`, and this hint must still work.\n * A dark toast pill with light text reads on any page background and either theme. */\n #showPanCaption(): void {\n if (this.#learned.pan) return;\n this.#showCaption('Drag to move'); // panning has no visual analog, so this one hint uses words\n }\n\n /** Show a transient caption pill over the book, replacing any caption already up. Self-contained\n * inline styles: the controls stylesheet is absent when `controls: false`, and a hint must still\n * work. Guarded by #hintsEnabled so `hints: false` shows nothing. */\n #showCaption(text: string): void {\n if (!this.#hintsEnabled) return;\n const doc = this.#container.ownerDocument;\n if (!doc || typeof this.#container.appendChild !== 'function') return;\n this.#hideCaption(); // one pill at a time; drop any caption (and its timer) already showing\n const el = doc.createElement('div');\n el.className = 'zine-hint-caption';\n el.setAttribute('aria-hidden', 'true'); // decorative; screen readers get the gesture via a11y text\n el.textContent = text;\n el.style.cssText =\n 'position:absolute;left:50%;bottom:16px;transform:translateX(-50%);z-index:3;' +\n 'pointer-events:none;padding:6px 12px;border-radius:999px;font:500 13px/1.2 system-ui,sans-serif;' +\n 'color:#fff;background:rgba(24,24,27,0.82);box-shadow:0 1px 4px rgba(0,0,0,0.35);' +\n 'white-space:nowrap;opacity:0;';\n const canAnimate = !this.#reducedMotion && typeof requestAnimationFrame === 'function';\n if (canAnimate) el.style.transition = 'opacity 200ms ease';\n this.#container.appendChild(el);\n this.#captionEl = el;\n if (canAnimate) {\n // Next frame so the initial opacity:0 is committed before the transition to 1.\n requestAnimationFrame(() => {\n if (this.#captionEl === el) el.style.opacity = '1';\n });\n } else {\n el.style.opacity = '1'; // no fade available — just show it\n }\n this.#captionTimer = setTimeout(() => this.#hideCaption(), CAPTION_MS);\n }\n\n /** Remove the caption (on the gesture it taught, its timeout, or teardown). */\n #hideCaption(): void {\n if (this.#captionTimer !== null) {\n clearTimeout(this.#captionTimer);\n this.#captionTimer = null;\n }\n this.#captionEl?.remove();\n this.#captionEl = null;\n }\n\n /** After a lone click lands where only a double-click would do anything (a dead zone at rest),\n * wait out the pairing window; if no second click pairs, the reader tried to act and nothing\n * happened — hint how to zoom. Mouse-only (touch has no double-click), once a session, and never\n * once zoom is learned. A real double-click clears this (it zooms, which teaches zoom directly). */\n #maybeArmZoomHint(clientX: number, clientY: number): void {\n if (!this.#hintsEnabled || this.#learned.zoom || this.#zoomHintShown) return;\n if (this.#lastPointerType !== 'mouse') return;\n // Only where a single click did nothing but a double-click would zoom (dead zone, scale 1, zoom\n // on). #cursorFor already folds in letterbox, flip-zone, and scale arbitration.\n if (this.#cursorFor(clientX, clientY) !== 'zoom-in') return;\n // Not on the tail of a rapid flipping streak — those dead-zone clicks are turning, not zooming.\n if (this.#machine.state === 'animating' || performance.now() - this.#lastFlipAt <= FLIP_STREAK_MS) {\n return;\n }\n this.#clearZoomHint();\n this.#zoomHintTimer = setTimeout(() => {\n this.#zoomHintTimer = null;\n if (this.#learned.zoom || this.#zoomHintShown) return; // a zoom landed while we waited\n this.#zoomHintShown = true;\n this.#showCaption(this.#zoomHintText());\n }, DOUBLE_CLICK_MS);\n }\n\n /** The zoom caption's text, naming whichever zoom gestures are actually enabled. Double-click is\n * always one (this fires only where a double-click would zoom); Ctrl/⌘-scroll is added when wheel\n * zoom is on, with the platform's modifier. */\n #zoomHintText(): string {\n const parts: string[] = [];\n if (this.#doubleClickLevels !== null) parts.push('Double-click');\n if (this.#wheelZoom) {\n const nav = this.#container.ownerDocument?.defaultView?.navigator;\n const mac = /Mac|iPhone|iPad/.test(nav?.platform ?? '');\n parts.push(`${mac ? '⌘' : 'Ctrl'}-scroll`);\n }\n return `${parts.join(' or ')} to zoom`;\n }\n\n #clearZoomHint(): void {\n if (this.#zoomHintTimer !== null) {\n clearTimeout(this.#zoomHintTimer);\n this.#zoomHintTimer = null;\n }\n }\n\n #clearPendingClickFlip(): void {\n if (this.#pendingClickTimer !== null) {\n clearTimeout(this.#pendingClickTimer);\n this.#pendingClickTimer = null;\n }\n }\n\n #cancelFlip(): void {\n const spread = this.#spreads[this.#current];\n if (spread) this.#paintSpread(spread, this.#currentContent);\n this.#machine.send('settle');\n this.#updateCursor(); // the snapped-back spread may have changed the hint under a resting pointer\n this.#emitter.emit('flipEnd', { page: this.#currentPage });\n this.#drainQueuedFlip();\n }\n\n #onPinchStart(): void {\n // A second finger abandons any single-pointer gesture in flight.\n this.#pendingGrab = null;\n this.#clearPendingClickFlip();\n if (this.#drag) {\n this.#drag = null;\n this.#machine.send('release');\n this.#machine.send('settle');\n const spread = this.#spreads[this.#current];\n if (spread) this.#paintSpread(spread, this.#currentContent);\n } else if (this.#pan) {\n this.#pan = null;\n this.#machine.send('panEnd');\n }\n this.#pinching = true;\n this.#pinchBaseScale = this.#scale;\n }\n\n #onPinchMove(centerX: number, centerY: number, scale: number): void {\n if (!this.#pinching) return;\n const rect = this.#container.getBoundingClientRect();\n this.setZoom(this.#pinchBaseScale * scale, { x: centerX - rect.left, y: centerY - rect.top });\n }\n\n #onPinchEnd(): void {\n this.#pinching = false;\n }\n\n /** Swallow the right-click menu over the book, when the consumer asked for that. */\n #bindContextMenu(): void {\n if (!this.#disableContextMenu) return;\n const onContextMenu = (event: Event): void => event.preventDefault();\n this.#container.addEventListener('contextmenu', onContextMenu);\n this.#unbindContextMenu = () =>\n this.#container.removeEventListener('contextmenu', onContextMenu);\n }\n\n #bindWheelZoom(): void {\n const onWheel = (event: WheelEvent): void => {\n // Ctrl/⌘ + wheel only — this is also what a desktop trackpad pinch emits.\n if (!this.#wheelZoom || !this.#zoomEnabled || !(event.ctrlKey || event.metaKey)) return;\n // Override the browser's native page zoom.\n event.preventDefault();\n const rect = this.#container.getBoundingClientRect();\n const factor = Math.exp(-event.deltaY * WHEEL_ZOOM_SENSITIVITY);\n this.setZoom(this.#scale * factor, {\n x: event.clientX - rect.left,\n y: event.clientY - rect.top,\n });\n };\n this.#container.addEventListener('wheel', onWheel as EventListener, { passive: false });\n this.#unbindWheel = () =>\n this.#container.removeEventListener('wheel', onWheel as EventListener);\n }\n\n #bindDoubleClickZoom(): void {\n // The library pairs clicks itself rather than trusting the browser's `dblclick`. The native\n // event drops out on rapid streaks — four fast clicks in one spot fire only a single\n // `dblclick`, because the browser resets its click counter mid-streak (often when the first\n // zoom shifts the target under the pointer). Detecting pairs from raw `click` events makes\n // every second click of a streak zoom (or cycle), so continuous clicks keep toggling.\n // Click carries no pointerType; capture it from the preceding pointerdown so the zoom hint can\n // stay mouse-only (touch has no double-click).\n const onDown = (event: PointerEvent): void => {\n this.#lastPointerType = event.pointerType;\n };\n const onClick = (event: MouseEvent): void => {\n const levels = this.#doubleClickLevels;\n if (!this.#zoomEnabled || levels === null || !this.#renderer) return;\n const t = performance.now();\n const prev = this.#lastClick;\n const paired =\n prev !== null &&\n t - prev.t <= DOUBLE_CLICK_MS &&\n Math.hypot(event.clientX - prev.x, event.clientY - prev.y) <= DOUBLE_CLICK_MOVE;\n if (!paired) {\n // First click of a potential pair: remember it and wait for a second.\n this.#lastClick = { t, x: event.clientX, y: event.clientY };\n // A lone click in a dead zone is a reader who tried something and got nothing: hint how to\n // zoom, unless the second click of a pair arrives first (a real zoom teaches it directly).\n this.#maybeArmZoomHint(event.clientX, event.clientY);\n return;\n }\n // Second click within the window → a double-click. Consume it so the next click starts a\n // fresh pair rather than chaining a third click into another zoom.\n this.#lastClick = null;\n this.#clearZoomHint(); // this pair is a real double-click; drop the pending lone-click hint\n this.#zoomAt(event.clientX, event.clientY, levels);\n };\n this.#container.addEventListener('pointerdown', onDown as EventListener);\n this.#container.addEventListener('click', onClick as EventListener);\n this.#unbindDblClick = () => {\n this.#container.removeEventListener('pointerdown', onDown as EventListener);\n this.#container.removeEventListener('click', onClick as EventListener);\n };\n }\n\n /** Cycle to the next configured zoom level, keeping the clicked point fixed. Honors the\n * flip-zone arbitration at scale 1 so an edge double-click still turns the page there, and\n * suppresses zoom on the tail of a rapid flipping streak (see below). */\n #zoomAt(clientX: number, clientY: number, levels: number[]): void {\n // At scale 1 a double-click inside a live flip zone belongs to click-to-flip, not zoom,\n // unless we're honoring double-click zoom there.\n if (this.#scale <= 1 && this.#clickToFlip !== 'off' && !this.#honorDoubleClickInFlipZone) {\n const local = this.#toBookPoint(clientX, clientY);\n const dir = this.#clickFlipDirection(local);\n // Yield to a zone that can actually turn: click-to-flip owns it, not zoom.\n if (dir !== null && this.#canFlip(dir)) return;\n // Otherwise — a dead edge at the book's ends, or the centre dead zone. That normally zooms,\n // but if the reader is mid-streak (a flip still folding, or one that just landed) the pair is\n // the tail of rapid flipping, not a zoom: swallow it. This is the cover/lone-page glitch —\n // the flip zone sits mid-screen there, so the first click turns the page and the second lands\n // in the centre of the spread it flipped to. A deliberate zoom comes once the reader pauses,\n // past the streak window.\n if (this.#machine.state === 'animating' || performance.now() - this.#lastFlipAt <= FLIP_STREAK_MS) {\n return;\n }\n }\n // A double-click means the single-click flip we may have queued was really a zoom.\n this.#clearPendingClickFlip();\n // Next configured level above the current scale, else wrap to the first.\n const next = levels.find((l) => l > this.#scale + 1e-6) ?? levels[0] ?? this.#scale;\n const rect = this.#container.getBoundingClientRect();\n this.setZoom(next, { x: clientX - rect.left, y: clientY - rect.top });\n }\n\n async #init(rendererOption: RendererOption): Promise<void> {\n // Load the renderer chunk while opening an async source (e.g. a PDF) in parallel.\n const rendererPromise = selectRenderer(rendererOption);\n // Report the download to the progress event and the built-in loader; register before open()\n // so the very first bytes are seen.\n this.#source.onProgress?.((p) => this.#onLoadProgress(p));\n if (typeof this.#source.open === 'function') {\n void this.#mountLoader(); // an async source has a wait worth indicating; sync ones do not\n try {\n await this.#source.open();\n this.#buildSpreadModel(); // page count known now; validates + builds spreads\n } catch (error) {\n this.#dismissLoader(); // tear the overlay down before `ready` rejects\n throw error;\n }\n this.#setLoaderPhase('preparing'); // bytes are in; the wait is now rasterizing\n }\n this.#source.prefetch([this.#currentPage]);\n\n const selected = await rendererPromise;\n const renderer = await this.#mountWithFallback(selected);\n this.#renderer = renderer;\n\n // Re-render the current spread when a source upgrades one of its pages in place.\n this.#source.onPageUpdate?.((index) => this.#onPageUpdate(index));\n\n // Now that we can measure, apply single-page mode if the container is narrow.\n this.#applySinglePage(renderer.measure().containerWidth);\n\n const pointer = new PointerRecognizer({\n onStart: (g) => this.#onDragStart(g.x, g.y),\n onMove: (g) => this.#onDragMove(g.dx, g.dy),\n onEnd: (g) => this.#onDragEnd(g),\n });\n const pinch = new PinchRecognizer({\n onPinchStart: () => this.#onPinchStart(),\n onPinchMove: (g) => this.#onPinchMove(g.centerX, g.centerY, g.scale),\n onPinchEnd: () => this.#onPinchEnd(),\n });\n this.#unbindInput = bindGestures(this.#container, { pointer, pinch });\n // We now own horizontal gestures; hand vertical panning (page scroll) back to the browser.\n this.#applyTouchAction();\n this.#bindWheelZoom();\n this.#bindDoubleClickZoom();\n this.#bindContextMenu();\n this.#bindCursorHints();\n this.#setupReducedMotion();\n this.#a11yCleanup = this.#setupA11y();\n\n await this.#renderCurrent();\n this.#dismissLoader(); // the first spread is painted; drop the overlay before the toolbar mounts\n this.#observeResize();\n this.#prefetchWindow();\n this.#announce();\n this.#bindDeepLink();\n await this.#mountControls();\n this.#emitter.emit('ready');\n // The book is live: peek the leading corner once, then watch for an idle reader.\n void this.#playPeek();\n this.#armIdleNudge();\n }\n\n /** Forward a download-progress tick to the public event and the built-in loader. */\n #onLoadProgress(progress: LoadProgress): void {\n this.#lastProgress = progress;\n this.#emitter.emit('progress', progress);\n this.#loader?.update(progress);\n }\n\n /**\n * Fetch the loader chunk and show the overlay, unless disabled or the book is already up.\n *\n * Lazy, so `loading: false` (or a source that never reports progress) never downloads it.\n * Failure is swallowed: a loader that cannot load should cost the reader a spinner, not the book.\n */\n async #mountLoader(): Promise<void> {\n if (this.#loadingOption === false || this.#destroyed) return;\n const container = this.#container;\n if (!container.ownerDocument || typeof container.appendChild !== 'function') return;\n try {\n const { mountLoading } = await import('./loading/loading');\n // The spread may have painted while the chunk was in flight; do not pop an overlay over it.\n if (this.#loaderPhase === 'done' || this.#destroyed) return;\n this.#loader = mountLoading(container);\n if (this.#lastProgress) this.#loader.update(this.#lastProgress);\n if (this.#loaderPhase === 'preparing') this.#loader.preparing();\n } catch {\n // No overlay; the book itself is unaffected.\n }\n }\n\n #setLoaderPhase(phase: 'preparing'): void {\n if (this.#loaderPhase === 'done') return;\n this.#loaderPhase = phase;\n this.#loader?.preparing();\n }\n\n #dismissLoader(): void {\n this.#loaderPhase = 'done';\n this.#loader?.destroy();\n this.#loader = null;\n }\n\n /** Mount `renderer`; if it can't initialize (e.g. no WebGL2 context) fall back to CSS. */\n async #mountWithFallback(renderer: Renderer): Promise<Renderer> {\n try {\n await renderer.mount(this.#container);\n renderer.onFatal?.(() => {\n void this.#fallbackToCss();\n });\n return renderer;\n } catch {\n renderer.destroy();\n const css = await selectRenderer('css');\n await css.mount(this.#container);\n this.#fellBack = true;\n this.#emitter.emit('rendererFallback', { from: 'webgl2', to: 'css' });\n return css;\n }\n }\n\n /** Runtime fallback: a live renderer signaled an unrecoverable failure (§8.4). */\n async #fallbackToCss(): Promise<void> {\n if (this.#fellBack || this.#destroyed) return;\n this.#fellBack = true;\n this.#renderer?.destroy();\n this.#renderer = null;\n const css = await selectRenderer('css');\n if (this.#destroyed) {\n css.destroy();\n return;\n }\n await css.mount(this.#container);\n this.#renderer = css;\n this.#applySinglePage(css.measure().containerWidth);\n await this.#renderCurrent();\n this.#emitter.emit('rendererFallback', { from: 'webgl2', to: 'css' });\n }\n\n #setupReducedMotion(): void {\n if (typeof matchMedia !== 'function') return;\n const query = matchMedia('(prefers-reduced-motion: reduce)');\n this.#reducedMotion = query.matches;\n query.addEventListener?.('change', (e) => {\n this.#reducedMotion = e.matches;\n });\n }\n\n /**\n * Build the toolbar and the right-click menu, whichever are wanted, if the container is real DOM.\n *\n * The chunk is fetched lazily so a book with neither feature never downloads it. Both share the\n * one chunk, so a book with `controls: false` but `contextMenu` on still gets the right-click menu\n * without a toolbar. Failure is swallowed on purpose: controls that cannot load should cost the\n * reader the controls, not the whole flipbook — `#init` has no error handling of its own, so a\n * throw here would reject `ready`.\n */\n async #mountControls(): Promise<void> {\n const wantsToolbar = this.#controlsOption !== false;\n const wantsContextMenu = this.#contextMenuOption !== false;\n if ((!wantsToolbar && !wantsContextMenu) || this.#destroyed) return;\n const container = this.#container;\n if (!container.ownerDocument || typeof container.appendChild !== 'function') return;\n try {\n const { mountControls, mountContextMenu } = await import('./controls/controls');\n if (this.#destroyed) return; // destroyed while the chunk was in flight\n if (this.#controlsOption !== false) {\n const options = this.#controlsOption === true ? {} : this.#controlsOption;\n this.#controlsCleanup = mountControls(this, container, options, this.#hiddenControls);\n }\n if (this.#contextMenuOption !== false) {\n const options = this.#contextMenuOption === true ? {} : this.#contextMenuOption;\n this.#contextMenuCleanup = mountContextMenu(this, container, options, this.#hiddenControls);\n }\n } catch {\n // No controls; the book itself is unaffected.\n }\n }\n\n /** Mirror the page in the URL, and follow the URL when it changes. */\n #bindDeepLink(): void {\n if (!this.#deepLinkEnabled || typeof globalThis.location === 'undefined') return;\n const win = globalThis as unknown as Window;\n this.#deepLink = bindDeepLink(win, (page) => {\n // Only act on a hash the reader changed — following a link, or pressing Back. A hash we\n // wrote ourselves describes where the book already is.\n if (page !== this.#currentPage) this.flipTo(page);\n });\n this.#deepLink.push(this.#currentPage);\n this.#unsubscribeDeepLink = this.#emitter.on('pageChanged', ({ page }) => {\n this.#deepLink?.push(page);\n });\n }\n\n /** The URL that opens this book at `page` (defaults to the current one). */\n pageLink(page: number = this.#currentPage): string {\n const href = globalThis.location?.href ?? '';\n if (!href) return '';\n const url = new URL(href);\n url.hash = hashWithPage(url.hash, clamp(page, 0, Math.max(0, this.#source.pageCount - 1)));\n return url.toString();\n }\n\n /** Wire keyboard nav + an aria-live page announcer. Skips non-DOM containers. */\n #setupA11y(): (() => void) | null {\n const container = this.#container;\n const doc = container.ownerDocument;\n if (!doc || typeof container.setAttribute !== 'function') return null;\n\n if (!container.hasAttribute('tabindex')) container.tabIndex = 0;\n container.setAttribute('aria-roledescription', 'flipbook');\n const onKey = (e: KeyboardEvent): void => this.#onKeyDown(e);\n container.addEventListener('keydown', onKey);\n\n const live = doc.createElement('div');\n live.setAttribute('aria-live', 'polite');\n live.setAttribute('aria-atomic', 'true');\n live.style.cssText =\n 'position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0;';\n container.appendChild(live);\n this.#liveRegion = live;\n\n return () => {\n container.removeEventListener('keydown', onKey);\n live.remove();\n this.#liveRegion = null;\n };\n }\n\n #onKeyDown(event: KeyboardEvent): void {\n const forwardKey = this.#direction === 'rtl' ? 'ArrowLeft' : 'ArrowRight';\n switch (event.key) {\n case 'ArrowRight':\n case 'ArrowLeft':\n if (event.key === forwardKey) this.flipNext();\n else this.flipPrev();\n event.preventDefault();\n break;\n case 'Home':\n this.flipTo(0);\n event.preventDefault();\n break;\n case 'End':\n this.flipTo(this.#source.pageCount - 1);\n event.preventDefault();\n break;\n default:\n break;\n }\n }\n\n #announce(): void {\n if (this.#liveRegion) {\n this.#liveRegion.textContent = `Page ${this.#currentPage + 1} of ${this.#source.pageCount}`;\n }\n }\n\n #effectiveDuration(): number {\n // Reduced motion still turns the page, just briefly and flat (see #effectiveCurl): a page\n // that teleports gives the reader no sense of which way the book moved.\n if (this.#reducedMotion) return REDUCED_MOTION_DURATION;\n return this.#flipDuration * (this.#singlePage ? LONE_PAGE_FLIP_SCALE : 1);\n }\n\n /** The curl to turn with: reduced motion flattens the sheet, whatever the book asked for. */\n #effectiveCurl(): CurlSpec {\n return this.#reducedMotion ? 'simple' : this.#curl;\n }\n\n /** Re-measure and re-render; call after the container resizes. */\n update(): void {\n if (!this.#renderer || this.#machine.state !== 'idle') return;\n this.#applySinglePage(this.#renderer.measure().containerWidth);\n void this.#renderCurrent();\n this.#updateCursor(); // the drawn box (and its zones) may have moved under a resting pointer\n }\n\n /** The layout to actually use: the responsive narrow fallback forces 'single'. */\n #effectiveMode(): SpreadMode {\n return this.#narrow || this.#spreadMode === 'single' ? 'single' : this.#spreadMode;\n }\n\n #applySinglePage(containerWidth: number): void {\n const narrow =\n this.#responsiveSpread && shouldSinglePage(containerWidth, this.#singlePageThreshold);\n if (narrow === this.#narrow) return;\n this.#narrow = narrow;\n this.#rebuildSpreads();\n }\n\n /** Regroup the pages after something changes the layout, keeping the reader on the page they\n * were looking at even though its spread index may have moved. */\n #rebuildSpreads(): void {\n const mode = this.#effectiveMode();\n const changed = mode !== this.#effectiveModeShown;\n this.#effectiveModeShown = mode;\n this.#singlePage = mode === 'single';\n this.#spreads = buildSpreads(this.#source.pageCount, { direction: this.#direction, mode });\n this.#current = this.#spreadIndexForPage(this.#currentPage);\n // The book's proportions change with the layout, so consumers sizing around it need to know.\n if (changed) this.#emitter.emit('spreadChanged', { mode, singlePage: this.#singlePage });\n }\n\n async #renderCurrent(): Promise<void> {\n const spread = this.#spreads[this.#current];\n if (!spread || !this.#renderer) return;\n this.#currentContent = await this.#resolveContent(spread);\n this.#paintSpread(spread, this.#currentContent);\n }\n\n #paintSpread(spread: Spread, content: SpreadContent): void {\n this.#renderer?.renderSpread(spread, content, { fill: this.#singlePage });\n this.#applyContainerAspect();\n // These pages were resolved at fit-to-screen; if the reader is zoomed, ask for them sharper.\n this.#zoomedAt = 1;\n this.#refreshZoomTiles();\n }\n\n /** Match the container's aspect-ratio to the book so it fits with no letterbox bars.\n * Non-destructive: `aspect-ratio` only drives whichever dimension the consumer leaves\n * auto (fit-width when they set a width), and is ignored if both are fixed. */\n #applyContainerAspect(): void {\n const b = this.#renderer?.measure().book;\n if (!b || b.width <= 0 || b.height <= 0) return;\n const ratio = (b.width / b.height).toFixed(4);\n if (ratio === this.#lastAspect) return;\n this.#lastAspect = ratio;\n this.#container.style.aspectRatio = ratio;\n }\n\n /**\n * A source upgraded a page's content (e.g. a progressive PDF swapping in the crisp render).\n *\n * Repainting mid-flip would disturb the animation, so an upgrade that lands during one is\n * noted and applied on settle instead of dropped. Dropping it left the page stuck at low\n * resolution for good: the upgrade fires once, and jumping straight to a page — from a search\n * hit, the page field, the outline — is exactly when it tends to arrive.\n */\n #onPageUpdate(index: number): void {\n if (this.#machine.state !== 'idle') {\n this.#pendingUpgrades.add(index);\n return;\n }\n const spread = this.#spreads[this.#current];\n if (!spread || !(spread.left === index || spread.right === index)) return;\n // While the reader is zoomed, the upgrade touched the fit raster underneath the sharp tile, not\n // what they see. A fit #renderCurrent would flash the page soft before the tiles rebuilt; refresh\n // the tiles instead (a no-op when they are already at the viewed scale).\n if (this.#scale > 1) {\n this.#refreshZoomTiles();\n return;\n }\n void this.#renderCurrent();\n }\n\n /**\n * Ask the source for a crisp render of whatever is visible, and lay it over the magnified page.\n *\n * Zooming is only a view transform, so without this the reader is magnifying the pixels of a\n * raster made to fit the screen. A vector source can do better; one backed by a fixed-resolution\n * original cannot, and says so by handing back a whole page, which is dropped.\n *\n * Debounced, and hidden while the view moves. A tile is crisp for one view only, so during a\n * pan it is stale by definition; showing it anyway puts two versions of the same text on screen\n * at once. Hiding leaves the reader with the uniformly magnified page an image book already\n * shows, which is smooth because there is nothing to disagree with it. The tile returns once\n * the view settles.\n */\n /**\n * Re-rasterize the visible pages at the magnification being viewed, and hand them to the\n * renderer as ordinary page content.\n *\n * Zoom is a view transform, so the renderer magnifies a raster made to fit the screen and text\n * goes soft. Giving it a sharper raster for the same page fixes that at the root: panning stays\n * a pure view transform over crisp pixels, which is why an image book pans smoothly and why an\n * overlay painted only between gestures never could.\n *\n * Debounced, since rasterizing a PDF page is far too slow to do per frame.\n */\n #refreshZoomTiles(): void {\n if (this.#tileTimer !== null) clearTimeout(this.#tileTimer);\n const wanted = this.#upgradeScale();\n if (wanted === this.#zoomedAt) return; // already showing this resolution\n if (wanted === 1) {\n this.#tileGeneration++; // abandon anything in flight: it is no longer wanted\n this.#zoomedAt = 1;\n void this.#renderCurrent(); // back to the fit-to-screen raster\n return;\n }\n this.#tileTimer = setTimeout(() => {\n this.#tileTimer = null;\n void this.#renderZoomTiles();\n }, ZOOM_TILE_DELAY);\n }\n\n /**\n * How many device pixels the visible page is painted across, over how many its fit-resolution\n * raster has (the neediest page of the spread). > 1 means the raster is stretched to cover the\n * display and text is soft; < 1 means the raster already has pixels to spare (a high-res image on\n * a low-dpr screen), which must be allowed so a crispness floor doesn't wastefully re-request it.\n *\n * A single-page spread gives one page the width two shared, so a fit raster sized for the double\n * layout is stretched over ~2x the space; this is the magnification the upgrade pass must undo.\n */\n #fitFactor(): number {\n const m = this.#renderer?.measure();\n const box = m?.content ?? m?.book;\n if (!box || box.width <= 0) return 1;\n const c = this.#currentContent;\n const painted = (c.left ? 1 : 0) + (c.right ? 1 : 0);\n if (painted === 0) return 1;\n // Both halves of a spread are equal width, so the per-page painted width is the box over the\n // number of pages in it (1 for a lone page, whose `content` box is already the centered half).\n const paintedPageCss = box.width / painted;\n const dpr = typeof devicePixelRatio === 'number' && devicePixelRatio > 0 ? devicePixelRatio : 1;\n let f = 0;\n for (const p of [c.left, c.right]) {\n if (!p || p.width <= 0) continue;\n // #renderZoomTiles swaps an upgraded (larger) raster into #currentContent and records its\n // multiplier in #zoomedAt; dividing it back out recovers the invariant fit-resolution width,\n // so a re-measure of an already-sharp page reports the same factor instead of flip-flopping\n // between fit and sharp.\n const intrinsic = p.width / this.#zoomedAt;\n f = Math.max(f, (paintedPageCss * dpr) / intrinsic);\n }\n return f === 0 ? 1 : f;\n }\n\n /**\n * The resolution multiplier to rasterize the visible pages at.\n *\n * Three demands compound onto the fit raster. `#fitFactor` covers a page painted larger than its\n * raster (device px). Zoom magnifies it further. And a text-crispness floor lifts the raster to\n * ~2x CSS px even when it already covers the display 1:1, because a 1:1 raster reads soft; the\n * floor is scaled by 1/dpr so a retina screen (already ~2x) is left alone and pays no memory.\n * Capped because a page costs the square of this in memory: at 4x a letter page is over 100 MB,\n * which would dwarf the source's whole cache.\n */\n #upgradeScale(): number {\n const dpr = typeof devicePixelRatio === 'number' && devicePixelRatio > 0 ? devicePixelRatio : 1;\n const crispnessFloor = Math.max(1, TEXT_SUPERSAMPLE / dpr);\n const zoom = this.#scale <= 1 ? 1 : this.#scale;\n const raw = this.#fitFactor() * Math.max(zoom, crispnessFloor);\n if (raw <= 1 + 1e-3) return 1;\n return Math.min(Math.ceil(raw * 2) / 2, MAX_PAGE_UPGRADE);\n }\n\n async #renderZoomTiles(): Promise<void> {\n const spread = this.#spreads[this.#current];\n if (!this.#renderer || !spread) return;\n const scale = this.#upgradeScale();\n if (scale === 1 || scale === this.#zoomedAt) return;\n // An upgrade repaints through renderSpread, which clears the renderer's turning leaf. If a flip\n // is animating (rapid edge clicks queue a settle-time upgrade whose debounce fires mid-next-turn)\n // painting now would wipe the curl and the leaf would snap flat. Skip: the flip's own settle\n // repaints and re-requests this once the book is idle, the defer rule #onPageUpdate follows too.\n // Checked here, before the slow rasterize, so a turn in progress costs nothing.\n if (this.#machine.state !== 'idle') return;\n\n const generation = ++this.#tileGeneration;\n const whole = { x: 0, y: 0, width: 1, height: 1 };\n const pages = await Promise.all(\n [spread.left, spread.right].map(async (index) => {\n if (index === null) return null;\n try {\n return await this.#source.get(index, {\n scale,\n region: whole,\n maxSize: this.#renderer?.maxTextureSize,\n });\n } catch (error) {\n // An upgrade is an enhancement: the readable, softer page stays on screen.\n this.#emitter.emit('sourceError', { index, error });\n return null;\n }\n }),\n );\n // Rasterizing is slow and the reader may have moved on, zoomed elsewhere, or begun a flip while\n // it resolved (the slow-source race the entry check cannot catch). Any of those and this raster\n // is stale; drop it and let the settle re-request.\n if (\n generation !== this.#tileGeneration ||\n this.#upgradeScale() !== scale ||\n this.#machine.state !== 'idle'\n ) {\n return;\n }\n // A side whose upgrade threw comes back null; keep the readable fit raster already on screen\n // rather than blanking that half (the upgrade is an enhancement, per the get() catch above).\n const left = pages[0] ?? this.#currentContent.left;\n const right = pages[1] ?? this.#currentContent.right;\n // Nothing sharper came back: a source with a fixed-resolution original hands over the raster\n // already on screen, and repainting it would be pure churn.\n if (left === this.#currentContent.left && right === this.#currentContent.right) return;\n\n this.#zoomedAt = scale;\n this.#currentContent = { left, right };\n this.#renderer.renderSpread(spread, this.#currentContent, { fill: this.#singlePage });\n }\n\n /** Apply any upgrade that arrived mid-flip, now that the book has settled. */\n #flushPendingUpgrades(): void {\n if (this.#pendingUpgrades.size === 0) return;\n const spread = this.#spreads[this.#current];\n const onScreen =\n spread !== undefined &&\n [spread.left, spread.right].some((p) => p !== null && this.#pendingUpgrades.has(p));\n this.#pendingUpgrades.clear();\n if (!onScreen) return;\n // Same rule as #onPageUpdate: while the reader is zoomed, a fit #renderCurrent would flash the\n // page soft before the tiles rebuilt. Refresh the tiles instead. Gated on the live zoom scale,\n // not #zoomedAt: the #commit that leads here just repainted at fit and reset #zoomedAt to 1.\n if (this.#scale > 1) this.#refreshZoomTiles();\n else void this.#renderCurrent();\n }\n\n #observeResize(): void {\n if (typeof ResizeObserver === 'undefined') return;\n this.#resizeObserver = new ResizeObserver(() => this.#scheduleUpdate());\n this.#resizeObserver.observe(this.#container);\n }\n\n #scheduleUpdate(): void {\n if (this.#updateScheduled) return;\n this.#updateScheduled = true;\n requestAnimationFrame(() => {\n this.#updateScheduled = false;\n this.update();\n });\n }\n\n async #resolveContent(spread: Spread): Promise<SpreadContent> {\n const [left, right] = await Promise.all([this.#getPage(spread.left), this.#getPage(spread.right)]);\n return { left, right };\n }\n\n /** Decode one page; on failure emit `sourceError` and degrade to a blank (null) page. */\n async #getPage(index: number | null): Promise<PageContent | null> {\n if (index === null) return null;\n try {\n return await this.#source.get(index);\n } catch (error) {\n this.#emitter.emit('sourceError', { index, error });\n return null;\n }\n }\n\n #prefetchWindow(): void {\n const { toMount } = this.#virtualizer.update(this.#current, this.#spreads.length);\n const pages: number[] = [];\n for (const index of toMount) {\n const spread = this.#spreads[index];\n if (spread?.left != null) pages.push(spread.left);\n if (spread?.right != null) pages.push(spread.right);\n }\n this.#source.prefetch(pages);\n }\n\n #spreadIndexForPage(page: number): number {\n const index = this.#spreads.findIndex((s) => s.left === page || s.right === page);\n return index === -1 ? 0 : index;\n }\n\n /** Validate the (now-known) page count + startPage and build the spread model. */\n #buildSpreadModel(): void {\n const pageCount = this.#source.pageCount;\n if (!Number.isInteger(pageCount) || pageCount < 1) {\n throw new Error(`Zine: source has ${pageCount} pages; a Source must have at least 1 page.`);\n }\n const startPage = this.#startPageOption;\n if (\n startPage !== undefined &&\n (!Number.isInteger(startPage) || startPage < 0 || startPage >= pageCount)\n ) {\n throw new Error(\n `Zine: startPage ${JSON.stringify(startPage)} is out of range for a ${pageCount}-page book (valid 0..${pageCount - 1}).`,\n );\n }\n const mode = this.#effectiveMode();\n this.#singlePage = mode === 'single';\n this.#spreads = buildSpreads(pageCount, { direction: this.#direction, mode });\n // A page in the URL wins over `startPage`: the reader followed a link to it, which is a\n // later and more specific intent than the page the author configured.\n const linked = this.#deepLinkEnabled ? pageFromHash(globalThis.location?.hash ?? '') : null;\n this.#currentPage = clamp(linked ?? startPage ?? 0, 0, pageCount - 1);\n this.#current = this.#spreadIndexForPage(this.#currentPage);\n }\n}\n\nfunction clamp(value: number, min: number, max: number): number {\n return value < min ? min : value > max ? max : value;\n}\n\n/**\n * The translate range that keeps content within the viewport along one axis.\n *\n * `pos`/`size` are where the pages sit with no pan applied, so the view puts them at `pos + t`.\n * When they are larger than the viewport the range spans from their far edge to their near one,\n * so the reader can reach every part. When smaller there is nothing to explore, so the range\n * collapses to the single value that centres them: a lone page at 2x fills the width exactly, and\n * any freedom there would only let it drift off to one side.\n */\nfunction axisPanRange(pos: number, size: number, viewport: number): [min: number, max: number] {\n if (size <= viewport) {\n const centered = (viewport - size) / 2 - pos;\n return [centered, centered];\n }\n return [viewport - (pos + size), -pos];\n}\n\n/** Quadratic ease-in-out. Deliberately gentler than a cubic: a cubic leaves the page nearly\n * motionless for the first quarter of the turn and then spikes through the middle at roughly\n * double the peak speed, which reads as a lurch rather than a sheet of paper being turned. */\nfunction easeInOutQuad(t: number): number {\n return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;\n}\n\nfunction typeName(v: unknown): string {\n if (v === null) return 'null';\n if (Array.isArray(v)) return 'array';\n return typeof v;\n}\n\n/** Validate constructor inputs up front, throwing agent-actionable errors. */\nfunction validateOptions(container: unknown, options: unknown): void {\n if (\n typeof container !== 'object' ||\n container === null ||\n typeof (container as { appendChild?: unknown }).appendChild !== 'function' ||\n typeof (container as { addEventListener?: unknown }).addEventListener !== 'function'\n ) {\n throw new Error(`Zine: container must be a DOM element; received ${typeName(container)}.`);\n }\n if (typeof options !== 'object' || options === null) {\n throw new Error('Zine: an options object with a `source` is required.');\n }\n const o = options as Record<string, unknown>;\n\n const source = o.source as { get?: unknown; pageCount?: unknown } | undefined;\n if (\n typeof source !== 'object' ||\n source === null ||\n typeof source.get !== 'function' ||\n typeof source.pageCount !== 'number'\n ) {\n throw new Error('Zine: `source` is required and must be a Source, e.g. new ImageSource(urls).');\n }\n // Note: pageCount >= 1 and startPage-in-range are validated in #buildSpreadModel —\n // synchronously for a sync source, or after open() for an async one (e.g. PdfSource).\n\n const startPage = o.startPage;\n if (startPage !== undefined && (typeof startPage !== 'number' || !Number.isInteger(startPage) || startPage < 0)) {\n throw new Error(`Zine: startPage must be a non-negative integer; got ${JSON.stringify(startPage)}.`);\n }\n\n const direction = o.direction;\n if (direction !== undefined && direction !== 'ltr' && direction !== 'rtl') {\n throw new Error(`Zine: direction must be 'ltr' or 'rtl'; got ${JSON.stringify(direction)}.`);\n }\n\n const clickToFlip = o.clickToFlip;\n if (clickToFlip !== undefined && !['edge', 'half', 'off'].includes(clickToFlip as string)) {\n throw new Error(`Zine: clickToFlip must be 'edge', 'half', or 'off'; got ${JSON.stringify(clickToFlip)}.`);\n }\n\n const curl = o.curl;\n if (curl !== undefined) {\n if (typeof curl === 'object' && curl !== null) {\n // An imported or custom model. Check the one member the renderer calls every frame, so a\n // wrong shape fails here rather than as an undefined call mid-flip.\n if (typeof (curl as { deform?: unknown }).deform !== 'function') {\n throw new Error('Zine: a curl model must have a deform() function.');\n }\n } else if (IMPORTABLE_CURLS.includes(curl as string)) {\n // A real curl, just not bundled: say how to get it rather than calling it invalid.\n throw new Error(\n `Zine: the '${String(curl)}' curl is not bundled. Import it and pass the model: ` +\n `import { ${String(curl)} } from '@zinejs/core/curls' → curl: ${String(curl)}`,\n );\n } else if (!CURL_TYPES.includes(curl as CurlType)) {\n throw new Error(\n `Zine: curl must be ${CURL_TYPES.join(' or ')}, or a model imported from ` +\n `'@zinejs/core/curls' (${IMPORTABLE_CURLS.join(', ')}); got ${JSON.stringify(curl)}.`,\n );\n }\n }\n\n for (const flag of ['deepLink', 'disableContextMenu', 'responsiveSpread', 'loading'] as const) {\n if (o[flag] !== undefined && typeof o[flag] !== 'boolean') {\n throw new Error(`Zine: ${flag} must be a boolean; got ${typeName(o[flag])}.`);\n }\n }\n\n const controls = o.controls;\n if (controls !== undefined && typeof controls !== 'boolean' && (typeof controls !== 'object' || controls === null)) {\n throw new Error(\n `Zine: controls must be a boolean or an options object; got ${typeName(controls)}.`,\n );\n }\n const position = (controls as { position?: unknown } | undefined)?.position;\n if (position !== undefined && !['top', 'bottom', 'left', 'right'].includes(position as string)) {\n throw new Error(\n `Zine: controls.position must be 'top', 'bottom', 'left', or 'right'; got ${JSON.stringify(position)}.`,\n );\n }\n const colorScheme = (controls as { colorScheme?: unknown } | undefined)?.colorScheme;\n if (colorScheme !== undefined && !['light', 'dark', 'auto'].includes(colorScheme as string)) {\n throw new Error(\n `Zine: controls.colorScheme must be 'light', 'dark', or 'auto'; got ${JSON.stringify(colorScheme)}.`,\n );\n }\n const arrows = (controls as { arrows?: unknown } | undefined)?.arrows;\n if (arrows !== undefined && typeof arrows !== 'boolean' && !['desktop', 'mobile'].includes(arrows as string)) {\n throw new Error(\n `Zine: controls.arrows must be a boolean, 'desktop', or 'mobile'; got ${JSON.stringify(arrows)}.`,\n );\n }\n\n const contextMenu = o.contextMenu;\n if (contextMenu !== undefined && typeof contextMenu !== 'boolean' && (typeof contextMenu !== 'object' || contextMenu === null)) {\n throw new Error(\n `Zine: contextMenu must be a boolean or an options object; got ${typeName(contextMenu)}.`,\n );\n }\n const menuColorScheme = (contextMenu as { colorScheme?: unknown } | undefined)?.colorScheme;\n if (menuColorScheme !== undefined && !['light', 'dark', 'auto'].includes(menuColorScheme as string)) {\n throw new Error(\n `Zine: contextMenu.colorScheme must be 'light', 'dark', or 'auto'; got ${JSON.stringify(menuColorScheme)}.`,\n );\n }\n if (o.disableContextMenu === true && contextMenu !== undefined && contextMenu !== false) {\n throw new Error(\n \"Zine: disableContextMenu and contextMenu are mutually exclusive — one removes the browser's \" +\n 'right-click menu, the other replaces it. Enable only one.',\n );\n }\n\n const hideControls = o.hideControls;\n if (\n hideControls !== undefined &&\n (!Array.isArray(hideControls) || hideControls.some((id) => typeof id !== 'string'))\n ) {\n throw new Error(\n `Zine: hideControls must be an array of control-id strings; got ${typeName(hideControls)}.`,\n );\n }\n\n const spreadMode = o.spreadMode;\n if (spreadMode !== undefined && !['double', 'single', 'cover', 'book'].includes(spreadMode as string)) {\n throw new Error(\n `Zine: spreadMode must be 'double', 'single', 'cover', or 'book'; got ${JSON.stringify(spreadMode)}.`,\n );\n }\n\n for (const key of ['frontCover', 'backCover'] as const) {\n const value = o[key];\n if (value !== undefined && typeof value !== 'string') {\n throw new Error(`Zine: ${key} must be an image URL string; got ${JSON.stringify(value)}.`);\n }\n }\n const pages = o.pages;\n if (pages !== undefined && (typeof pages !== 'object' || pages === null || Array.isArray(pages))) {\n throw new Error('Zine: pages must be an object mapping page indices to image URLs.');\n }\n\n assertMin(o.width, 'width', 1);\n assertMin(o.height, 'height', 1);\n assertMin(o.flipDuration, 'flipDuration', 0);\n assertMin(o.clickZoneSize, 'clickZoneSize', 0);\n assertMin(o.clickFlipDelay, 'clickFlipDelay', 0);\n assertMin(o.singlePageThreshold, 'singlePageThreshold', 0);\n validateZoomOption(o.zoom);\n validateRendererOption(o.renderer);\n}\n\nfunction assertMin(value: unknown, name: string, min: number): void {\n if (value === undefined) return;\n if (typeof value !== 'number' || !Number.isFinite(value) || value < min) {\n throw new Error(`Zine: ${name} must be a number >= ${min}; got ${JSON.stringify(value)}.`);\n }\n}\n\nfunction validateZoomOption(zoom: unknown): void {\n if (zoom === undefined) return;\n if (typeof zoom !== 'object' || zoom === null || Array.isArray(zoom)) {\n throw new Error('Zine: zoom must be an object, e.g. { max: 4 }.');\n }\n const z = zoom as Record<string, unknown>;\n if (z.enabled !== undefined && typeof z.enabled !== 'boolean') {\n throw new Error(`Zine: zoom.enabled must be a boolean; got ${JSON.stringify(z.enabled)}.`);\n }\n if (z.wheel !== undefined && typeof z.wheel !== 'boolean') {\n throw new Error(`Zine: zoom.wheel must be a boolean; got ${JSON.stringify(z.wheel)}.`);\n }\n if (z.doubleClickInFlipZone !== undefined && typeof z.doubleClickInFlipZone !== 'boolean') {\n throw new Error(\n `Zine: zoom.doubleClickInFlipZone must be a boolean; got ${JSON.stringify(z.doubleClickInFlipZone)}.`,\n );\n }\n if (z.doubleClick !== undefined && z.doubleClick !== false) {\n const levels = z.doubleClick;\n if (\n !Array.isArray(levels) ||\n levels.some((n) => typeof n !== 'number' || !Number.isFinite(n) || n < 1)\n ) {\n throw new Error(\n 'Zine: zoom.doubleClick must be false or an array of zoom levels >= 1, e.g. [1, 2, 4].',\n );\n }\n }\n assertMin(z.max, 'zoom.max', 1);\n}\n\nfunction validateRendererOption(renderer: unknown): void {\n if (renderer === undefined) return;\n if (typeof renderer === 'string') {\n if (renderer !== 'auto' && renderer !== 'css' && renderer !== 'webgl2') {\n throw new Error(\n `Zine: renderer '${renderer}' is not recognized; use 'auto', 'css', 'webgl2', an array of those, or a custom Renderer.`,\n );\n }\n return;\n }\n if (Array.isArray(renderer)) {\n for (const kind of renderer) {\n if (kind !== 'css' && kind !== 'webgl2') {\n throw new Error(\n `Zine: renderer order array may only contain 'css' or 'webgl2'; got ${JSON.stringify(kind)}.`,\n );\n }\n }\n return;\n }\n if (\n typeof renderer === 'object' &&\n renderer !== null &&\n typeof (renderer as { mount?: unknown }).mount === 'function'\n ) {\n return;\n }\n throw new Error(\n \"Zine: renderer must be 'auto', 'css', 'webgl2', an array of those, or a custom Renderer instance.\",\n );\n}\n","import type { PageContent } from '../renderer/types';\nimport type { Source } from './types';\n\nexport type ImageFit = 'contain' | 'cover';\n\nexport interface ImageSourceOptions {\n /** How many adjacent pages to prefetch around a requested page. Default 1. */\n preload?: number;\n /** How each image fits its page; a hint consumed by the render layer. Default 'contain'. */\n fit?: ImageFit;\n}\n\n/**\n * Default content source for image books: decodes a list of image URLs to\n * ImageBitmaps, memoizes them, and prefetches neighbors on access.\n */\nexport class ImageSource implements Source {\n readonly pageCount: number;\n readonly fit: ImageFit;\n #urls: string[];\n #preload: number;\n #cache = new Map<number, Promise<ImageBitmap>>();\n\n constructor(urls: string[], options: ImageSourceOptions = {}) {\n this.#urls = urls;\n this.pageCount = urls.length;\n this.#preload = options.preload ?? 1;\n this.fit = options.fit ?? 'contain';\n }\n\n get(index: number): Promise<PageContent> {\n const decoded = this.#decode(index);\n for (let d = 1; d <= this.#preload; d++) {\n this.prefetch([index - d, index + d]);\n }\n return decoded;\n }\n\n prefetch(indices: number[]): void {\n for (const i of indices) {\n if (i >= 0 && i < this.pageCount) {\n void this.#decode(i).catch(() => {}); // swallow: prefetch failures surface on real get()\n }\n }\n }\n\n destroy(): void {\n for (const pending of this.#cache.values()) {\n void pending.then((bitmap) => bitmap.close()).catch(() => {});\n }\n this.#cache.clear();\n }\n\n #decode(index: number): Promise<ImageBitmap> {\n const cached = this.#cache.get(index);\n if (cached) return cached;\n\n const url = this.#urls[index];\n if (url === undefined) {\n return Promise.reject(\n new RangeError(`ImageSource: page ${index} is out of range (0..${this.pageCount - 1}).`),\n );\n }\n\n const promise = (async () => {\n const res = await fetch(url);\n if (!res.ok) {\n throw new Error(`ImageSource: failed to fetch page ${index} (${url}) — HTTP ${res.status}.`);\n }\n return createImageBitmap(await res.blob());\n })().catch((err: unknown) => {\n this.#cache.delete(index); // drop the failed entry so a later get() can retry\n throw err;\n });\n\n this.#cache.set(index, promise);\n return promise;\n }\n}\n","export { Zine } from './zine';\nexport type { ZineOptions, ZoomOptions, SearchHit } from './zine';\n\n// Just the registry mechanism — the toolbar, its icons and the built-in definitions all live in\n// the lazily-loaded chunk, so registering a custom control costs nothing up front.\nexport { defineControl, getControl, DEFAULT_ITEMS } from './controls/registry';\nexport type {\n ControlContext,\n ControlDef,\n ControlItem,\n ControlsColorScheme,\n ControlsOptions,\n ControlsPosition,\n} from './controls/types';\n\nexport { ImageSource } from './source/imageSource';\nexport type { ImageSourceOptions, ImageFit } from './source/imageSource';\nexport type { Source, DownloadInfo, LoadProgress, OutlineItem, PageRequest } from './source/types';\n\nexport type { RendererOption, RendererKind } from './renderer/select';\nexport type { Renderer, SpreadContent, PageContent, FlipDirection, LayoutMetrics, RenderOptions } from './renderer/types';\n\nexport type { ZineEventMap } from './engine/emitter';\nexport type { Direction, Spread } from './engine/spread';\nexport { CURL_TYPES, IMPORTABLE_CURLS } from './geometry/curls/types';\nexport type { CurlType, CurlSpec, CurlModel, CurlDeform, CurlAnchor } from './geometry/curls/types';\nexport type { PageMesh } from './geometry/curls/mesh';\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.umd.js","names":["#radius","#mounted","#listeners","#state","#handlers","#activeId","#start","#prev","#last","#end","#reset","#pinching","#points","#pair","#startDistance","#remove","#container","#clip","#viewport","#book","#pageLeft","#makeCanvas","#pageRight","#leaf","#leafFront","#leafBack","#leafShadow","#paint","#fill","#fit","#contentShift","#trackAspect","#layoutBook","#applyShift","#fromShiftUnit","#toShiftUnit","#bookW","#pageAspect","#docAspect","#bookBox","#drawAspect","#contentBox","#container","#canvas","#onContextLost","#onContextRestored","#onVisibilityChange","#gl","#maxTextureSize","#buildGlResources","#clearRestoreTimer","#texCache","#blankTex","#quadVao","#curlVao","#flat","#curl","#cacheBytes","#content","#flip","#fatalHandler","#fill","#fit","#trackAspect","#render","#pageAspect","#docAspect","#flipT","#curlModel","#anchor","#fromShiftUnit","#toShiftUnit","#view","#bookBox","#contentBox","#shiftX","#dpr","#drawAspect","#contextLost","#restoreTimer","#fatal","#fatalFired","#flatU","#curlU","#makeQuadVao","#buildCurlMesh","#makeBlankTexture","#idxCount","#posBuf","#normBuf","#mesh","#resize","#fitBook","#drawFlip","#drawStatic","#bookW","#bookH","#useFlat","#drawQuad","#bindFace","#fillFade","#textureFor","#evictTextures","#base","#front","#back","#rawPages","#resolvePages","#resolved","#image","#cache","#ctorCache","#volume","#url","#muted","#context","#decode","#ctx","#buffer","#gain","#decoding","#loadBuffer","#doc","#lastFocus","#strings","#backdrop","#dialog","#linkRow","#socialRow","#qr","#onKey","#trapFocus","#copy","#timer","#doc","#context","#onAfterAction","#hidden","#items","#doc","#holder","#pageBox","#place","#armDismiss","#trackPage","#fitToPage","#resizeObserver","#wrap","#close","#scrim","#onKeydown","#zine","#doc","#bar","#build","#markCurrent","#unsubscribe","#rows","#observeRows","#paint","#observer","#zine","#doc","#onDismiss","#bar","#note","#build","#unsubscribe","#markCurrent","#addLevel","#rows","#zine","#doc","#onDismiss","#bar","#input","#timer","#search","#hits","#run","#renderResults","#results","#note","#zine","#device","#prev","#button","#next","#place","#wrap","#unsubscribe","#refresh","#edges","#zine","#container","#hidden","#doc","#colorScheme","#position","#root","#bar","#onKeyDown","#isolate","#place","#refresh","#unsubscribe","#popover","#closePopover","#wrap","#build","#arrows","#probeOutline","#panel","#panelKind","#context","#activate","#buttons","#openMenu","#placePopover","#pageInput","#hasOutline","#searchState","#zine","#container","#hidden","#doc","#colorScheme","#items","#open","#unbind","#menu","#close","#context","#place","#renderer","#container","#source","#direction","#deepLinkEnabled","#disableContextMenu","#spreadMode","#configuredMode","#curl","#clickToFlip","#fit","#strings","#clickZoneSize","#cursorHints","#hintsEnabled","#hintsPersist","#loadLearned","#soundEnabled","#soundOptions","#soundMuted","#soundPersist","#loadSoundMuted","#singlePageThreshold","#responsiveSpread","#controlsOption","#contextMenuOption","#hiddenControls","#loadingOption","#startPageOption","#buildSpreadModel","#flipDuration","#zoomEnabled","#wheelZoom","#doubleClickLevels","#maxZoom","#honorDoubleClickInFlipZone","#clickFlipDelayValue","#ready","#init","#currentPage","#spreads","#current","#singlePage","#narrow","#rebuildSpreads","#renderCurrent","#applySinglePage","#getPage","#sameOriginFile","#outline","#isCrossOrigin","#emitter","#stepFlip","#requestFlip","#startFlip","#spreadIndexForPage","#machine","#activeAnim","#queuedFlip","#finishActiveAnim","#raf","#flushPendingUpgrades","#scale","#cancelPeek","#cancelNudge","#tx","#ty","#clampPan","#applyTouchAction","#refreshZoomTiles","#updateCursor","#markLearned","#showPanCaption","#sound","#saveSoundMuted","#loadSound","#destroyed","#clearPendingClickFlip","#clearIdleNudge","#clearZoomHint","#hideCaption","#resizeObserver","#deepLink","#unsubscribeDeepLink","#controlsCleanup","#contextMenuCleanup","#dismissLoader","#a11yCleanup","#unbindWheel","#unbindDblClick","#unbindContextMenu","#unbindCursor","#unbindInput","#tileTimer","#tileGeneration","#leadPage","#playFlipSound","#runFlip","#flipGeneration","#resolveContent","#currentContent","#effectiveCurl","#anchorY","#animateProgress","#commit","#announcePage","#effectiveDuration","#announce","#paintSpread","#prefetchWindow","#lastFlipAt","#drainQueuedFlip","#pinching","#pan","#contentRect","#toBookPoint","#press","#pendingGrab","#drag","#stageDrag","#promoteGrab","#swipeFlip","#maybeClickFlip","#cancelFlip","#canFlip","#playEdgeNudge","#clickFlipDirection","#pendingClickTimer","#pointerClient","#cursorFor","#learned","#reducedMotion","#idleTimer","#playPeek","#peekAnim","#repaintRestingSpread","#nudgeAnim","#showCaption","#captionEl","#captionTimer","#zoomHintShown","#lastPointerType","#zoomHintTimer","#zoomHintText","#pinchBaseScale","#lastClick","#maybeArmZoomHint","#zoomAt","#onLoadProgress","#mountLoader","#setLoaderPhase","#mountWithFallback","#onPageUpdate","#onDragStart","#onDragMove","#onDragEnd","#onPinchStart","#onPinchMove","#onPinchEnd","#bindWheelZoom","#bindDoubleClickZoom","#bindContextMenu","#bindCursorHints","#setupReducedMotion","#setupA11y","#observeResize","#bindDeepLink","#mountControls","#armIdleNudge","#soundLoadStarted","#lastProgress","#loader","#loaderPhase","#fallbackToCss","#fellBack","#onKeyDown","#liveRegion","#effectiveMode","#effectiveModeShown","#applyContainerAspect","#zoomedAt","#lastAspect","#pendingUpgrades","#upgradeScale","#renderZoomTiles","#fitFactor","#scheduleUpdate","#updateScheduled","#virtualizer","#urls","#preload","#decode","#cache"],"sources":["../src/engine/spread.ts","../src/engine/virtualizer.ts","../src/engine/emitter.ts","../src/engine/stateMachine.ts","../src/engine/input.ts","../src/engine/deeplink.ts","../src/geometry/curls/types.ts","../src/geometry/flipProgressToPose.ts","../src/renderer/cssRenderer.ts","../src/geometry/curls/mesh.ts","../src/geometry/curls/cone.ts","../src/geometry/curls/simple.ts","../src/geometry/curls/bundled.ts","../src/renderer/gl/context.ts","../src/renderer/gl/program.ts","../src/renderer/gl/texture.ts","../src/renderer/webglRenderer.ts","../src/renderer/select.ts","../src/source/compose.ts","../src/strings.ts","../src/sound/clip.ts","../src/sound/flipSound.ts","../src/loading/loading.ts","../src/controls/icons.ts","../src/controls/registry.ts","../src/controls/socials.ts","../src/controls/qr.ts","../src/controls/styles.ts","../src/controls/share.ts","../src/controls/builtins.ts","../src/controls/menu.ts","../src/controls/sidebar.ts","../src/controls/thumbnails.ts","../src/controls/outline.ts","../src/controls/search.ts","../src/controls/arrows.ts","../src/controls/toolbar.ts","../src/controls/contextMenu.ts","../src/controls/controls.ts","../src/zine.ts","../src/source/imageSource.ts","../src/index.ts"],"sourcesContent":["export type Direction = 'ltr' | 'rtl';\n\n/**\n * How pages group into spreads:\n * - `double`: paired from the start — `[0|1] [2|3] …`\n * - `single`: one page per spread — `[0] [1] …`\n * - `cover`: first page alone, then paired — `[·|0] [1|2] …`\n * - `book`: first AND last alone (front + back cover) — `[·|0] [1|2] … [N-1|·]`\n */\nexport type SpreadMode = 'double' | 'single' | 'cover' | 'book';\n\nexport interface Spread {\n /** Page index shown on the left, or null for a blank side. */\n left: number | null;\n /** Page index shown on the right, or null for a blank side. */\n right: number | null;\n}\n\nexport interface SpreadOptions {\n direction?: Direction;\n mode?: SpreadMode;\n}\n\n/**\n * Map a flat page list (indices 0..pageCount-1) into display spreads.\n *\n * Reading order is always preserved; `direction` only mirrors each spread's\n * left/right positions, so RTL is just an LTR layout with the sides swapped.\n * Out-of-range slots are `null` (a blank half on odd counts / a lone cover).\n */\nexport function buildSpreads(pageCount: number, options: SpreadOptions = {}): Spread[] {\n const { direction = 'ltr', mode = 'cover' } = options;\n if (pageCount <= 0) return [];\n\n const page = (i: number): number | null => (i >= 0 && i < pageCount ? i : null);\n const spreads: Spread[] = [];\n\n if (mode === 'single') {\n for (let i = 0; i < pageCount; i++) spreads.push({ left: null, right: i });\n } else if (mode === 'double') {\n for (let i = 0; i < pageCount; i += 2) spreads.push({ left: page(i), right: page(i + 1) });\n } else if (mode === 'cover') {\n spreads.push({ left: null, right: 0 });\n for (let i = 1; i < pageCount; i += 2) spreads.push({ left: page(i), right: page(i + 1) });\n } else {\n // book: front cover alone, interior paired, back cover alone\n spreads.push({ left: null, right: 0 });\n if (pageCount > 1) {\n for (let i = 1; i <= pageCount - 2; i += 2) {\n spreads.push({ left: page(i), right: i + 1 <= pageCount - 2 ? page(i + 1) : null });\n }\n spreads.push({ left: pageCount - 1, right: null });\n }\n }\n\n return direction === 'rtl'\n ? spreads.map((s) => ({ left: s.right, right: s.left }))\n : spreads;\n}\n\n/** Whether a container this wide should fall back to one page per spread. */\nexport function shouldSinglePage(containerWidth: number, threshold: number): boolean {\n return containerWidth < threshold;\n}\n","/** Spread indices that should be mounted around `current`, clamped to the book. */\nexport function mountWindow(current: number, spreadCount: number, radius: number): number[] {\n const window: number[] = [];\n const from = Math.max(0, current - radius);\n const to = Math.min(spreadCount - 1, current + radius);\n for (let i = from; i <= to; i++) {\n window.push(i);\n }\n return window;\n}\n\nexport interface VirtualizerDelta {\n toMount: number[];\n toEvict: number[];\n}\n\n/**\n * Tracks which spreads are mounted and, on each move, reports the difference:\n * spreads entering the ±radius window (`toMount`) and those leaving it\n * (`toEvict`). Default radius 2 = current spread ±2.\n */\nexport class Virtualizer {\n #radius: number;\n #mounted = new Set<number>();\n\n constructor(radius = 2) {\n this.#radius = radius;\n }\n\n get mounted(): number[] {\n return [...this.#mounted].sort((a, b) => a - b);\n }\n\n /** Recenter on `current`; mounts/evicts to match the window and returns the delta. */\n update(current: number, spreadCount: number): VirtualizerDelta {\n const window = new Set(mountWindow(current, spreadCount, this.#radius));\n const toMount = [...window].filter((i) => !this.#mounted.has(i)).sort((a, b) => a - b);\n const toEvict = [...this.#mounted].filter((i) => !window.has(i)).sort((a, b) => a - b);\n for (const i of toEvict) this.#mounted.delete(i);\n for (const i of toMount) this.#mounted.add(i);\n return { toMount, toEvict };\n }\n}\n","import type { SpreadMode } from './spread.js';\nimport type { LoadProgress } from '../source/types.js';\n\nexport type Listener<T> = (payload: T) => void;\n\n/** Events typed as `void` are emitted with no payload argument. */\ntype PayloadArgs<T> = [T] extends [void] ? [] : [payload: T];\n\n/** Minimal typed event emitter keyed by an event map. */\nexport class Emitter<Events> {\n #listeners = new Map<keyof Events, Set<Listener<unknown>>>();\n\n /** Subscribe; returns an unsubscribe function. */\n on<K extends keyof Events>(type: K, listener: Listener<Events[K]>): () => void {\n let set = this.#listeners.get(type);\n if (!set) {\n set = new Set();\n this.#listeners.set(type, set);\n }\n set.add(listener as Listener<unknown>);\n return () => this.off(type, listener);\n }\n\n off<K extends keyof Events>(type: K, listener: Listener<Events[K]>): void {\n this.#listeners.get(type)?.delete(listener as Listener<unknown>);\n }\n\n emit<K extends keyof Events>(type: K, ...args: PayloadArgs<Events[K]>): void {\n const payload = args[0] as Events[K];\n // Copy so a listener that unsubscribes mid-emit doesn't disturb iteration.\n const set = this.#listeners.get(type);\n if (!set) return;\n for (const listener of [...set]) {\n (listener as Listener<Events[K]>)(payload);\n }\n }\n\n clear(): void {\n this.#listeners.clear();\n }\n}\n\n/**\n * Public event surface of a Zine instance. Payloads are provisional and will\n * firm up as the engine wires the emitters that fire them.\n */\nexport interface ZineEventMap {\n ready: void;\n flipStart: { from: number; to: number };\n flipEnd: { page: number };\n pageChanged: { page: number };\n /** Pages regrouped: the spread mode changed, or a narrow container forced one page. */\n spreadChanged: { mode: SpreadMode; singlePage: boolean };\n zoomChanged: { scale: number };\n /** The document is downloading. Fires only for sources that report byte progress (a PDF from a\n * URL); use it to drive a custom loading indicator, and hide it on `ready`. */\n progress: LoadProgress;\n sourceError: { index: number; error: unknown };\n rendererFallback: { from: string; to: string };\n}\n\nexport type ZineEmitter = Emitter<ZineEventMap>;\n","export type FlipState = 'idle' | 'dragging' | 'animating' | 'zoomed-panning';\n\nexport type FlipEvent = 'grab' | 'release' | 'settle' | 'panStart' | 'panEnd' | 'flip';\n\n/**\n * Legal transitions. A flip drag runs idle → dragging → animating → idle; a\n * programmatic flip jumps idle → animating directly via `flip`; zoom panning\n * (only reachable when zoomed in) is a separate branch idle → zoomed-panning →\n * idle. Any (state, event) pair absent here is illegal.\n */\nconst TRANSITIONS: Record<FlipState, Partial<Record<FlipEvent, FlipState>>> = {\n idle: { grab: 'dragging', panStart: 'zoomed-panning', flip: 'animating' },\n dragging: { release: 'animating' },\n animating: { settle: 'idle' },\n 'zoomed-panning': { panEnd: 'idle' },\n};\n\n/** Pure transition: the next state, or null if `event` is illegal from `state`. */\nexport function nextState(state: FlipState, event: FlipEvent): FlipState | null {\n return TRANSITIONS[state][event] ?? null;\n}\n\n/** Stateful holder around `nextState`; illegal events are ignored. */\nexport class FlipMachine {\n #state: FlipState;\n\n constructor(initial: FlipState = 'idle') {\n this.#state = initial;\n }\n\n get state(): FlipState {\n return this.#state;\n }\n\n /** Apply `event`; returns the new state, or null (no-op) if the event was illegal. */\n send(event: FlipEvent): FlipState | null {\n const next = nextState(this.#state, event);\n if (next !== null) {\n this.#state = next;\n }\n return next;\n }\n}\n","export interface GestureStart {\n x: number;\n y: number;\n t: number;\n}\n\nexport interface GestureMove {\n x: number;\n y: number;\n /** Displacement from the start point. */\n dx: number;\n dy: number;\n}\n\nexport interface GestureEnd {\n x: number;\n y: number;\n dx: number;\n dy: number;\n /** Elapsed time from start to end (ms). */\n dt: number;\n /** Velocity of the final move segment (px/ms). */\n vx: number;\n vy: number;\n /** A fast flick: final speed exceeded the swipe threshold and the gesture was not canceled. */\n swipe: boolean;\n /** The pointer was canceled (e.g. pointercancel) rather than lifted. */\n canceled: boolean;\n}\n\nexport interface PointerHandlers {\n onStart?: (g: GestureStart) => void;\n onMove?: (g: GestureMove) => void;\n onEnd?: (g: GestureEnd) => void;\n}\n\n/** Flick threshold in px/ms; a release faster than this is a swipe. */\nconst SWIPE_VELOCITY = 0.3;\n\ninterface Sample {\n x: number;\n y: number;\n t: number;\n}\n\n/**\n * Recognizes a single-pointer drag from raw pointer samples and reports normalized\n * gestures. DOM-agnostic on purpose: `bindPointerInput` maps real PointerEvents onto\n * it, but the kinematics stay pure and unit-testable. Extra pointers are ignored\n * (multi-touch pinch is a separate recognizer).\n */\nexport class PointerRecognizer {\n #handlers: PointerHandlers;\n #activeId: number | null = null;\n #start: Sample | null = null;\n #prev: Sample | null = null;\n #last: Sample | null = null;\n\n constructor(handlers: PointerHandlers = {}) {\n this.#handlers = handlers;\n }\n\n get active(): boolean {\n return this.#activeId !== null;\n }\n\n down(id: number, x: number, y: number, t: number): void {\n if (this.#activeId !== null) return; // single-pointer; ignore extra pointers\n this.#activeId = id;\n const s: Sample = { x, y, t };\n this.#start = s;\n this.#prev = s;\n this.#last = s;\n this.#handlers.onStart?.({ x, y, t });\n }\n\n move(id: number, x: number, y: number, t: number): void {\n if (id !== this.#activeId || this.#start === null) return;\n this.#prev = this.#last;\n this.#last = { x, y, t };\n this.#handlers.onMove?.({ x, y, dx: x - this.#start.x, dy: y - this.#start.y });\n }\n\n up(id: number, x: number, y: number, t: number): void {\n this.#end(id, x, y, t, false);\n }\n\n cancel(id: number, x: number, y: number, t: number): void {\n this.#end(id, x, y, t, true);\n }\n\n #end(id: number, x: number, y: number, t: number, canceled: boolean): void {\n if (id !== this.#activeId || this.#start === null) return;\n const start = this.#start;\n // Velocity comes from the last move segment, not the release point — on lift the\n // pointer usually stops, so release-relative velocity would kill every flick.\n const last = this.#last ?? start;\n const prev = this.#prev ?? start;\n const segDt = last.t - prev.t;\n const vx = segDt > 0 ? (last.x - prev.x) / segDt : 0;\n const vy = segDt > 0 ? (last.y - prev.y) / segDt : 0;\n const speed = Math.hypot(vx, vy);\n this.#reset();\n this.#handlers.onEnd?.({\n x,\n y,\n dx: x - start.x,\n dy: y - start.y,\n dt: t - start.t,\n vx,\n vy,\n swipe: !canceled && speed > SWIPE_VELOCITY,\n canceled,\n });\n }\n\n #reset(): void {\n this.#activeId = null;\n this.#start = null;\n this.#prev = null;\n this.#last = null;\n }\n}\n\n/**\n * Wire real PointerEvents from `target` into a recognizer. Returns an unbind\n * function that removes every listener. Coordinates are passed through as\n * client-space; mapping to page-local space is the engine's job.\n */\nexport function bindPointerInput(target: EventTarget, recognizer: PointerRecognizer): () => void {\n const down = (e: Event): void => {\n const p = e as PointerEvent;\n recognizer.down(p.pointerId, p.clientX, p.clientY, p.timeStamp);\n };\n const move = (e: Event): void => {\n const p = e as PointerEvent;\n recognizer.move(p.pointerId, p.clientX, p.clientY, p.timeStamp);\n };\n const up = (e: Event): void => {\n const p = e as PointerEvent;\n recognizer.up(p.pointerId, p.clientX, p.clientY, p.timeStamp);\n };\n const cancel = (e: Event): void => {\n const p = e as PointerEvent;\n recognizer.cancel(p.pointerId, p.clientX, p.clientY, p.timeStamp);\n };\n target.addEventListener('pointerdown', down);\n target.addEventListener('pointermove', move);\n target.addEventListener('pointerup', up);\n target.addEventListener('pointercancel', cancel);\n return () => {\n target.removeEventListener('pointerdown', down);\n target.removeEventListener('pointermove', move);\n target.removeEventListener('pointerup', up);\n target.removeEventListener('pointercancel', cancel);\n };\n}\n\n/**\n * Wire PointerEvents from `target` into a drag recognizer and/or a pinch\n * recognizer at once, so a single pointer stream feeds both. Returns an unbind.\n */\nexport function bindGestures(\n target: EventTarget,\n recognizers: { pointer?: PointerRecognizer; pinch?: PinchRecognizer },\n): () => void {\n const { pointer, pinch } = recognizers;\n const down = (e: Event): void => {\n const p = e as PointerEvent;\n pointer?.down(p.pointerId, p.clientX, p.clientY, p.timeStamp);\n pinch?.down(p.pointerId, p.clientX, p.clientY);\n };\n const move = (e: Event): void => {\n const p = e as PointerEvent;\n pointer?.move(p.pointerId, p.clientX, p.clientY, p.timeStamp);\n pinch?.move(p.pointerId, p.clientX, p.clientY);\n };\n const up = (e: Event): void => {\n const p = e as PointerEvent;\n pointer?.up(p.pointerId, p.clientX, p.clientY, p.timeStamp);\n pinch?.up(p.pointerId);\n };\n const cancel = (e: Event): void => {\n const p = e as PointerEvent;\n pointer?.cancel(p.pointerId, p.clientX, p.clientY, p.timeStamp);\n pinch?.cancel(p.pointerId);\n };\n target.addEventListener('pointerdown', down);\n target.addEventListener('pointermove', move);\n target.addEventListener('pointerup', up);\n target.addEventListener('pointercancel', cancel);\n return () => {\n target.removeEventListener('pointerdown', down);\n target.removeEventListener('pointermove', move);\n target.removeEventListener('pointerup', up);\n target.removeEventListener('pointercancel', cancel);\n };\n}\n\nexport interface PinchStart {\n centerX: number;\n centerY: number;\n /** Distance between the two pointers at pinch start. */\n distance: number;\n}\n\nexport interface PinchMove {\n centerX: number;\n centerY: number;\n /** Current distance / start distance. */\n scale: number;\n}\n\nexport interface PinchHandlers {\n onPinchStart?: (g: PinchStart) => void;\n onPinchMove?: (g: PinchMove) => void;\n onPinchEnd?: () => void;\n}\n\ninterface Pt {\n x: number;\n y: number;\n}\n\nfunction distance(a: Pt, b: Pt): number {\n return Math.hypot(a.x - b.x, a.y - b.y);\n}\n\n/**\n * Recognizes a two-pointer pinch and reports zoom scale (relative to the start\n * distance) and the live center point. No timestamps: pinch drives zoom, not a\n * flick. A third pointer is ignored; lifting either tracked pointer ends the pinch.\n * Coordinating this with the drag recognizer (a second finger should suspend a\n * single-finger drag) is the engine's job.\n */\nexport class PinchRecognizer {\n #handlers: PinchHandlers;\n #points = new Map<number, Pt>();\n #startDistance = 0;\n #pinching = false;\n\n constructor(handlers: PinchHandlers = {}) {\n this.#handlers = handlers;\n }\n\n get pinching(): boolean {\n return this.#pinching;\n }\n\n down(id: number, x: number, y: number): void {\n if (this.#points.has(id) || this.#points.size >= 2) return; // track only two\n this.#points.set(id, { x, y });\n if (this.#points.size === 2) {\n const { a, b } = this.#pair();\n this.#startDistance = distance(a, b);\n this.#pinching = true;\n this.#handlers.onPinchStart?.({\n centerX: (a.x + b.x) / 2,\n centerY: (a.y + b.y) / 2,\n distance: this.#startDistance,\n });\n }\n }\n\n move(id: number, x: number, y: number): void {\n const p = this.#points.get(id);\n if (!p) return;\n p.x = x;\n p.y = y;\n if (!this.#pinching) return;\n const { a, b } = this.#pair();\n this.#handlers.onPinchMove?.({\n centerX: (a.x + b.x) / 2,\n centerY: (a.y + b.y) / 2,\n scale: this.#startDistance > 0 ? distance(a, b) / this.#startDistance : 1,\n });\n }\n\n up(id: number): void {\n this.#remove(id);\n }\n\n cancel(id: number): void {\n this.#remove(id);\n }\n\n #remove(id: number): void {\n if (!this.#points.delete(id)) return;\n if (this.#pinching && this.#points.size < 2) {\n this.#pinching = false;\n this.#startDistance = 0;\n this.#handlers.onPinchEnd?.();\n }\n }\n\n #pair(): { a: Pt; b: Pt } {\n const values = [...this.#points.values()];\n const a = values[0];\n const b = values[1];\n if (!a || !b) throw new Error('Zine: pinch requires two active pointers.');\n return { a, b };\n }\n}\n","/**\n * Keeping the page in the URL, so a link opens where the reader was.\n *\n * The hash is treated as shared ground rather than ours: only a `page` key is read or written,\n * anything else in it is preserved, and updates go through `replaceState` so turning pages does\n * not fill the reader's back button with history entries.\n */\nconst KEY = 'page';\n\n/**\n * Whether some book on this page already owns the hash.\n *\n * Two books sharing one URL would each overwrite the other's page, and the second to load would\n * open on the first one's page. Only the first claims it; the rest still work, they just do not\n * appear in the address bar.\n */\nlet claimed = false;\n\n/** Take the hash if it is going, and report whether this caller got it. */\nexport function claimHash(): boolean {\n if (claimed) return false;\n claimed = true;\n return true;\n}\n\n/** Give the hash back, so a book that replaces this one can own it. */\nexport function releaseHash(): void {\n claimed = false;\n}\n\n/** Read the 1-based page from a hash, as a 0-based index. Null when it carries no page. */\nexport function pageFromHash(hash: string): number | null {\n const params = new URLSearchParams(hash.replace(/^#/, ''));\n const raw = params.get(KEY);\n if (raw === null) return null;\n const page = Number(raw);\n return Number.isInteger(page) && page >= 1 ? page - 1 : null;\n}\n\n/** The hash that names `page` (0-based), leaving any other keys in `hash` untouched. */\nexport function hashWithPage(hash: string, page: number): string {\n const params = new URLSearchParams(hash.replace(/^#/, ''));\n params.set(KEY, String(page + 1));\n return `#${params.toString()}`;\n}\n\nexport interface DeepLinkHandle {\n /** Write the current page into the address bar. */\n push(page: number): void;\n stop(): void;\n}\n\n/**\n * Mirror the reader's page in the URL, and follow it when the URL changes (back/forward, or a\n * pasted link). `onNavigate` fires only for a page the book is not already on.\n */\nexport function bindDeepLink(win: Window, onNavigate: (page: number) => void): DeepLinkHandle {\n // The page the URL is known to describe. Compared against rather than the hash string itself:\n // some environments emit `hashchange` for our own replaceState, and a string comparison there\n // would let the echo through and turn the page a second time.\n let shown: number | null = null;\n const onHashChange = (): void => {\n const page = pageFromHash(win.location.hash);\n if (page === null || page === shown) return; // absent, or already where we are\n shown = page;\n onNavigate(page);\n };\n win.addEventListener('hashchange', onHashChange);\n return {\n push(page) {\n if (page === shown) return;\n shown = page;\n const hash = hashWithPage(win.location.hash, page);\n if (hash === win.location.hash) return;\n // replaceState, not assignment: a reader flipping through fifty pages should not have to\n // press Back fifty times to leave.\n win.history?.replaceState?.(win.history.state, '', hash);\n },\n stop() {\n win.removeEventListener('hashchange', onHashChange);\n },\n };\n}\n","/**\n * Curl type identifiers and the deform contract. Kept free of the deform\n * implementations so the engine (and CSS-only builds) can reference the union and the\n * validation list without pulling the WebGL-only curl math into the eager bundle.\n */\nimport type { PageMesh } from './mesh';\n\n/** Where a corner-anchored curl folds from: y in [0,1], 0 = top edge, 1 = bottom edge. */\nexport interface CurlAnchor {\n y: number;\n /**\n * Full-width lone page (single-page mode). The leaf hinges at the container edge and\n * has no facing half to land on — deforms may soften and delay the flop so the curl\n * reads naturally before the renderer dissolves the sheet.\n */\n fill?: boolean;\n}\n\nexport type CurlDeform = (mesh: PageMesh, W: number, H: number, t: number, anchor: CurlAnchor) => void;\n\nexport interface CurlModel {\n readonly deform: CurlDeform;\n /** True when the fold originates at a corner (`anchor.y`) rather than the whole edge. */\n readonly anchored: boolean;\n /**\n * True when the sheet is flat by mid-turn, so a lone page can start dissolving earlier: there\n * is no curl left to read. Defaults to false, which suits a model still bent as it lands.\n */\n readonly flat?: boolean;\n /**\n * False to drop the specular highlight, for a sheet that never bends and so has no curve to\n * catch the light. Defaults to true.\n */\n readonly gloss?: boolean;\n}\n\n/**\n * The curl models carried in the bundle: the default, and the flat turn reduced motion falls\n * back to. Everything else is imported from `@zinejs/core/curls` and passed as a model, so a\n * book that never uses it never pays for it.\n */\nexport type CurlType = 'cone' | 'simple';\n\nexport const CURL_TYPES: CurlType[] = ['cone', 'simple'];\n\n/**\n * Curl models that exist but are not bundled. Named only so passing one as a string can say what\n * to do about it instead of failing as an unknown value.\n */\nexport const IMPORTABLE_CURLS = ['roll', 'leaf', 'flick', 'silk'];\n\nexport const DEFAULT_CURL: CurlType = 'cone';\n\n/** What the `curl` option accepts: a bundled name, or a model imported (or written) by the caller. */\nexport type CurlSpec = CurlType | CurlModel;\n","export interface FlipPose {\n /** Turning-page rotation about the spine, radians: 0 (flat/open) → π (fully turned). */\n angle: number;\n /** Mesh cylinder-wrap curl, 0..1 — flat at the ends, peaks edge-on at t=0.5. CSS renderer ignores it. */\n curl: number;\n /** Fold-shadow intensity, 0..1 — peaks edge-on at t=0.5. */\n shadowAlpha: number;\n}\n\n/**\n * Map a 0→1 flip progress to a renderer-neutral fold pose.\n *\n * Linear in `t` on purpose — easing belongs to the animator that produces `t`,\n * so seeking to a fixed progress (e.g. the 0/.25/.5/.75/1 visual-regression stops)\n * is deterministic. The CSS renderer consumes `angle` as a spine `rotateY`; the\n * WebGL2 renderer additionally bends its mesh by `curl`.\n */\nexport function flipProgressToPose(t: number): FlipPose {\n const clamped = t < 0 ? 0 : t > 1 ? 1 : t;\n const arc = Math.sin(clamped * Math.PI);\n return {\n angle: clamped * Math.PI,\n curl: arc,\n shadowAlpha: arc,\n };\n}\n","import { flipProgressToPose } from '../geometry/flipProgressToPose';\nimport type { Spread } from '../engine/spread';\nimport type {\n FlipDirection,\n LayoutMetrics,\n PageContent,\n Renderer,\n RenderOptions,\n SpreadContent,\n} from './types';\n\nconst RAD_TO_DEG = 180 / Math.PI;\n\n/** How far to shift a spread to centre a lone page, in units of a quarter container width:\n * -1 = only a right page (cover), +1 = only a left page (back), 0 = a full spread. */\nfunction shiftUnit(content: SpreadContent): number {\n const lone = (content.left === null) !== (content.right === null);\n return lone ? (content.right !== null ? -1 : 1) : 0;\n}\n\n/**\n * Layered gradients painted over the turning leaf to fake paper depth on the flat\n * fold: a crease shadow at the spine, a soft curve sheen, and a free-edge shade.\n * The spine is the leaf's pivot edge — left for a forward flip, right for backward.\n */\nfunction foldOverlay(direction: FlipDirection): string {\n const dir = direction === 'forward' ? 'to right' : 'to left';\n return [\n `linear-gradient(${dir}, rgba(0,0,0,0.6), rgba(0,0,0,0.15) 40%, rgba(0,0,0,0) 60%)`,\n `linear-gradient(${dir}, transparent 40%, rgba(255,255,255,0.32) 66%, transparent 90%)`,\n `linear-gradient(${dir}, transparent 66%, rgba(0,0,0,0.35))`,\n ].join(',');\n}\n\n/**\n * Baseline renderer: a 2-panel CSS-3D fold. Runs on every browser, no GPU. Pages\n * are <canvas> elements; the turning leaf (front + back faces, backface-hidden)\n * rotates about the spine driven by flipProgressToPose(), with a gradient shadow\n * overlay for depth. `beginFlip` stages the surfaces; `setFlipProgress` animates.\n */\nexport class CssRenderer implements Renderer {\n #container: HTMLElement | null = null;\n #clip!: HTMLDivElement;\n #viewport!: HTMLDivElement;\n #book!: HTMLDivElement;\n #pageLeft!: HTMLCanvasElement;\n #pageRight!: HTMLCanvasElement;\n #leaf!: HTMLDivElement;\n #leafFront!: HTMLCanvasElement;\n #leafBack!: HTMLCanvasElement;\n #leafShadow!: HTMLDivElement;\n // Lone-page centering, interpolated across a flip (see the WebGL renderer).\n #fromShiftUnit = 0;\n #toShiftUnit = 0;\n // Book aspect fit: the spread is letterboxed to the page aspect (see the WebGL renderer).\n #pageAspect = 0;\n // First page's aspect, latched: the container is sized to this document-wide so off-size pages\n // do not resize it. `contain` letterboxes them within it; `fill` stretches them to it.\n #docAspect = 0;\n #fit: 'contain' | 'fill' = 'contain';\n #bookW = 0;\n #fill = false;\n #contentShift = 0; // shiftUnit of the current static spread (nonzero = lone page, centered)\n\n mount(container: HTMLElement): Promise<void> {\n const doc = container.ownerDocument;\n this.#container = container;\n\n // Fixed-size clip box; NOT transformed, so zoom can't scale the crop region.\n this.#clip = doc.createElement('div');\n this.#clip.className = 'zine-clip';\n this.#clip.style.cssText = 'position:relative;width:100%;height:100%;overflow:hidden;';\n\n // Transform target for zoom/pan; lives inside the clip.\n this.#viewport = doc.createElement('div');\n this.#viewport.className = 'zine-viewport';\n this.#viewport.style.cssText = 'position:absolute;inset:0;transform-origin:0 0;';\n\n this.#book = doc.createElement('div');\n this.#book.className = 'zine-book';\n this.#book.style.cssText =\n 'position:absolute;inset:0;perspective:2000px;transform-style:preserve-3d;';\n\n this.#pageLeft = this.#makeCanvas(doc, 'zine-page-left', 'left:0;');\n this.#pageRight = this.#makeCanvas(doc, 'zine-page-right', 'right:0;');\n\n this.#leaf = doc.createElement('div');\n this.#leaf.className = 'zine-leaf';\n this.#leaf.style.cssText =\n 'position:absolute;top:0;width:50%;height:100%;transform-style:preserve-3d;display:none;';\n\n this.#leafFront = doc.createElement('canvas');\n this.#leafFront.className = 'zine-leaf-front';\n this.#leafFront.style.cssText =\n 'position:absolute;inset:0;width:100%;height:100%;backface-visibility:hidden;';\n\n this.#leafBack = doc.createElement('canvas');\n this.#leafBack.className = 'zine-leaf-back';\n this.#leafBack.style.cssText =\n 'position:absolute;inset:0;width:100%;height:100%;backface-visibility:hidden;transform:rotateY(180deg);';\n\n this.#leafShadow = doc.createElement('div');\n this.#leafShadow.className = 'zine-leaf-shadow';\n this.#leafShadow.style.cssText = 'position:absolute;inset:0;opacity:0;pointer-events:none;';\n this.#leafShadow.style.background = foldOverlay('forward'); // default; beginFlip sets per direction\n\n this.#leaf.append(this.#leafFront, this.#leafBack, this.#leafShadow);\n this.#book.append(this.#pageLeft, this.#pageRight, this.#leaf);\n this.#viewport.append(this.#book);\n this.#clip.append(this.#viewport);\n container.append(this.#clip);\n\n return Promise.resolve();\n }\n\n destroy(): void {\n this.#clip?.remove();\n this.#container = null;\n }\n\n renderSpread(_spread: Spread, content: SpreadContent, options?: RenderOptions): void {\n if (options?.fill) {\n // Single-page mode: the lone page fills the container; hide the other panel.\n this.#pageLeft.style.width = '100%';\n this.#pageRight.style.display = 'none';\n this.#paint(this.#pageLeft, content.right ?? content.left);\n } else {\n this.#pageLeft.style.width = '50%';\n this.#pageRight.style.display = '';\n this.#paint(this.#pageLeft, content.left);\n this.#paint(this.#pageRight, content.right);\n }\n this.#fill = options?.fill ?? false;\n if (options?.fit) this.#fit = options.fit;\n this.#contentShift = this.#fill ? 0 : shiftUnit(content);\n this.#trackAspect(content);\n this.#layoutBook();\n this.#applyShift(this.#contentShift);\n // A fresh spread cancels any in-progress flip.\n this.#leaf.style.display = 'none';\n this.#leaf.style.transform = '';\n this.#leafShadow.style.opacity = '0';\n }\n\n beginFlip(\n from: SpreadContent,\n to: SpreadContent,\n direction: FlipDirection,\n options?: RenderOptions,\n ): void {\n if (options?.fill) {\n // Single-page: a full-width leaf turns about one edge, revealing `to` underneath.\n this.#pageLeft.style.width = '100%';\n this.#pageRight.style.display = 'none';\n this.#leaf.style.width = '100%';\n this.#leaf.style.left = '0';\n this.#leaf.style.transformOrigin = direction === 'forward' ? 'left center' : 'right center';\n this.#paint(this.#pageLeft, to.right ?? to.left);\n this.#paint(this.#leafFront, from.right ?? from.left);\n this.#paint(this.#leafBack, to.right ?? to.left);\n } else {\n this.#pageLeft.style.width = '50%';\n this.#pageRight.style.display = '';\n this.#leaf.style.width = '50%';\n if (direction === 'forward') {\n // Right page lifts and swings left about the spine. Left stays; right reveals `to`.\n this.#paint(this.#pageLeft, from.left);\n this.#paint(this.#pageRight, to.right);\n this.#paint(this.#leafFront, from.right);\n this.#paint(this.#leafBack, to.left);\n this.#leaf.style.left = '50%';\n this.#leaf.style.transformOrigin = 'left center';\n } else {\n // Left page swings right about its right edge. Right stays; left reveals `to`.\n this.#paint(this.#pageRight, from.right);\n this.#paint(this.#pageLeft, to.left);\n this.#paint(this.#leafFront, from.left);\n this.#paint(this.#leafBack, to.right);\n this.#leaf.style.left = '0';\n this.#leaf.style.transformOrigin = 'right center';\n }\n }\n this.#fromShiftUnit = options?.fill ? 0 : shiftUnit(from);\n this.#toShiftUnit = options?.fill ? 0 : shiftUnit(to);\n this.#fill = options?.fill ?? false;\n if (options?.fit) this.#fit = options.fit;\n this.#trackAspect(to);\n this.#layoutBook();\n this.#applyShift(this.#fromShiftUnit);\n this.#leaf.style.display = 'block';\n this.#leaf.style.transform = 'rotateY(0deg)';\n this.#leafShadow.style.background = foldOverlay(direction);\n this.#leafShadow.style.opacity = '0';\n }\n\n setFlipProgress(t: number, direction: FlipDirection): void {\n const pose = flipProgressToPose(t);\n const deg = pose.angle * RAD_TO_DEG;\n this.#applyShift(this.#fromShiftUnit + (this.#toShiftUnit - this.#fromShiftUnit) * t);\n this.#leaf.style.display = 'block';\n this.#leaf.style.transform = `rotateY(${direction === 'forward' ? -deg : deg}deg)`;\n this.#leafShadow.style.opacity = String(pose.shadowAlpha);\n }\n\n #applyShift(unit: number): void {\n const px = unit * (this.#bookW / 4);\n this.#book.style.transform = px ? `translateX(${px}px)` : '';\n }\n\n /** Remember the current spread's page aspect (for drawing) and latch the first page's aspect (for\n * the container). The container follows #docAspect so off-size pages do not resize it. */\n #trackAspect(content: SpreadContent): void {\n const p = content.left ?? content.right;\n if (p && p.height) {\n this.#pageAspect = p.width / p.height;\n if (this.#docAspect === 0) this.#docAspect = this.#pageAspect;\n }\n }\n\n /** Aspect the book is drawn at: the current page under `contain` (letterboxed within the stable\n * container), or the document aspect under `fill` (fills it, stretching off-size pages). */\n #drawAspect(): number {\n return this.#fit === 'fill' && this.#docAspect > 0 ? this.#docAspect : this.#pageAspect;\n }\n\n /** Letterbox the 2-page book to the page aspect, centered in the container, so pages\n * fill their halves without stretching; the surround is the container background. */\n #layoutBook(): void {\n const b = this.#bookBox();\n this.#bookW = b.width;\n const s = this.#book.style;\n s.inset = 'auto';\n s.left = `${b.x}px`;\n s.top = `${b.y}px`;\n s.width = `${b.width}px`;\n s.height = `${b.height}px`;\n }\n\n /** The fitted book rect in container px (letterbox aware) — a stable 2-page area. */\n #bookBox(): { x: number; y: number; width: number; height: number } {\n const cw = this.#container?.clientWidth ?? 0;\n const ch = this.#container?.clientHeight ?? 0;\n const aspect = this.#drawAspect();\n if (aspect <= 0 || cw <= 0 || ch <= 0) return { x: 0, y: 0, width: cw, height: ch };\n const ba = (this.#fill ? 1 : 2) * aspect;\n let bw = cw;\n let bh = ch;\n if (ba > cw / ch) bh = cw / ba;\n else bw = ch * ba;\n return { x: (cw - bw) / 2, y: (ch - bh) / 2, width: bw, height: bh };\n }\n\n /** Where the current spread is actually painted: the book, or its centered half for a lone\n * page (mirrors #applyShift). This is what the engine hit-tests against. */\n #contentBox(): { x: number; y: number; width: number; height: number } {\n const b = this.#bookBox();\n return this.#contentShift !== 0 ? { x: b.x + b.width / 4, y: b.y, width: b.width / 2, height: b.height } : b;\n }\n\n setViewTransform(scale: number, x: number, y: number): void {\n this.#viewport.style.transform = `translate(${x}px, ${y}px) scale(${scale})`;\n }\n\n measure(): LayoutMetrics {\n const el = this.#container;\n const w = el?.clientWidth ?? 0;\n const h = el?.clientHeight ?? 0;\n const known = this.#pageAspect > 0;\n const book = known ? this.#bookBox() : undefined;\n const content = known ? this.#contentBox() : undefined;\n // The shift lives on #book, inside the scaled #viewport, so unlike the WebGL2 renderer it\n // magnifies along with everything else and the whole box simply scales.\n const screenAt = content\n ? (scale: number): { x: number; y: number; width: number; height: number } => ({\n x: content.x * scale,\n y: content.y * scale,\n width: content.width * scale,\n height: content.height * scale,\n })\n : undefined;\n return {\n containerWidth: w,\n containerHeight: h,\n pageWidth: w / 2,\n pageHeight: h,\n book,\n content,\n screenAt,\n containerAspect: this.#docAspect > 0 ? (this.#fill ? 1 : 2) * this.#docAspect : undefined,\n };\n }\n\n #makeCanvas(doc: Document, className: string, extra: string): HTMLCanvasElement {\n const c = doc.createElement('canvas');\n c.className = className;\n c.style.cssText = `position:absolute;top:0;width:50%;height:100%;${extra}`;\n return c;\n }\n\n #paint(canvas: HTMLCanvasElement, content: PageContent | null): void {\n const ctx = canvas.getContext('2d');\n if (content === null) {\n ctx?.clearRect(0, 0, canvas.width, canvas.height);\n return;\n }\n canvas.width = content.width;\n canvas.height = content.height;\n ctx?.drawImage(content, 0, 0);\n }\n}\n","/**\n * Shared grid mesh + normal utilities for the page-curl models.\n *\n * A curl model fills `positions`/`normals` in place each frame; UVs are static grid\n * coordinates (no texture swimming). All models bend the same (cols+1)×(rows+1) grid.\n */\n\nexport interface PageMesh {\n readonly cols: number;\n readonly rows: number;\n /** ((cols+1)*(rows+1))*2 — static grid UVs (u across width, v down height). */\n readonly uvs: Float32Array;\n /** *3 — deformed positions, updated by the curl model. */\n readonly positions: Float32Array;\n /** *3 — per-vertex normals, updated by the curl model. */\n readonly normals: Float32Array;\n}\n\n/** Allocate a (cols+1)×(rows+1) grid mesh with static UVs (positions/normals filled by a curl). */\nexport function createPageMesh(cols: number, rows: number): PageMesh {\n const nx = cols + 1;\n const ny = rows + 1;\n const count = nx * ny;\n const uvs = new Float32Array(count * 2);\n for (let j = 0; j < ny; j++) {\n for (let i = 0; i < nx; i++) {\n const k = j * nx + i;\n uvs[k * 2] = i / cols;\n uvs[k * 2 + 1] = j / rows;\n }\n }\n return {\n cols,\n rows,\n uvs,\n positions: new Float32Array(count * 3),\n normals: new Float32Array(count * 3),\n };\n}\n\n/** Per-vertex normals from central differences of the deformed grid (front = +z when flat). */\nexport function computeNormals(mesh: PageMesh): void {\n const nx = mesh.cols + 1;\n const ny = mesh.rows + 1;\n const pos = mesh.positions;\n const nrm = mesh.normals;\n\n for (let j = 0; j < ny; j++) {\n for (let i = 0; i < nx; i++) {\n const iPrev = i > 0 ? i - 1 : i;\n const iNext = i < nx - 1 ? i + 1 : i;\n const jPrev = j > 0 ? j - 1 : j;\n const jNext = j < ny - 1 ? j + 1 : j;\n\n const a = (j * nx + iNext) * 3;\n const b = (j * nx + iPrev) * 3;\n const c = (jNext * nx + i) * 3;\n const e = (jPrev * nx + i) * 3;\n\n const tuX = pos[a]! - pos[b]!;\n const tuY = pos[a + 1]! - pos[b + 1]!;\n const tuZ = pos[a + 2]! - pos[b + 2]!;\n const tvX = pos[c]! - pos[e]!;\n const tvY = pos[c + 1]! - pos[e + 1]!;\n const tvZ = pos[c + 2]! - pos[e + 2]!;\n\n let nx0 = tuY * tvZ - tuZ * tvY;\n let ny0 = tuZ * tvX - tuX * tvZ;\n let nz0 = tuX * tvY - tuY * tvX;\n const l = Math.hypot(nx0, ny0, nz0) || 1;\n nx0 /= l;\n ny0 /= l;\n nz0 /= l;\n\n const k = (j * nx + i) * 3;\n nrm[k] = nx0;\n nrm[k + 1] = ny0;\n nrm[k + 2] = nz0;\n }\n }\n}\n","import { computeNormals, type PageMesh } from './mesh';\nimport type { CurlAnchor } from './types';\n\n/**\n * Cone angle at peak curl. π/2 is flat; smaller values wind the sheet tighter.\n * ~30° gives a soft, paper-like ridge without self-intersection on typical page\n * aspect ratios (Hong / Nuon / Project Austin all land in the 18–35° band).\n */\nconst THETA_MIN = (30 * Math.PI) / 180;\n\n/**\n * How far past the page the cone apex sits, as a fraction of height, at rest.\n * Closer mid-turn sharpens the diagonal; farther reads more cylindrical.\n * Kept moderately distant so the sheet stays page-like without cloth-y sliding.\n */\nconst APEX_REST = 1.35;\nconst APEX_PEAK = 0.7;\n\n/**\n * Above this |anchor.y − 0.5| the deform is a pure cone; at the mid-edge it\n * blends fully to a cylinder (Project Austin’s conicContribution).\n */\nconst CORNER_PURE = 0.42;\n\n/**\n * Curl envelope ends before t=1 so the last stretch is a flat spine settle.\n * Holding sin(πt) curl into the 0.9s left a tall residual ridge that deflated\n * while the free edge was already nearly home — that read as a rebound bounce.\n */\nconst CURL_END = 0.84;\n\n/**\n * Spine rotation reaches ±π before t=1 and holds. Flip easing decelerates hard into\n * the end (a near-still tail after PAGE_LEAD); finishing the flop early keeps that\n * tail from slowly dropping the leaf into place (the settle/rebound feel).\n * Same schedule for lone pages: the dissolve then covers a still, landed sheet\n * rather than a late whip off the hinge.\n */\nconst RHO_END = 0.88;\n\n/**\n * Natural conical page curl — the Xerox PARC / iBooks model.\n *\n * The leaf wraps an imaginary right circular cone whose apex rides the spine and\n * whose opening angle θ breathes over the turn (Hong, Card & Chen, 2004). A corner\n * grab yields a diagonal cone; a mid-edge grab blends toward a cylinder so the\n * free edge lifts as one. The curled sheet then rotates about the spine and settles\n * flat on the far side. Arc length from the apex is preserved, so the paper never\n * stretches — the motion reads as stiff, premium stock rather than cloth.\n *\n * Lone pages (`anchor.fill`) use the same cone and flop as a spread half; the\n * renderer hinges at the container edge and dissolves the leaf as it finishes.\n * Softening or delaying the fill path made single-page turns read as a peel-fade\n * instead of a page turn.\n */\nexport function deformCone(mesh: PageMesh, W: number, H: number, t: number, anchor: CurlAnchor): void {\n const nx = mesh.cols + 1;\n const ny = mesh.rows + 1;\n const pos = mesh.positions;\n\n // Curl envelope: 0 at the ends (flat), 1 near mid-turn. Span ends at CURL_END\n // so the ridge is gone before the spine holds the landed pose through the ease tail.\n const curlT = t >= CURL_END ? 1 : t / CURL_END;\n const curl = Math.sin(Math.PI * curlT);\n\n // θ: π/2 (identity) → THETA_MIN (pronounced curl) → π/2.\n const theta = Math.PI / 2 - curl * (Math.PI / 2 - THETA_MIN);\n const sinTheta = Math.sin(theta);\n const cosTheta = Math.cos(theta);\n const invSinTheta = 1 / Math.max(sinTheta, 1e-6);\n\n // Apex distance: pulled in as the curl develops for a tighter diagonal.\n const apexDist = H * (APEX_REST + (APEX_PEAK - APEX_REST) * curl);\n\n // Corner bias: −1 = top edge, +1 = bottom. Near 0 → cylinder blend.\n const bias = (anchor.y - 0.5) * 2;\n const fromBottom = bias >= 0;\n const absBias = Math.abs(bias);\n let conic = Math.min(1, absBias / CORNER_PURE);\n const cylWeight = 1 - conic;\n\n // Cylinder radius matched to the cone’s free-edge cross-section (Austin), so\n // the blend is continuous as the grab slides from corner to mid-edge.\n // Apex sits past the *leading* edge (below for a bottom grab, above for top).\n const AyRef = fromBottom ? H + apexDist : -apexDist;\n const cylR = Math.hypot(W, 0.5 * H - AyRef);\n const cylRadius = Math.max(cylR * sinTheta, W / Math.PI);\n\n // Spine rotation: concurrent with the cone so the page flops while it curls.\n // Same x–z convention as `roll` (ρ: 0 → −π). Finish by RHO_END and hold so the\n // ease-out / dissolve tail stays still (spread land or lone fade alike).\n // `anchor.fill` is a renderer concern; geometry matches a spread half.\n const rhoT = Math.min(1, t / RHO_END);\n const rho = -Math.PI * rhoT;\n const cosR = Math.cos(rho);\n const sinR = Math.sin(rho);\n\n // Near the rests the cone is the identity plane. A finite-radius cylinder blend\n // still carries a tiny residual arc — skip it and rotate the flat sheet.\n if (curl < 1e-4) {\n for (let j = 0; j < ny; j++) {\n const y = (j / mesh.rows) * H;\n for (let i = 0; i < nx; i++) {\n const x0 = (i / mesh.cols) * W;\n const k = (j * nx + i) * 3;\n pos[k] = x0 * cosR;\n pos[k + 1] = y;\n pos[k + 2] = -x0 * sinR;\n }\n }\n computeNormals(mesh);\n return;\n }\n\n for (let j = 0; j < ny; j++) {\n const y0 = (j / mesh.rows) * H;\n for (let i = 0; i < nx; i++) {\n const x0 = (i / mesh.cols) * W;\n\n let cx = x0;\n let cy = y0;\n let cz = 0;\n\n if (conic > 1e-5) {\n // Canonical cone: apex above the page (A < 0) makes the *near* edge lead.\n // In our y-down leaf, that means the top. For a bottom grab we flip into\n // that space, deform, then flip back — one code path for both corners.\n const yIn = fromBottom ? H - y0 : y0;\n const A = -apexDist;\n const R = Math.hypot(x0, yIn - A);\n if (R > 1e-8) {\n const r = R * sinTheta;\n const beta = Math.asin(Math.min(1, Math.max(0, x0 / R))) * invSinTheta;\n const oneCos = 1 - Math.cos(beta);\n const yCone = R + A - r * oneCos * sinTheta;\n cx = r * Math.sin(beta);\n cy = fromBottom ? H - yCone : yCone;\n cz = r * oneCos * cosTheta;\n }\n }\n\n let bx = cx;\n let by = cy;\n let bz = cz;\n\n if (cylWeight > 1e-5) {\n const beta = x0 / cylRadius;\n const sx = cylRadius * Math.sin(beta);\n const sz = cylRadius * (1 - Math.cos(beta));\n bx = conic * cx + cylWeight * sx;\n by = conic * cy + cylWeight * y0;\n bz = conic * cz + cylWeight * sz;\n }\n\n const k = (j * nx + i) * 3;\n pos[k] = bx * cosR + bz * sinR;\n pos[k + 1] = by;\n pos[k + 2] = -bx * sinR + bz * cosR;\n }\n }\n\n computeNormals(mesh);\n}\n","import { computeNormals, type PageMesh } from './mesh';\n\n/**\n * Flat page turn — the plainest flip, matching the CSS renderer. The leaf stays a rigid\n * flat sheet and rotates around the spine (angle 0→π), going edge-on at the midpoint. No\n * cylinder bend; GPU-rendered here with the shared perspective so it reads like the CSS fold.\n */\nexport function deformSimple(mesh: PageMesh, W: number, H: number, t: number): void {\n const nx = mesh.cols + 1;\n const ny = mesh.rows + 1;\n const pos = mesh.positions;\n\n const angle = t * Math.PI;\n const cosA = Math.cos(angle);\n const sinA = Math.sin(angle);\n\n for (let i = 0; i < nx; i++) {\n const x0 = (i / mesh.cols) * W; // distance from the spine\n const X = x0 * cosA;\n const Z = x0 * sinA;\n for (let j = 0; j < ny; j++) {\n const k = (j * nx + i) * 3;\n pos[k] = X;\n pos[k + 1] = (j / mesh.rows) * H;\n pos[k + 2] = Z;\n }\n }\n\n computeNormals(mesh);\n}\n","import { deformCone } from './cone';\nimport { deformSimple } from './simple';\nimport { IMPORTABLE_CURLS, type CurlModel, type CurlSpec, type CurlType } from './types';\n\n/**\n * The curl models carried in the bundle, and the only ones nameable by string.\n *\n * Kept apart from `./index` so importing them does not drag in the other four: the barrel names\n * every model, which is right for a caller picking one but wrong for the renderer, which must\n * only ever pull these two.\n */\nexport const cone: CurlModel = { deform: deformCone, anchored: true };\nexport const simple: CurlModel = {\n deform: deformSimple,\n anchored: false,\n flat: true,\n gloss: false,\n};\n\nexport const BUNDLED_CURLS: Record<CurlType, CurlModel> = { cone, simple };\n\n/**\n * Turn a `curl` option into a model. Strings name a bundled curl; anything else is already a\n * model, imported from `@zinejs/core/curls` or written by the caller.\n */\nexport function resolveCurl(spec: CurlSpec): CurlModel {\n if (typeof spec !== 'string') return spec;\n const model = BUNDLED_CURLS[spec];\n if (model) return model;\n // Reached only when validation was bypassed (a renderer used directly, say), so name the fix\n // rather than the mistake: these curls are real, they just are not bundled.\n if (IMPORTABLE_CURLS.includes(spec)) {\n throw new Error(\n `Zine: the '${spec}' curl is not bundled. Import it and pass the model: ` +\n `import { ${spec} } from '@zinejs/core/curls' → curl: ${spec}`,\n );\n }\n throw new Error(`Zine: unknown curl ${JSON.stringify(spec)}.`);\n}\n","/** Create a WebGL2 context, or throw so the caller can fall back to CSS (§8.4). */\nexport function createGlContext(canvas: HTMLCanvasElement): WebGL2RenderingContext {\n const gl = canvas.getContext('webgl2', {\n alpha: true,\n premultipliedAlpha: true,\n antialias: true,\n depth: false,\n stencil: false,\n // We render on demand (not every frame), so the buffer must persist between\n // composites — otherwise the settled page flickers once the flip animation\n // stops and nothing is redrawing each frame.\n preserveDrawingBuffer: true,\n });\n if (gl === null) {\n throw new Error('WebglRenderer: could not create a WebGL2 context.');\n }\n return gl;\n}\n","/** Compile + link a shader program; throws with the driver log on failure. */\nexport function createProgram(\n gl: WebGL2RenderingContext,\n vertexSrc: string,\n fragmentSrc: string,\n): WebGLProgram {\n const program = gl.createProgram();\n if (program === null) throw new Error('WebglRenderer: could not create a program.');\n\n const vs = compileShader(gl, gl.VERTEX_SHADER, vertexSrc);\n const fs = compileShader(gl, gl.FRAGMENT_SHADER, fragmentSrc);\n gl.attachShader(program, vs);\n gl.attachShader(program, fs);\n gl.linkProgram(program);\n\n // Shaders can be detached/deleted once linked.\n gl.deleteShader(vs);\n gl.deleteShader(fs);\n\n if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n const log = gl.getProgramInfoLog(program);\n gl.deleteProgram(program);\n throw new Error(`WebglRenderer: program link failed: ${log ?? 'unknown error'}`);\n }\n return program;\n}\n\nfunction compileShader(gl: WebGL2RenderingContext, type: number, src: string): WebGLShader {\n const shader = gl.createShader(type);\n if (shader === null) throw new Error('WebglRenderer: could not create a shader.');\n gl.shaderSource(shader, src);\n gl.compileShader(shader);\n if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n const log = gl.getShaderInfoLog(shader);\n gl.deleteShader(shader);\n throw new Error(`WebglRenderer: shader compile failed: ${log ?? 'unknown error'}`);\n }\n return shader;\n}\n","/** Create a page texture with clamp + linear filtering (no mipmaps — §8.3). */\nexport function createTexture(gl: WebGL2RenderingContext): WebGLTexture {\n const tex = gl.createTexture();\n if (tex === null) throw new Error('WebglRenderer: could not create a texture.');\n gl.bindTexture(gl.TEXTURE_2D, tex);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);\n return tex;\n}\n\n/**\n * Upload a decoded page raster (ImageBitmap or canvas) into `tex`, returning the dimensions actually\n * uploaded (which differ from the source when it was too big for the GPU).\n *\n * A raster larger than `gl.MAX_TEXTURE_SIZE` on either axis cannot be uploaded: `texImage2D` fails\n * and leaves the texture incomplete, which samples as solid black. A zoomed page on a high-DPR phone\n * (small MAX_TEXTURE_SIZE, e.g. 4096) hits this readily. Downscale over-large rasters to fit,\n * preserving aspect, so a slightly-softer page beats a black one.\n */\nexport function uploadTexture(\n gl: WebGL2RenderingContext,\n tex: WebGLTexture,\n source: TexImageSource,\n): { width: number; height: number } {\n const max = gl.getParameter(gl.MAX_TEXTURE_SIZE) as number;\n const upload = fitToLimit(source, max);\n gl.bindTexture(gl.TEXTURE_2D, tex);\n // No Y flip: the vertex shader maps aUnit.y=0 to the page top and samples the\n // source's top row there. UNPACK_FLIP_Y is honored for <canvas> sources but\n // IGNORED for ImageBitmap, so flipping would invert PDF (canvas) pages while\n // leaving image (ImageBitmap) pages upright. Off keeps both consistent.\n gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);\n gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);\n gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, upload);\n return { width: upload.width, height: upload.height };\n}\n\n/** Return `source` unchanged if it fits `max`, else a canvas holding it scaled down to fit. */\nfunction fitToLimit(source: TexImageSource, max: number): TexImageSource & { width: number; height: number } {\n const dims = source as unknown as { width: number; height: number };\n const longest = Math.max(dims.width, dims.height);\n if (!Number.isFinite(max) || max <= 0 || longest <= max) {\n return dims as TexImageSource & { width: number; height: number };\n }\n const factor = max / longest;\n const canvas = document.createElement('canvas');\n canvas.width = Math.max(1, Math.floor(dims.width * factor));\n canvas.height = Math.max(1, Math.floor(dims.height * factor));\n canvas.getContext('2d')?.drawImage(source as CanvasImageSource, 0, 0, canvas.width, canvas.height);\n return canvas;\n}\n","// Imported from the leaf modules, not the barrel: the barrel pulls in every curl, which would\n// defeat the point of only bundling the two the engine can name.\nimport { createPageMesh, type PageMesh } from '../geometry/curls/mesh';\nimport { BUNDLED_CURLS, resolveCurl } from '../geometry/curls/bundled';\nimport { DEFAULT_CURL, type CurlAnchor, type CurlModel, type CurlSpec } from '../geometry/curls/types';\nimport type { Spread } from '../engine/spread';\nimport type {\n FlipDirection,\n LayoutMetrics,\n Renderer,\n RenderOptions,\n SpreadContent,\n} from './types';\nimport { createGlContext } from './gl/context';\nimport { createProgram } from './gl/program';\nimport { createTexture, uploadTexture } from './gl/texture';\n\n// Flat program: a page rect (device px, top-left origin) with zoom/pan and a soft\n// permanent gutter shadow toward the spine so the turning leaf's shadow lands into it.\nconst FLAT_VERT = `#version 300 es\nin vec2 aUnit;\nuniform vec2 uViewport;\nuniform vec4 uRect;\nuniform vec3 uView;\nout vec2 vUv;\nvoid main() {\n vec2 px = uRect.xy + aUnit * uRect.zw;\n px = px * uView.x + uView.yz;\n vec2 clip = px / uViewport * 2.0 - 1.0;\n gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0);\n vUv = aUnit;\n}`;\n\nconst FLAT_FRAG = `#version 300 es\nprecision mediump float;\nin vec2 vUv;\nuniform sampler2D uTex;\nuniform float uGutterSide; // +1 = spine at right edge, -1 = at left edge, 0 = none\nout vec4 outColor;\nvoid main() {\n vec4 c = texture(uTex, vUv);\n if (uGutterSide != 0.0) {\n float d = uGutterSide > 0.0 ? (1.0 - vUv.x) : vUv.x;\n c.rgb *= mix(0.72, 1.0, smoothstep(0.0, 0.10, d));\n }\n outColor = c;\n}`;\n\n// Curl program: a thin pass-through for the CPU-deformed page mesh (positions and\n// normals come from the selected curl model). It places the leaf, applies a top-safe depth\n// perspective, and lights from the true surface normal.\nconst CURL_VERT = `#version 300 es\nin vec3 aPos; // leaf-local device px: x = bent offset from spine, y 0..H, z depth\nin vec3 aNormal;\nin vec2 aUv;\nuniform vec2 uViewport;\nuniform vec3 uView; // scale, tx, ty\nuniform float uOriginX; // spine x in device px\nuniform float uDir; // +1 leaf to the right of spine, -1 to the left\nuniform float uLeafW; // leaf width in device px (perspective scale)\nout vec2 vUv;\nout float vFacing;\nout float vU;\nvoid main() {\n float bookX = uOriginX + uDir * aPos.x;\n float bookY = aPos.y;\n float Z = aPos.z;\n // Eye distance, in leaf widths. Keeps the perspective resolution-independent. 5.3 puts the\n // roll's deepest point ~14% larger than flat; stronger than that (a nearer eye) balloons the\n // turning sheet and reads more like a fisheye than a page lifting.\n float D = uLeafW * 5.3;\n float persp = D / max(D - Z, 1.0);\n float cx = uViewport.x * 0.5;\n float cy = uViewport.y * 0.5;\n float px = cx + (bookX - cx) * persp;\n float py = cy + (bookY - cy) * min(persp, 1.0); // Y only shrinks (top-safe)\n vec2 p = vec2(px, py) * uView.x + uView.yz;\n vec2 clip = p / uViewport * 2.0 - 1.0;\n gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0);\n vUv = aUv;\n vFacing = aNormal.z;\n vU = aUv.x;\n}`;\n\nconst CURL_FRAG = `#version 300 es\nprecision mediump float;\nin vec2 vUv;\nin float vFacing;\nin float vU;\nuniform sampler2D uFront;\nuniform sampler2D uBack;\nuniform highp float uDir;\nuniform float uGloss; // 0 disables the specular highlight (e.g. the flat 'simple' curl)\nuniform float uAlpha; // leaf opacity; a lone page dissolves into the page landing beneath it\nuniform float uCrease; // spine-crease strength: 1 matches a spread's gutter; ramps from 0 for a lone leaf\nout vec4 outColor;\nvoid main() {\n // The turn mesh carries its own facing in the normal; the front points at the viewer\n // (normal.z > 0) until the leaf passes vertical, for both flip directions. u->x still\n // flips with uDir so a left-hand leaf reads spine-inward.\n bool showFront = vFacing > 0.0;\n float fx = uDir > 0.0 ? vUv.x : 1.0 - vUv.x;\n vec4 c = showFront ? texture(uFront, vec2(fx, vUv.y)) : texture(uBack, vec2(1.0 - fx, vUv.y));\n\n // Fold shading: the sheet darkens sharply where it curves away from the viewer, so the\n // rolled edge reads as a deep crease with a soft highlight riding the ridge.\n float diff = clamp(abs(vFacing), 0.0, 1.0);\n float lit = mix(0.42, 1.0, diff);\n // Spine crease matches the gutter shadow on a spread. A lone page has no gutter, so uCrease\n // ramps it in from 0 as the leaf lifts — otherwise a still-flat leaf paints a shadow band on\n // the anchor edge before the turn even begins.\n lit *= mix(1.0, mix(0.7, 1.0, smoothstep(0.0, 0.12, vU)), uCrease);\n\n // Glossy specular: brightest where the surface tilts ~30 deg from facing the viewer,\n // so a thin glossy highlight rides across the sheet as it rolls.\n float spec = pow(max(cos(acos(diff) - 0.52), 0.0), 220.0) * 0.22 * uGloss;\n\n c.rgb = clamp(c.rgb * lit + spec, 0.0, 1.0);\n // Premultiplied alpha (blendFunc ONE, ONE_MINUS_SRC_ALPHA): scale colour as well as alpha,\n // or fading would brighten the sheet additively instead of dissolving it.\n outColor = vec4(c.rgb * uAlpha, c.a * uAlpha);\n}`;\n\nconst GRID_COLS = 28;\nconst GRID_ROWS = 36;\n/** Progress at which a lone page starts dissolving. A full-width leaf has no facing half to\n * flop onto, so instead of landing on empty space it fades into the page arriving beneath it\n * over the rest of the turn. Roll is already flat by mid-turn; curling models stay bent\n * longer, so they dissolve later. Timed so the fade covers the still landed pose (curl RHO_END)\n * rather than washing out a sheet that is still swinging. */\nconst FILL_FADE_START_ROLL = 0.7;\nconst FILL_FADE_START_CURL = 0.78;\nconst RESTORE_TIMEOUT_MS = 4000;\nconst MAX_TEXTURE_CACHE_BYTES = 256 * 1024 * 1024;\n\ninterface Rect {\n x: number;\n y: number;\n w: number;\n h: number;\n}\n\ninterface FlipState {\n underLeft: TexImageSource | null;\n underRight: TexImageSource | null;\n underFull: TexImageSource | null; // fill mode\n front: TexImageSource | null;\n back: TexImageSource | null;\n dir: number; // +1 forward (leaf right of spine), -1 backward (left)\n fill: boolean;\n}\n\n/** How far to shift a spread to centre a lone page, in units of a quarter container width:\n * -1 = only a right page (cover), +1 = only a left page (back), 0 = a full spread. */\nfunction shiftUnit(content: SpreadContent): number {\n const lone = (content.left === null) !== (content.right === null);\n return lone ? (content.right !== null ? -1 : 1) : 0;\n}\n\n/**\n * GPU renderer (WebGL2, hand-written). The turning leaf is a dense grid deformed on\n * the CPU by the selected curl model (see geometry/curls) and\n * uploaded each frame, lit from real normals; the static pages are flat quads with a\n * gutter shadow. Restores\n * GL state on context loss; signals onFatal (→ CSS fallback) when unrecoverable.\n */\nexport class WebglRenderer implements Renderer {\n #container: HTMLElement | null = null;\n #canvas: HTMLCanvasElement | null = null;\n #gl: WebGL2RenderingContext | null = null;\n\n #flat: WebGLProgram | null = null;\n #curl: WebGLProgram | null = null;\n #quadVao: WebGLVertexArrayObject | null = null;\n #curlVao: WebGLVertexArrayObject | null = null;\n #posBuf: WebGLBuffer | null = null;\n #normBuf: WebGLBuffer | null = null;\n #idxCount = 0;\n #mesh: PageMesh = createPageMesh(GRID_COLS, GRID_ROWS);\n\n #texCache = new Map<TexImageSource, { tex: WebGLTexture; bytes: number }>();\n #cacheBytes = 0;\n #blankTex: WebGLTexture | null = null;\n // The GPU's largest accepted texture dimension; a zoom tile past it would upload black (see\n // uploadTexture). Read once at mount and exposed so the source can cap its rasters to fit.\n #maxTextureSize = 0;\n\n #flatU: Record<string, WebGLUniformLocation | null> = {};\n #curlU: Record<string, WebGLUniformLocation | null> = {};\n\n #view = { scale: 1, tx: 0, ty: 0 };\n #dpr = 1;\n #content: SpreadContent | null = null;\n #fill = false;\n #flip: FlipState | null = null;\n #flipT = 0;\n #curlModel: CurlModel = BUNDLED_CURLS[DEFAULT_CURL];\n #anchor: CurlAnchor = { y: 0.5 };\n // A lone page (cover / book front-back) sits on one half; these center it by shifting\n // the view a quarter-width, interpolated across a flip so the open/close doesn't jump.\n #shiftX = 0;\n #fromShiftUnit = 0;\n #toShiftUnit = 0;\n // Book aspect fit: the 2-page spread is letterboxed to the pages' aspect, centered\n // in the container, so pages fill their halves without stretching. 0 = aspect unknown.\n #pageAspect = 0;\n // The first page's aspect, latched: the container is sized to this for the whole document so\n // off-size pages do not resize it (layout shift). `contain` letterboxes them; `fill` stretches.\n #docAspect = 0;\n #fit: 'contain' | 'fill' = 'contain';\n #bookW = 0;\n #bookH = 0;\n\n #contextLost = false;\n #fatalFired = false;\n #restoreTimer: ReturnType<typeof setTimeout> | null = null;\n #fatalHandler: (() => void) | null = null;\n\n mount(container: HTMLElement): Promise<void> {\n this.#container = container;\n const doc = container.ownerDocument;\n const canvas = doc.createElement('canvas');\n canvas.style.cssText = 'display:block;width:100%;height:100%;';\n container.append(canvas);\n this.#canvas = canvas;\n\n canvas.addEventListener('webglcontextlost', this.#onContextLost as EventListener);\n canvas.addEventListener('webglcontextrestored', this.#onContextRestored as EventListener);\n doc.addEventListener('visibilitychange', this.#onVisibilityChange);\n\n this.#gl = createGlContext(canvas); // throws if unavailable -> engine falls back (§8.4)\n this.#maxTextureSize = this.#gl.getParameter(this.#gl.MAX_TEXTURE_SIZE) as number;\n this.#buildGlResources();\n return Promise.resolve();\n }\n\n destroy(): void {\n this.#clearRestoreTimer();\n const canvas = this.#canvas;\n if (canvas !== null) {\n canvas.removeEventListener('webglcontextlost', this.#onContextLost as EventListener);\n canvas.removeEventListener('webglcontextrestored', this.#onContextRestored as EventListener);\n canvas.ownerDocument.removeEventListener('visibilitychange', this.#onVisibilityChange);\n }\n const gl = this.#gl;\n if (gl !== null) {\n for (const cached of this.#texCache.values()) gl.deleteTexture(cached.tex);\n if (this.#blankTex !== null) gl.deleteTexture(this.#blankTex);\n if (this.#quadVao !== null) gl.deleteVertexArray(this.#quadVao);\n if (this.#curlVao !== null) gl.deleteVertexArray(this.#curlVao);\n if (this.#flat !== null) gl.deleteProgram(this.#flat);\n if (this.#curl !== null) gl.deleteProgram(this.#curl);\n gl.getExtension('WEBGL_lose_context')?.loseContext();\n }\n canvas?.remove();\n this.#texCache.clear();\n this.#cacheBytes = 0;\n this.#blankTex = null;\n this.#container = null;\n this.#canvas = null;\n this.#gl = null;\n this.#content = null;\n this.#flip = null;\n }\n\n onFatal(handler: () => void): void {\n this.#fatalHandler = handler;\n }\n\n get maxTextureSize(): number | undefined {\n return this.#maxTextureSize || undefined;\n }\n\n renderSpread(_spread: Spread, content: SpreadContent, options?: RenderOptions): void {\n this.#content = content;\n this.#fill = options?.fill ?? false;\n if (options?.fit) this.#fit = options.fit;\n this.#flip = null;\n this.#trackAspect(content);\n this.#render();\n }\n\n /** Remember the current spread's page aspect (for drawing) and latch the first page's aspect (for\n * the container). The container follows #docAspect so off-size pages do not resize it. */\n #trackAspect(content: SpreadContent): void {\n const p = content.left ?? content.right;\n if (p && p.height) {\n this.#pageAspect = p.width / p.height;\n if (this.#docAspect === 0) this.#docAspect = this.#pageAspect;\n }\n }\n\n /** Aspect the book is drawn at: the current page under `contain` (letterboxed within the stable\n * container), or the document aspect under `fill` (fills it, stretching off-size pages). */\n #drawAspect(): number {\n return this.#fit === 'fill' && this.#docAspect > 0 ? this.#docAspect : this.#pageAspect;\n }\n\n beginFlip(\n from: SpreadContent,\n to: SpreadContent,\n direction: FlipDirection,\n options?: RenderOptions,\n ): void {\n const fill = options?.fill ?? false;\n if (options?.fit) this.#fit = options.fit;\n this.#flipT = 0;\n if (options?.curl) this.#curlModel = resolveCurl(options.curl);\n if (options?.anchor) this.#anchor = options.anchor;\n this.#fromShiftUnit = fill ? 0 : shiftUnit(from);\n this.#toShiftUnit = fill ? 0 : shiftUnit(to);\n this.#trackAspect(to);\n if (fill) {\n this.#flip = {\n underLeft: null,\n underRight: null,\n underFull: to.right ?? to.left,\n front: from.right ?? from.left,\n // A lone page has no facing \"next\" leaf on its back; show the same page mirrored\n // (the shader flips the back UV) so it reads as the sheet's own reverse side.\n back: from.right ?? from.left,\n dir: direction === 'forward' ? 1 : -1,\n fill: true,\n };\n } else if (direction === 'forward') {\n this.#flip = {\n underLeft: from.left,\n underRight: to.right,\n underFull: null,\n front: from.right,\n back: to.left,\n dir: 1,\n fill: false,\n };\n } else {\n this.#flip = {\n underLeft: to.left,\n underRight: from.right,\n underFull: null,\n front: from.left,\n back: to.right,\n dir: -1,\n fill: false,\n };\n }\n this.#render();\n }\n\n setFlipProgress(t: number, _direction: FlipDirection): void {\n this.#flipT = t;\n this.#render();\n }\n\n setViewTransform(scale: number, x: number, y: number): void {\n this.#view = { scale, tx: x, ty: y };\n this.#render();\n }\n\n measure(): LayoutMetrics {\n const el = this.#container;\n const w = el?.clientWidth ?? 0;\n const h = el?.clientHeight ?? 0;\n const known = this.#pageAspect > 0;\n const book = known ? this.#bookBox() : undefined;\n const content = known ? this.#contentBox() : undefined;\n const screenAt = content\n ? (scale: number): { x: number; y: number; width: number; height: number } => {\n // The shader applies `px * scale + view.t + shiftX`, so the lone-page shift sits\n // *outside* the scale: a fixed screen offset, not part of the magnified geometry.\n const shiftCss = this.#shiftX / this.#dpr;\n return {\n x: (content.x - shiftCss) * scale + shiftCss,\n y: content.y * scale,\n width: content.width * scale,\n height: content.height * scale,\n };\n }\n : undefined;\n return {\n containerWidth: w,\n containerHeight: h,\n pageWidth: w / 2,\n pageHeight: h,\n book,\n content,\n screenAt,\n containerAspect: this.#docAspect > 0 ? (this.#fill ? 1 : 2) * this.#docAspect : undefined,\n };\n }\n\n /** The fitted book rect in container CSS px (letterbox aware) — a stable 2-page area. */\n #bookBox(): { x: number; y: number; width: number; height: number } {\n const el = this.#container;\n const cw = el?.clientWidth ?? 0;\n const ch = el?.clientHeight ?? 0;\n const aspect = this.#drawAspect();\n if (aspect <= 0 || cw <= 0 || ch <= 0) return { x: 0, y: 0, width: cw, height: ch };\n const ba = (this.#fill ? 1 : 2) * aspect;\n let bw = cw;\n let bh = ch;\n if (ba > cw / ch) bh = cw / ba;\n else bw = ch * ba;\n return { x: (cw - bw) / 2, y: (ch - bh) / 2, width: bw, height: bh };\n }\n\n /** Where the current spread is actually painted: the book, or its centered half for a lone\n * page (mirrors the draw-time #shiftX centering). This is what the engine hit-tests against. */\n #contentBox(): { x: number; y: number; width: number; height: number } {\n const b = this.#bookBox();\n const lone = !this.#fill && this.#content !== null && shiftUnit(this.#content) !== 0;\n return lone ? { x: b.x + b.width / 4, y: b.y, width: b.width / 2, height: b.height } : b;\n }\n\n // --- context loss / restore ---------------------------------------------------\n\n #onContextLost = (event: Event): void => {\n event.preventDefault();\n this.#contextLost = true;\n if (this.#restoreTimer === null) {\n this.#restoreTimer = setTimeout(() => this.#fatal(), RESTORE_TIMEOUT_MS);\n }\n };\n\n #onContextRestored = (): void => {\n this.#clearRestoreTimer();\n this.#contextLost = false;\n this.#buildGlResources();\n this.#render();\n };\n\n #onVisibilityChange = (): void => {\n if (this.#canvas?.ownerDocument.visibilityState === 'visible') this.#render();\n };\n\n #fatal(): void {\n if (this.#fatalFired) return;\n this.#fatalFired = true;\n this.#fatalHandler?.();\n }\n\n #clearRestoreTimer(): void {\n if (this.#restoreTimer !== null) {\n clearTimeout(this.#restoreTimer);\n this.#restoreTimer = null;\n }\n }\n\n #buildGlResources(): void {\n const gl = this.#gl;\n if (gl === null) return;\n\n this.#flat = createProgram(gl, FLAT_VERT, FLAT_FRAG);\n this.#curl = createProgram(gl, CURL_VERT, CURL_FRAG);\n for (const name of ['uViewport', 'uRect', 'uView', 'uTex', 'uGutterSide']) {\n this.#flatU[name] = gl.getUniformLocation(this.#flat, name);\n }\n for (const name of ['uViewport', 'uView', 'uOriginX', 'uDir', 'uLeafW', 'uFront', 'uBack', 'uGloss', 'uAlpha', 'uCrease']) {\n this.#curlU[name] = gl.getUniformLocation(this.#curl, name);\n }\n\n this.#quadVao = this.#makeQuadVao(gl, this.#flat);\n this.#buildCurlMesh(gl, this.#curl);\n\n this.#texCache.clear();\n this.#cacheBytes = 0;\n this.#blankTex = this.#makeBlankTexture(gl);\n gl.useProgram(this.#flat);\n gl.uniform1i(this.#flatU.uTex ?? null, 0);\n gl.useProgram(this.#curl);\n gl.uniform1i(this.#curlU.uFront ?? null, 0);\n gl.uniform1i(this.#curlU.uBack ?? null, 1);\n gl.clearColor(0, 0, 0, 0);\n gl.enable(gl.BLEND);\n gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); // premultiplied alpha\n }\n\n #makeQuadVao(gl: WebGL2RenderingContext, program: WebGLProgram): WebGLVertexArrayObject {\n const vao = gl.createVertexArray();\n const buffer = gl.createBuffer();\n if (vao === null || buffer === null) throw new Error('WebglRenderer: could not create buffers.');\n gl.bindVertexArray(vao);\n gl.bindBuffer(gl.ARRAY_BUFFER, buffer);\n gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]), gl.STATIC_DRAW);\n const aUnit = gl.getAttribLocation(program, 'aUnit');\n gl.enableVertexAttribArray(aUnit);\n gl.vertexAttribPointer(aUnit, 2, gl.FLOAT, false, 0, 0);\n gl.bindVertexArray(null);\n return vao;\n }\n\n #buildCurlMesh(gl: WebGL2RenderingContext, program: WebGLProgram): void {\n const nx = GRID_COLS + 1;\n const indices: number[] = [];\n for (let j = 0; j < GRID_ROWS; j++) {\n for (let i = 0; i < GRID_COLS; i++) {\n const a = j * nx + i;\n const b = a + 1;\n const c = a + nx;\n const d = c + 1;\n indices.push(a, c, b, b, c, d);\n }\n }\n this.#idxCount = indices.length;\n\n const vao = gl.createVertexArray();\n this.#posBuf = gl.createBuffer();\n this.#normBuf = gl.createBuffer();\n const uvBuf = gl.createBuffer();\n const idxBuf = gl.createBuffer();\n if (vao === null || this.#posBuf === null || this.#normBuf === null || uvBuf === null || idxBuf === null) {\n throw new Error('WebglRenderer: could not create mesh buffers.');\n }\n gl.bindVertexArray(vao);\n\n gl.bindBuffer(gl.ARRAY_BUFFER, this.#posBuf);\n gl.bufferData(gl.ARRAY_BUFFER, this.#mesh.positions, gl.DYNAMIC_DRAW);\n const aPos = gl.getAttribLocation(program, 'aPos');\n gl.enableVertexAttribArray(aPos);\n gl.vertexAttribPointer(aPos, 3, gl.FLOAT, false, 0, 0);\n\n gl.bindBuffer(gl.ARRAY_BUFFER, this.#normBuf);\n gl.bufferData(gl.ARRAY_BUFFER, this.#mesh.normals, gl.DYNAMIC_DRAW);\n const aNormal = gl.getAttribLocation(program, 'aNormal');\n gl.enableVertexAttribArray(aNormal);\n gl.vertexAttribPointer(aNormal, 3, gl.FLOAT, false, 0, 0);\n\n gl.bindBuffer(gl.ARRAY_BUFFER, uvBuf);\n gl.bufferData(gl.ARRAY_BUFFER, this.#mesh.uvs, gl.STATIC_DRAW);\n const aUv = gl.getAttribLocation(program, 'aUv');\n gl.enableVertexAttribArray(aUv);\n gl.vertexAttribPointer(aUv, 2, gl.FLOAT, false, 0, 0);\n\n gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, idxBuf);\n gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);\n\n gl.bindVertexArray(null);\n this.#curlVao = vao;\n }\n\n // --- drawing ------------------------------------------------------------------\n\n #render(): void {\n const gl = this.#gl;\n const canvas = this.#canvas;\n if (gl === null || canvas === null || this.#contextLost || gl.isContextLost()) return;\n this.#resize();\n gl.clear(gl.COLOR_BUFFER_BIT);\n this.#fitBook();\n if (this.#flip !== null) this.#drawFlip();\n else if (this.#content !== null) this.#drawStatic();\n }\n\n /** Letterbox the 2-page book to the page aspect, centered; restricts the GL viewport\n * so pages fill their halves without stretching (the surround stays cleared). */\n #fitBook(): void {\n const gl = this.#gl!;\n const canvas = this.#canvas!;\n const cw = canvas.width;\n const ch = canvas.height;\n const aspect = this.#drawAspect();\n if (aspect > 0) {\n const bookAspect = (this.#fill ? 1 : 2) * aspect;\n if (bookAspect > cw / ch) {\n this.#bookW = cw;\n this.#bookH = Math.round(cw / bookAspect);\n } else {\n this.#bookH = ch;\n this.#bookW = Math.round(ch * bookAspect);\n }\n } else {\n this.#bookW = cw;\n this.#bookH = ch;\n }\n const bx = Math.round((cw - this.#bookW) / 2);\n const by = Math.round((ch - this.#bookH) / 2); // centered, so GL's bottom offset == by\n gl.viewport(bx, by, this.#bookW, this.#bookH);\n }\n\n #drawStatic(): void {\n const w = this.#bookW;\n const h = this.#bookH;\n const content = this.#content!;\n this.#shiftX = this.#fill ? 0 : shiftUnit(content) * (w / 4);\n this.#useFlat();\n if (this.#fill) {\n this.#drawQuad({ x: 0, y: 0, w, h }, content.right ?? content.left);\n } else {\n // A lone page (one side null) gets no gutter shadow — it's a standalone, centered page.\n this.#drawQuad({ x: 0, y: 0, w: w / 2, h }, content.left, content.right ? 1 : 0);\n this.#drawQuad({ x: w / 2, y: 0, w: w / 2, h }, content.right, content.left ? -1 : 0);\n }\n }\n\n #drawFlip(): void {\n const gl = this.#gl!;\n const w = this.#bookW;\n const h = this.#bookH;\n const flip = this.#flip!;\n\n // Interpolate the lone-page centering across the flip so an opening cover (or a\n // closing back page) slides between centered and spread instead of jumping.\n this.#shiftX = flip.fill\n ? 0\n : (this.#fromShiftUnit + (this.#toShiftUnit - this.#fromShiftUnit) * this.#flipT) * (w / 4);\n\n // Static pages underneath (with gutter shadow).\n this.#useFlat();\n if (flip.fill) {\n this.#drawQuad({ x: 0, y: 0, w, h }, flip.underFull);\n } else {\n this.#drawQuad({ x: 0, y: 0, w: w / 2, h }, flip.underLeft, 1);\n this.#drawQuad({ x: w / 2, y: 0, w: w / 2, h }, flip.underRight, -1);\n }\n\n if (flip.front === null && flip.back === null) return;\n\n // Deform the leaf mesh with the selected curl model on the CPU, upload it, then draw.\n const leafW = flip.fill ? w : w / 2;\n const originX = flip.fill ? (flip.dir > 0 ? 0 : w) : w / 2;\n this.#curlModel.deform(this.#mesh, leafW, h, this.#flipT, {\n y: this.#anchor.y,\n fill: flip.fill,\n });\n gl.bindBuffer(gl.ARRAY_BUFFER, this.#posBuf);\n gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.#mesh.positions);\n gl.bindBuffer(gl.ARRAY_BUFFER, this.#normBuf);\n gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.#mesh.normals);\n\n this.#bindFace(gl.TEXTURE0, flip.front);\n this.#bindFace(gl.TEXTURE1, flip.back);\n gl.activeTexture(gl.TEXTURE0);\n\n gl.useProgram(this.#curl);\n gl.bindVertexArray(this.#curlVao);\n gl.uniform2f(this.#curlU.uViewport ?? null, w, h);\n gl.uniform3f(\n this.#curlU.uView ?? null,\n this.#view.scale,\n this.#view.tx * this.#dpr + this.#shiftX,\n this.#view.ty * this.#dpr,\n );\n gl.uniform1f(this.#curlU.uOriginX ?? null, originX);\n gl.uniform1f(this.#curlU.uDir ?? null, flip.dir);\n gl.uniform1f(this.#curlU.uLeafW ?? null, leafW);\n gl.uniform1f(this.#curlU.uGloss ?? null, this.#curlModel.gloss === false ? 0 : 1);\n gl.uniform1f(this.#curlU.uAlpha ?? null, flip.fill ? this.#fillFade() : 1);\n // Spreads carry the full gutter crease; a lone leaf ramps it in over the first fifth of the\n // turn so a still-flat sheet shows no shadow band at its anchor before it lifts.\n gl.uniform1f(this.#curlU.uCrease ?? null, flip.fill ? Math.min(1, this.#flipT / 0.2) : 1);\n gl.drawElements(gl.TRIANGLES, this.#idxCount, gl.UNSIGNED_SHORT, 0);\n }\n\n /** Leaf opacity for a lone page: solid until the curl-appropriate fade start, then\n * dissolving to 0 as it lands, so the turn resolves into the destination page rather\n * than onto blank space. The fade eases so opacity hangs longer then finishes cleanly. */\n #fillFade(): number {\n const start = this.#curlModel.flat ? FILL_FADE_START_ROLL : FILL_FADE_START_CURL;\n const over = (this.#flipT - start) / (1 - start);\n if (over <= 0) return 1;\n if (over >= 1) return 0;\n // Ease-out the remaining opacity: stay readable through most of the window, then settle.\n return Math.pow(1 - over, 1.5);\n }\n\n #useFlat(): void {\n const gl = this.#gl!;\n gl.useProgram(this.#flat);\n gl.bindVertexArray(this.#quadVao);\n gl.activeTexture(gl.TEXTURE0);\n gl.uniform2f(this.#flatU.uViewport ?? null, this.#bookW, this.#bookH);\n gl.uniform3f(\n this.#flatU.uView ?? null,\n this.#view.scale,\n this.#view.tx * this.#dpr + this.#shiftX,\n this.#view.ty * this.#dpr,\n );\n }\n\n #drawQuad(rect: Rect, source: TexImageSource | null, gutterSide = 0): void {\n const gl = this.#gl!;\n if (source === null) return;\n this.#bindFace(gl.TEXTURE0, source);\n gl.uniform4f(this.#flatU.uRect ?? null, rect.x, rect.y, rect.w, rect.h);\n gl.uniform1f(this.#flatU.uGutterSide ?? null, gutterSide);\n gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);\n }\n\n #bindFace(unit: number, source: TexImageSource | null): void {\n const gl = this.#gl!;\n gl.activeTexture(unit);\n gl.bindTexture(gl.TEXTURE_2D, source !== null ? this.#textureFor(source) : this.#blankTex);\n }\n\n #textureFor(source: TexImageSource): WebGLTexture {\n const gl = this.#gl!;\n const existing = this.#texCache.get(source);\n if (existing !== undefined) {\n this.#texCache.delete(source);\n this.#texCache.set(source, existing);\n return existing.tex;\n }\n const tex = createTexture(gl);\n // uploadTexture may downscale a raster too large for the GPU; account for what it really uploaded.\n const uploaded = uploadTexture(gl, tex, source);\n const bytes = uploaded.width * uploaded.height * 4;\n this.#texCache.set(source, { tex, bytes });\n this.#cacheBytes += bytes;\n this.#evictTextures();\n return tex;\n }\n\n #evictTextures(): void {\n const gl = this.#gl!;\n for (const [src, cached] of this.#texCache) {\n if (this.#cacheBytes <= MAX_TEXTURE_CACHE_BYTES || this.#texCache.size <= 4) break;\n this.#texCache.delete(src);\n this.#cacheBytes -= cached.bytes;\n gl.deleteTexture(cached.tex);\n }\n }\n\n #makeBlankTexture(gl: WebGL2RenderingContext): WebGLTexture {\n const tex = createTexture(gl);\n gl.bindTexture(gl.TEXTURE_2D, tex);\n gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([0, 0, 0, 0]));\n return tex;\n }\n\n #resize(): void {\n const gl = this.#gl;\n const canvas = this.#canvas;\n const el = this.#container;\n if (gl === null || canvas === null || el === null) return;\n this.#dpr = typeof devicePixelRatio === 'number' ? devicePixelRatio : 1;\n const w = Math.max(1, Math.round(el.clientWidth * this.#dpr));\n const h = Math.max(1, Math.round(el.clientHeight * this.#dpr));\n if (canvas.width !== w || canvas.height !== h) {\n canvas.width = w;\n canvas.height = h;\n gl.viewport(0, 0, w, h);\n }\n }\n}\n","import type { Renderer } from './types';\n\nexport type RendererKind = 'css' | 'webgl2';\n\n/**\n * How the consumer chooses a renderer:\n * - `'auto'` (default): prefer the GPU path (WebGL2), else CSS.\n * - a single kind: force it (`'webgl2'` throws if the device has no WebGL2).\n * - an ordered array: try each in turn, first viable wins.\n * - a `Renderer` instance: use it as-is (custom implementation, §6.1/§11).\n */\nexport type RendererOption = 'auto' | RendererKind | RendererKind[] | Renderer;\n\nexport interface Capabilities {\n /** WebGL2 available — the GPU renderer's only backend. */\n gpu: boolean;\n}\n\ntype RendererLoader = () => Promise<Renderer>;\n\n/**\n * Lazy loaders, one per kind. Each is a dynamic `import()` so the bundler\n * code-splits it into its own chunk and a device fetches only the one selected.\n */\nconst LOADERS: Partial<Record<RendererKind, RendererLoader>> = {\n css: async () => new (await import('./cssRenderer')).CssRenderer(),\n webgl2: async () => new (await import('./webglRenderer')).WebglRenderer(),\n};\n\n// The WebGL2 renderer is auto-selected on GPU-capable devices; CSS is the baseline\n// for non-GPU devices and the fallback for unrecoverable GPU failures (§8.4).\nconst WEBGL2_AUTO = true;\n\nfunction isRenderer(option: RendererOption): option is Renderer {\n return (\n typeof option === 'object' &&\n option !== null &&\n !Array.isArray(option) &&\n typeof (option as Renderer).mount === 'function'\n );\n}\n\nfunction autoOrder(caps: Capabilities): RendererKind[] {\n return caps.gpu && WEBGL2_AUTO ? ['webgl2', 'css'] : ['css'];\n}\n\n/** Detect WebGL2 support — the GPU renderer's only backend (no WebGL1, no WebGPU). */\nexport function detectCapabilities(): Capabilities {\n return { gpu: hasWebGl2() };\n}\n\nfunction hasWebGl2(): boolean {\n try {\n return Boolean(document.createElement('canvas').getContext('webgl2'));\n } catch {\n return false;\n }\n}\n\n/**\n * Resolve the `renderer` option to a mounted renderer instance, lazy-loading its\n * chunk. Throws an agent-actionable error if nothing viable can be loaded.\n */\nexport async function selectRenderer(\n option: RendererOption = 'auto',\n caps: Capabilities = detectCapabilities(),\n): Promise<Renderer> {\n if (isRenderer(option)) return option;\n\n const order = Array.isArray(option) ? option : option === 'auto' ? autoOrder(caps) : [option];\n\n for (const kind of order) {\n if (kind === 'webgl2' && !caps.gpu) continue; // skip the GPU path without WebGL2\n const loader = LOADERS[kind];\n if (loader) return loader();\n }\n\n if (order.length === 1 && order[0] === 'webgl2' && !caps.gpu) {\n throw new Error(\n \"renderer: 'webgl2' was forced but this device has no WebGL2. Use 'css' or 'auto'.\",\n );\n }\n throw new Error(\n `renderer: none of [${order.join(', ')}] could be loaded — use 'css' or 'auto'.`,\n );\n}\n","import type { PageContent } from '../renderer/types';\nimport type { DownloadInfo, LoadProgress, OutlineItem, PageRequest, Source } from './types';\n\nexport interface CompositionOptions {\n /** Image URL prepended as a lone front cover (adds a page). */\n frontCover?: string | null;\n /** Image URL appended as a lone back cover (adds a page). */\n backCover?: string | null;\n /** Replace source pages with image URLs, keyed by 0-based source index (negative = from the end). */\n pages?: Record<number, string> | null;\n}\n\n/**\n * Wrap `base` so image covers/replacements sit alongside it, or return `base`\n * unchanged when nothing is requested. Covers ADD pages (front prepended, back\n * appended); `pages` REPLACE existing source pages. `pages` indexes the base's own\n * pages, so it is unaffected by an added cover.\n */\nexport function composeSource(base: Source, options: CompositionOptions): Source {\n const hasFront = typeof options.frontCover === 'string';\n const hasBack = typeof options.backCover === 'string';\n const hasPages = options.pages != null && Object.keys(options.pages).length > 0;\n if (!hasFront && !hasBack && !hasPages) return base;\n return new CompositeSource(base, options);\n}\n\nfunction loadImage(url: string): Promise<ImageBitmap> {\n return fetch(url)\n .then((res) => {\n if (!res.ok) throw new Error(`zine: could not load image \"${url}\" (HTTP ${res.status}).`);\n return res.blob();\n })\n .then((blob) => createImageBitmap(blob));\n}\n\nclass CompositeSource implements Source {\n #base: Source;\n #front: string | null;\n #back: string | null;\n #rawPages: Record<number, string>;\n #resolved = new Map<number, string>(); // base-index → url (negatives resolved)\n #cache = new Map<string, Promise<ImageBitmap>>();\n open?: () => Promise<void>;\n onPageUpdate?: (handler: (index: number) => void) => void;\n onProgress?: (handler: (progress: LoadProgress) => void) => void;\n getText?: (index: number) => Promise<string>;\n getDownload?: () => Promise<DownloadInfo | null>;\n getOutline?: () => Promise<OutlineItem[]>;\n\n constructor(base: Source, options: CompositionOptions) {\n this.#base = base;\n this.#front = typeof options.frontCover === 'string' ? options.frontCover : null;\n this.#back = typeof options.backCover === 'string' ? options.backCover : null;\n this.#rawPages = options.pages ?? {};\n\n if (typeof base.open === 'function') {\n // Async base: page count (and negative indices) known only after open().\n this.open = async (): Promise<void> => {\n await base.open!();\n this.#resolvePages();\n };\n } else {\n this.#resolvePages(); // sync base: count is known now\n }\n\n if (typeof base.onPageUpdate === 'function') {\n // Forward base page updates, shifting to book indices (past the front cover).\n this.onPageUpdate = (handler) => {\n base.onPageUpdate!((src) => handler(src + (this.#front !== null ? 1 : 0)));\n };\n }\n\n if (typeof base.onProgress === 'function') {\n // Download progress is about the whole document, not any one page, so it passes straight\n // through: covers and replacements do not shift it.\n this.onProgress = (handler) => base.onProgress!(handler);\n }\n\n if (typeof base.getText === 'function') {\n // Mirrors get(): covers and replaced pages are images, so they have no text of their own —\n // returning the base page's text there would describe content the reader cannot see.\n this.getText = async (index) => {\n const frontOffset = this.#front !== null ? 1 : 0;\n if (this.#front !== null && index === 0) return '';\n if (this.#back !== null && index === this.pageCount - 1) return '';\n const src = index - frontOffset;\n if (this.#resolved.has(src)) return '';\n return base.getText!(src);\n };\n }\n\n if (typeof base.getDownload === 'function') {\n // Covers and replacements change what is displayed, not the original document, so the\n // download is the base source's file either way.\n this.getDownload = () => base.getDownload!();\n }\n\n if (typeof base.getOutline === 'function') {\n // The outline points at base pages; a front cover pushes every one of them along by one.\n this.getOutline = async () => {\n const items = await base.getOutline!();\n const offset = this.#front !== null ? 1 : 0;\n if (offset === 0) return items;\n const shift = (list: OutlineItem[]): OutlineItem[] =>\n list.map((item) => ({\n ...item,\n page: item.page === null ? null : item.page + offset,\n children: shift(item.children),\n }));\n return shift(items);\n };\n }\n }\n\n get pageCount(): number {\n return this.#base.pageCount + (this.#front !== null ? 1 : 0) + (this.#back !== null ? 1 : 0);\n }\n\n get(index: number, opts?: PageRequest): Promise<PageContent> {\n const frontOffset = this.#front !== null ? 1 : 0;\n // Covers and page overrides are images: they have no detail beyond their own resolution, so\n // the zoom request has nothing to act on and is dropped rather than forwarded.\n if (this.#front !== null && index === 0) return this.#image(this.#front);\n if (this.#back !== null && index === this.pageCount - 1) return this.#image(this.#back);\n const src = index - frontOffset;\n const override = this.#resolved.get(src);\n return override !== undefined ? this.#image(override) : this.#base.get(src, opts);\n }\n\n prefetch(indices: number[]): void {\n const frontOffset = this.#front !== null ? 1 : 0;\n const baseIndices: number[] = [];\n for (const index of indices) {\n if (this.#front !== null && index === 0) {\n void this.#image(this.#front).catch(() => {});\n } else if (this.#back !== null && index === this.pageCount - 1) {\n void this.#image(this.#back).catch(() => {});\n } else {\n const src = index - frontOffset;\n const override = this.#resolved.get(src);\n if (override !== undefined) void this.#image(override).catch(() => {});\n else baseIndices.push(src);\n }\n }\n this.#base.prefetch(baseIndices);\n }\n\n destroy(): void {\n this.#base.destroy();\n for (const promise of this.#cache.values()) {\n void promise.then((bmp) => bmp.close()).catch(() => {});\n }\n this.#cache.clear();\n }\n\n #resolvePages(): void {\n const n = this.#base.pageCount;\n this.#resolved.clear();\n for (const [key, url] of Object.entries(this.#rawPages)) {\n const raw = Number(key);\n const src = raw < 0 ? n + raw : raw;\n if (Number.isInteger(src) && src >= 0 && src < n) this.#resolved.set(src, url);\n }\n }\n\n #image(url: string): Promise<ImageBitmap> {\n let promise = this.#cache.get(url);\n if (promise === undefined) {\n promise = loadImage(url).catch((error: unknown) => {\n this.#cache.delete(url); // let a later get() retry\n throw error;\n });\n this.#cache.set(url, promise);\n }\n return promise;\n }\n}\n","/**\n * User-facing text for the reader UI (control labels, screen-reader announcements, panel and\n * loading text). English by default; a consumer overrides any subset via the `strings` option.\n *\n * Static entries are plain strings. Entries that mix in a number or piece of text are functions,\n * so a translation can handle pluralization and word order that a `{placeholder}` string cannot.\n * Page numbers passed to these functions are already 1-based (display) values.\n */\nexport interface ZineStrings {\n // Toolbar / control buttons (each is tooltip + aria-label + visible label).\n prevPage: string;\n nextPage: string;\n firstPage: string;\n lastPage: string;\n zoomIn: string;\n zoomOut: string;\n fullscreen: string;\n mute: string;\n unmute: string;\n share: string;\n downloadPdf: string;\n showOnePage: string;\n showTwoPages: string;\n print: string;\n more: string;\n controlsLabel: string;\n showThumbnails: string;\n hideThumbnails: string;\n showOutline: string;\n hideOutline: string;\n searchOpen: string;\n searchClose: string;\n pageWidgetLabel: string; // toolbar page-number widget title\n pageNumberLabel: string; // page-number input aria-label\n roledescription: string; // container aria-roledescription\n\n // Panels.\n thumbnailsLabel: string;\n outlineLabel: string;\n outlineLoading: string;\n outlineEmpty: string;\n untitled: string; // outline entry with no title\n searchResultsLabel: string;\n searchPlaceholder: string;\n searchInputLabel: string;\n searching: string;\n\n // Loading overlay.\n loadingOpening: string;\n loadingPreparing: string;\n\n // Share dialog.\n shareFallbackTitle: string; // used when the document has no title\n close: string;\n copy: string;\n copied: string;\n copyManual: string;\n qrLabel: string;\n linkLabel: string;\n email: string; // social label (brand names are not translated)\n\n // Hints (visual captions).\n panHint: string;\n\n // Dynamic (interpolated / pluralizable) entries.\n pageAnnounce: (current: number, total: number) => string;\n pageTotal: (total: number) => string;\n downloadingPercent: (percent: number) => string;\n downloadingSize: (megabytes: number) => string;\n zoomHint: (opts: { doubleClick: boolean; wheel: boolean; mac: boolean }) => string;\n noMatches: (query: string) => string;\n searchHitLabel: (page: number) => string;\n searchHitAria: (page: number, excerpt: string) => string;\n shareOn: (network: string) => string;\n thumbnailAria: (pageLabels: string[]) => string;\n outlineEntryAria: (title: string, page: number) => string;\n}\n\n/** Built-in English text. Overridden per key by the `strings` option. */\nexport const defaultStrings: ZineStrings = {\n prevPage: 'Previous page',\n nextPage: 'Next page',\n firstPage: 'First page',\n lastPage: 'Last page',\n zoomIn: 'Zoom in',\n zoomOut: 'Zoom out',\n fullscreen: 'Fullscreen',\n mute: 'Mute page sound',\n unmute: 'Unmute page sound',\n share: 'Share',\n downloadPdf: 'Download PDF',\n showOnePage: 'Show one page',\n showTwoPages: 'Show two pages',\n print: 'Print',\n more: 'More',\n controlsLabel: 'Flipbook controls',\n showThumbnails: 'Show thumbnails',\n hideThumbnails: 'Hide thumbnails',\n showOutline: 'Show outline',\n hideOutline: 'Hide outline',\n searchOpen: 'Search',\n searchClose: 'Hide search',\n pageWidgetLabel: 'Page',\n pageNumberLabel: 'Page number',\n roledescription: 'flipbook',\n\n thumbnailsLabel: 'Pages',\n outlineLabel: 'Outline',\n outlineLoading: 'Loading…',\n outlineEmpty: 'This document has no outline.',\n untitled: 'Untitled',\n searchResultsLabel: 'Search results',\n searchPlaceholder: 'Search…',\n searchInputLabel: 'Search the document',\n searching: 'Searching…',\n\n loadingOpening: 'Opening document…',\n loadingPreparing: 'Preparing pages…',\n\n shareFallbackTitle: 'Flipbook',\n close: 'Close',\n copy: 'Copy',\n copied: 'Copied',\n copyManual: 'Press Ctrl+C',\n qrLabel: 'QR code for this page',\n linkLabel: 'Link to this page',\n email: 'Email',\n\n panHint: 'Drag to move',\n\n pageAnnounce: (current, total) => `Page ${current} of ${total}`,\n pageTotal: (total) => `/ ${total}`,\n downloadingPercent: (percent) => `Downloading document… ${percent}%`,\n downloadingSize: (megabytes) => `Downloading document… ${megabytes.toFixed(1)} MB`,\n zoomHint: ({ doubleClick, wheel, mac }) => {\n const parts: string[] = [];\n if (doubleClick) parts.push('Double-click');\n if (wheel) parts.push(`${mac ? '⌘' : 'Ctrl'}-scroll`);\n return `${parts.join(' or ')} to zoom`;\n },\n noMatches: (query) => `No matches for “${query}”`,\n searchHitLabel: (page) => `Page ${page}`,\n searchHitAria: (page, excerpt) => `Page ${page}: ${excerpt}`,\n shareOn: (network) => `Share on ${network}`,\n thumbnailAria: (labels) =>\n labels.length > 1 ? `Pages ${labels[0]}–${labels.at(-1)}` : `Page ${labels[0] ?? ''}`,\n outlineEntryAria: (title, page) => `${title}, page ${page}`,\n};\n\n/** Merge consumer overrides over the English defaults. A missing key keeps the default. */\nexport function resolveStrings(overrides?: Partial<ZineStrings>): ZineStrings {\n return overrides ? { ...defaultStrings, ...overrides } : defaultStrings;\n}\n","/**\n * The bundled default page-flip sound, as a base64 `data:` URI.\n *\n * Generated by `scripts/embed-flip-clip.mjs` from `packages/core/assets/flip.mp3` — do not\n * hand-edit. Inlined rather than shipped as a separate asset so the default clip needs no bundler\n * URL resolution and no network fetch (works offline / under strict CSP); consumers who want an\n * external file use the `sound.url` option instead.\n */\nexport const FLIP_CLIP = 'data:audio/mpeg;base64,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VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//PkZAAA4AQAAHgAAAHACAAA8AAAMBkak0dlxzAYGoNHlcZMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//PkZAAAAAGkAAAAAAAAA0gAAAAATEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//PkZAAAAAGkAAAAAAAAA0gAAAAATEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//PkZAAAAAGkAAAAAAAAA0gAAAAATEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//PkZAAAAAGkAAAAAAAAA0gAAAAATEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//PkZAAAAAGkAAAAAAAAA0gAAAAATEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//PkZAAAAAGkAAAAAAAAA0gAAAAATEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//PkZAAAAAGkAAAAAAAAA0gAAAAATEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//PkZAAAAAGkAAAAAAAAA0gAAAAATEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//PkZAAAAAGkAAAAAAAAA0gAAAAATEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//PkZAAAAAGkAAAAAAAAA0gAAAAATEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV';\n","import { FLIP_CLIP } from './clip';\n\n/** Default playback volume when the reader does not set one. */\nconst DEFAULT_VOLUME = 0.5;\n\ntype AudioContextCtor = typeof AudioContext;\n\nfunction audioContextCtor(): AudioContextCtor | undefined {\n if (typeof window === 'undefined') return undefined;\n const w = window as unknown as { AudioContext?: AudioContextCtor; webkitAudioContext?: AudioContextCtor };\n return w.AudioContext ?? w.webkitAudioContext;\n}\n\nfunction clamp01(n: number): number {\n return n < 0 ? 0 : n > 1 ? 1 : n;\n}\n\n/** Decode a `data:...;base64,...` URI to bytes, for feeding decodeAudioData. */\nfunction dataUriToBytes(dataUri: string): ArrayBuffer | undefined {\n if (typeof atob !== 'function') return undefined;\n const comma = dataUri.indexOf(',');\n if (comma < 0) return undefined;\n const binary = atob(dataUri.slice(comma + 1));\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);\n return bytes.buffer;\n}\n\nexport interface FlipSoundOptions {\n /** A clip URL that overrides the bundled default (fetched at runtime). */\n url?: string;\n /** Playback volume, 0..1. Default 0.5. */\n volume?: number;\n}\n\n/**\n * Plays a short sound on each page turn.\n *\n * Everything here is best-effort: a browser that gates audio behind a gesture, has no Web Audio, or\n * cannot decode the clip simply stays silent. A page flip must never throw or stall because of\n * sound, so every path is guarded and failures are swallowed.\n *\n * Web Audio (not `<audio>`) so rapid flips overlap cleanly — each turn plays a fresh buffer source\n * through one shared gain node (volume + mute).\n */\nexport class FlipSound {\n #ctorCache: AudioContextCtor | undefined;\n #ctx: AudioContext | null = null;\n #gain: GainNode | null = null;\n #buffer: AudioBuffer | null = null;\n #decoding: Promise<void> | null = null;\n #volume: number;\n #muted = false;\n #url: string | undefined;\n\n constructor(options: FlipSoundOptions = {}) {\n this.#ctorCache = audioContextCtor();\n this.#volume = clamp01(options.volume ?? DEFAULT_VOLUME);\n this.#url = options.url;\n }\n\n /** Whether this environment can play at all — no context constructor means never. */\n get available(): boolean {\n return this.#ctorCache !== undefined && (Boolean(this.#url) || FLIP_CLIP.length > 0);\n }\n\n /**\n * Play the clip once. Best-effort: silent if audio is unavailable, muted, still gesture-locked,\n * or the clip has not decoded yet (the first flip may miss while it decodes; the rest play).\n */\n play(): void {\n if (this.#muted || !this.available) return;\n // Create/resume the context synchronously here, inside the flip's own gesture frame, so the\n // browser unlocks audio. Decoding is async and may lag the very first flip; later flips are hot.\n const ctx = this.#context();\n if (!ctx) return;\n if (ctx.state === 'suspended') void ctx.resume().catch(() => {});\n void this.#decode().then(() => {\n if (!this.#ctx || !this.#buffer || !this.#gain || this.#muted) return;\n try {\n const source = this.#ctx.createBufferSource();\n source.buffer = this.#buffer;\n source.connect(this.#gain);\n source.start();\n } catch {\n // Context closed mid-play, or otherwise unavailable: stay silent.\n }\n });\n }\n\n setMuted(muted: boolean): void {\n this.#muted = muted;\n if (this.#gain) this.#gain.gain.value = muted ? 0 : this.#volume;\n }\n\n isMuted(): boolean {\n return this.#muted;\n }\n\n destroy(): void {\n try {\n void this.#ctx?.close();\n } catch {\n // Already closed or closing.\n }\n this.#ctx = null;\n this.#gain = null;\n this.#buffer = null;\n }\n\n #context(): AudioContext | null {\n if (this.#ctx) return this.#ctx;\n if (!this.#ctorCache) return null;\n try {\n const ctx = new this.#ctorCache();\n const gain = ctx.createGain();\n gain.gain.value = this.#muted ? 0 : this.#volume;\n gain.connect(ctx.destination);\n this.#ctx = ctx;\n this.#gain = gain;\n } catch {\n this.#ctx = null;\n }\n return this.#ctx;\n }\n\n /** Decode the clip to an AudioBuffer once; later calls await the first attempt. */\n #decode(): Promise<void> {\n if (this.#decoding) return this.#decoding;\n this.#decoding = this.#loadBuffer().catch(() => {\n // Fetch/decode failed → stay silent; do not retry on every flip.\n });\n return this.#decoding;\n }\n\n async #loadBuffer(): Promise<void> {\n const ctx = this.#context();\n if (!ctx) return;\n let bytes: ArrayBuffer | undefined;\n if (this.#url) {\n const res = await fetch(this.#url);\n bytes = await res.arrayBuffer();\n } else if (FLIP_CLIP) {\n bytes = dataUriToBytes(FLIP_CLIP);\n }\n if (!bytes) return;\n // slice(0): some engines detach the source ArrayBuffer during decode.\n this.#buffer = await ctx.decodeAudioData(bytes.slice(0));\n }\n}\n","import type { LoadProgress } from '../source/types';\nimport type { ZineStrings } from '../strings';\n\n/**\n * The built-in loading overlay: a spinner, a status line, and a download progress bar.\n *\n * This module is the lazy chunk `zine.ts` reaches through a dynamic `import()`, so a book built\n * with `loading: false`, or one whose source never reports progress, never downloads it. The file\n * is named `loading.ts` rather than `index.ts` so the emitted chunk is `loading-<hash>.js`, which\n * is what the size-limit budget watches (the same trick as `controls/controls.ts`).\n *\n * Everything is scoped under `.zine-loading`, and the colours come from custom properties so a\n * consumer can retheme without overriding rules. Each falls back to `light-dark(light, dark)`, which\n * resolves against the used `color-scheme`, so the overlay follows the host page's light or dark\n * theme with no configuration (the same approach as the controls). A consumer who wants a wholly\n * different indicator passes `loading: false` and drives their own UI from the `progress` event and\n * the `ready` promise.\n */\n\nconst STYLE_ID = 'zine-loading-style';\n\n// Wait this long before showing the overlay: a small or cached PDF resolves in a few frames, and\n// flashing a spinner for that is worse than showing nothing.\nconst REVEAL_DELAY_MS = 150;\n\nexport const CSS = `\n.zine-loading {\n position: absolute;\n inset: 0;\n z-index: 3;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 14px;\n /* The overlay shows during download, before the book is measured and the container gets its\n aspect-ratio (and thus its height). A consumer who sizes the container by width alone leaves it\n near-zero-height meanwhile, which would crush the overlay to a strip and clip the spinner.\n A floor keeps it tall enough to lay out; the box snaps to the book's ratio once it paints. */\n min-height: 160px;\n padding: 24px;\n box-sizing: border-box;\n text-align: center;\n font: 500 14px/1.4 system-ui, sans-serif;\n color: var(--zine-loading-fg, light-dark(#3f3f46, #d4d4d8));\n background: var(--zine-loading-bg, light-dark(#fafafa, #0c0c11));\n border-radius: inherit;\n opacity: 0;\n transition: opacity 150ms ease;\n}\n.zine-loading-shown { opacity: 1; }\n.zine-loading-spinner {\n width: 32px;\n height: 32px;\n border: 3px solid var(--zine-loading-track, light-dark(rgba(0, 0, 0, 0.1), rgba(255, 255, 255, 0.14)));\n border-top-color: var(--zine-loading-accent, light-dark(#0284c7, #7dd3fc));\n border-radius: 50%;\n animation: zine-loading-spin 0.8s linear infinite;\n}\n@keyframes zine-loading-spin { to { transform: rotate(360deg); } }\n.zine-loading-text { font-variant-numeric: tabular-nums; }\n.zine-loading-bar {\n width: min(240px, 70%);\n height: 6px;\n background: var(--zine-loading-track, light-dark(rgba(0, 0, 0, 0.1), rgba(255, 255, 255, 0.14)));\n border-radius: 3px;\n overflow: hidden;\n}\n.zine-loading-fill {\n width: 0;\n height: 100%;\n background: var(--zine-loading-accent, light-dark(#0284c7, #7dd3fc));\n border-radius: 3px;\n transition: width 150ms ease-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .zine-loading { transition: none; }\n .zine-loading-spinner { animation-duration: 2s; }\n .zine-loading-fill { transition: none; }\n}\n`;\n\n/** Add the stylesheet to `doc` if it is not already there. */\nfunction ensureStyles(doc: Document): void {\n if (doc.getElementById(STYLE_ID)) return;\n const style = doc.createElement('style');\n style.id = STYLE_ID;\n style.textContent = CSS;\n (doc.head ?? doc.documentElement)?.appendChild(style);\n}\n\n/** Handle the engine uses to drive and tear down the overlay. */\nexport interface LoaderHandle {\n /** Download phase: reflect bytes fetched, showing a percentage bar when a total is known. */\n update(progress: LoadProgress): void;\n /** Downloading is done; the wait is now rasterizing the first spread. */\n preparing(): void;\n /** Remove the overlay and restore anything it changed on the container. */\n destroy(): void;\n}\n\n/**\n * Build the overlay inside `container` and return a handle to drive it.\n *\n * The overlay is hidden for the first {@link REVEAL_DELAY_MS} so a fast load never flashes it;\n * `destroy()` before then cancels the reveal outright.\n */\nexport function mountLoading(container: HTMLElement, strings: ZineStrings): LoaderHandle {\n const doc = container.ownerDocument;\n ensureStyles(doc);\n\n const overlay = doc.createElement('div');\n overlay.className = 'zine-loading';\n overlay.setAttribute('role', 'status');\n overlay.setAttribute('aria-live', 'polite');\n\n const spinner = doc.createElement('div');\n spinner.className = 'zine-loading-spinner';\n spinner.setAttribute('aria-hidden', 'true');\n\n const text = doc.createElement('div');\n text.className = 'zine-loading-text';\n text.textContent = strings.loadingOpening;\n\n const bar = doc.createElement('div');\n bar.className = 'zine-loading-bar';\n bar.style.display = 'none';\n const fill = doc.createElement('div');\n fill.className = 'zine-loading-fill';\n bar.appendChild(fill);\n\n overlay.append(spinner, text, bar);\n container.appendChild(overlay);\n\n // The overlay is absolutely positioned; a statically-positioned container would let it escape.\n // Give the container a positioning context and restore it on teardown. getComputedStyle is\n // absent under some test DOMs, so guard it.\n let restorePosition: string | null = null;\n const computed =\n typeof globalThis.getComputedStyle === 'function' ? globalThis.getComputedStyle(container) : null;\n if (!computed || computed.position === 'static' || computed.position === '') {\n restorePosition = container.style.position;\n container.style.position = 'relative';\n }\n\n let revealed = false;\n const revealTimer = setTimeout(() => {\n revealed = true;\n overlay.classList.add('zine-loading-shown');\n }, REVEAL_DELAY_MS);\n\n return {\n update(progress: LoadProgress): void {\n if (progress.total > 0) {\n const pct = Math.min(100, Math.round((progress.loaded / progress.total) * 100));\n text.textContent = strings.downloadingPercent(pct);\n bar.style.display = '';\n fill.style.width = `${pct}%`;\n } else {\n text.textContent = strings.downloadingSize(progress.loaded / 1024 / 1024);\n bar.style.display = 'none';\n }\n },\n preparing(): void {\n text.textContent = strings.loadingPreparing;\n bar.style.display = 'none';\n },\n destroy(): void {\n if (!revealed) clearTimeout(revealTimer);\n overlay.remove();\n if (restorePosition !== null) container.style.position = restorePosition;\n },\n };\n}\n","/**\n * Toolbar glyphs, inlined so the bundle carries no icon-font or sprite dependency.\n *\n * Paths are from Lucide (https://lucide.dev), ISC licensed:\n * Copyright (c) for portions of Lucide are held by Cole Bemis 2013-2022 as part of Feather\n * (MIT). All other copyright (c) for Lucide are held by Lucide Contributors 2022.\n *\n * Each value is the inner markup of a 24x24 `viewBox` drawn with `stroke=\"currentColor\"`,\n * `fill=\"none\"` and round caps/joins — {@link createIcon} supplies those, so the strings stay to\n * the geometry.\n */\n// Not annotated `Record<string, string>`: that would make every ICONS.foo possibly-undefined\n// under noUncheckedIndexedAccess, even for names written right here. `satisfies` keeps the\n// literal's exact keys while still checking the values are markup strings.\nexport const ICONS = {\n /** chevron-left */\n prev: '<path d=\"m15 18-6-6 6-6\"/>',\n /** chevron-right */\n next: '<path d=\"m9 18 6-6-6-6\"/>',\n /** chevrons-left */\n first: '<path d=\"m11 17-5-5 5-5\"/><path d=\"m18 17-5-5 5-5\"/>',\n /** chevrons-right */\n last: '<path d=\"m6 17 5-5-5-5\"/><path d=\"m13 17 5-5-5-5\"/>',\n /** circle-plus: zoom in. A plus, not a magnifier, so it never reads as a third \"search\". */\n zoomIn: '<circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M8 12h8\"/><path d=\"M12 8v8\"/>',\n /** circle-minus: zoom out, paired with zoomIn. */\n zoomOut: '<circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M8 12h8\"/>',\n /** search */\n search: '<circle cx=\"11\" cy=\"11\" r=\"8\"/><path d=\"m21 21-4.3-4.3\"/>',\n /** share-2 */\n share:\n '<circle cx=\"18\" cy=\"5\" r=\"3\"/><circle cx=\"6\" cy=\"12\" r=\"3\"/><circle cx=\"18\" cy=\"19\" r=\"3\"/><path d=\"m8.6 13.5 6.8 4\"/><path d=\"m15.4 6.5-6.8 4\"/>',\n /** ellipsis-vertical */\n menu: '<circle cx=\"12\" cy=\"12\" r=\"1\"/><circle cx=\"12\" cy=\"5\" r=\"1\"/><circle cx=\"12\" cy=\"19\" r=\"1\"/>',\n /** maximize */\n fullscreen:\n '<path d=\"M8 3H5a2 2 0 0 0-2 2v3\"/><path d=\"M21 8V5a2 2 0 0 0-2-2h-3\"/><path d=\"M3 16v3a2 2 0 0 0 2 2h3\"/><path d=\"M16 21h3a2 2 0 0 0 2-2v-3\"/>',\n /** minimize */\n exitFullscreen:\n '<path d=\"M8 3v3a2 2 0 0 1-2 2H3\"/><path d=\"M21 8h-3a2 2 0 0 1-2-2V3\"/><path d=\"M3 16h3a2 2 0 0 1 2 2v3\"/><path d=\"M16 21v-3a2 2 0 0 1 2-2h3\"/>',\n /** gallery-vertical-end */\n thumbnails:\n '<path d=\"M7 2h10\"/><path d=\"M5 6h14\"/><rect width=\"18\" height=\"12\" x=\"3\" y=\"10\" rx=\"2\"/>',\n /** list-tree */\n outline:\n '<path d=\"M21 12h-8\"/><path d=\"M21 6h-8\"/><path d=\"M21 18h-8\"/><path d=\"M3 6v4c0 1.1.9 2 2 2h3\"/><path d=\"M3 10v6c0 1.1.9 2 2 2h3\"/>',\n /** book-open: two facing pages */\n twoPages:\n '<path d=\"M12 7v14\"/><path d=\"M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z\"/>',\n /** file: a single page */\n onePage: '<path d=\"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z\"/><path d=\"M14 2v5h6\"/>',\n /** printer */\n print:\n '<path d=\"M6 9V2h12v7\"/><path d=\"M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2\"/><rect width=\"12\" height=\"8\" x=\"6\" y=\"14\" rx=\"1\"/>',\n /** download */\n download: '<path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\"/><path d=\"M7 10l5 5 5-5\"/><path d=\"M12 15V3\"/>',\n /** x */\n close: '<path d=\"M18 6 6 18\"/><path d=\"m6 6 12 12\"/>',\n /** volume-2: sound on */\n soundOn:\n '<path d=\"M11 4.7a.7.7 0 0 0-1.2-.5L6 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h3l3.8 3.8a.7.7 0 0 0 1.2-.5z\"/><path d=\"M16 9a5 5 0 0 1 0 6\"/><path d=\"M19.4 5.6a10 10 0 0 1 0 12.8\"/>',\n /** volume-x: muted */\n soundOff:\n '<path d=\"M11 4.7a.7.7 0 0 0-1.2-.5L6 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h3l3.8 3.8a.7.7 0 0 0 1.2-.5z\"/><path d=\"m16 9 5 6\"/><path d=\"m21 9-5 6\"/>',\n} satisfies Record<string, string>;\n\n/** Build an `<svg>` for one of the {@link ICONS}, or null if the name is unknown. */\nexport function createIcon(doc: Document, markup: string): SVGSVGElement {\n const svg = doc.createElementNS('http://www.w3.org/2000/svg', 'svg');\n svg.setAttribute('viewBox', '0 0 24 24');\n svg.setAttribute('fill', 'none');\n svg.setAttribute('stroke', 'currentColor');\n // Slightly under Lucide's default 2: at the toolbar's icon size a full-weight stroke reads\n // heavy and closes up the tighter shapes.\n svg.setAttribute('stroke-width', '1.75');\n svg.setAttribute('stroke-linecap', 'round');\n svg.setAttribute('stroke-linejoin', 'round');\n svg.setAttribute('aria-hidden', 'true');\n svg.setAttribute('focusable', 'false');\n // Icon markup is library-authored, never consumer input.\n svg.innerHTML = markup;\n return svg;\n}\n","import type { ControlDef, ControlItem } from './types';\n\n/**\n * The control registry.\n *\n * Deliberately just the mechanism — no icons, no built-in definitions — because `defineControl`\n * has to be callable before the toolbar chunk loads, so this module lands in the base bundle.\n * The built-ins live in `./builtins`, which the lazy chunk pulls in along with the icon set.\n */\nconst registry = new Map<string, ControlDef>();\n\n/**\n * Add a control, or replace one by reusing its id. Returns the definition so it can be dropped\n * straight into a layout.\n *\n * ```js\n * defineControl({ id: 'print', title: 'Print', icon: '<path d=\"…\"/>', action: () => print() });\n * new Zine(el, { source, controls: { items: ['prev', 'next', 'print'] } });\n * ```\n */\nexport function defineControl(def: ControlDef): ControlDef {\n registry.set(def.id, def);\n return def;\n}\n\n/** Look up a registered control. */\nexport function getControl(id: string): ControlDef | undefined {\n return registry.get(id);\n}\n\n/**\n * Turn a layout entry into a concrete definition. A bare string names a registered control; an\n * object with only some fields is layered over the registered one, so `{ id: 'next', title: 'Go\n * on' }` relabels the built-in without reimplementing it.\n */\nexport function resolveControl(item: ControlItem): ControlDef {\n if (typeof item === 'string') {\n const found = registry.get(item);\n if (!found) {\n throw new Error(\n `controls: unknown control '${item}'. Register it with defineControl() or pass a definition object.`,\n );\n }\n return found;\n }\n const base = registry.get(item.id);\n if (!base) return item as ControlDef;\n return { ...base, ...item };\n}\n\n/** The toolbar shown when `items` is not given. */\nexport const DEFAULT_ITEMS: readonly ControlItem[] = [\n 'prev',\n 'pageInput',\n 'next',\n '|',\n 'zoomOut',\n 'zoomIn',\n 'search',\n 'share',\n 'menu',\n 'fullscreen',\n];\n","/**\n * Where a reader can send a link, and how each service wants it.\n *\n * These are plain share-intent URLs — no SDKs, no third-party scripts, nothing that could track\n * the reader before they choose to share. Each brand mark is drawn from its own official glyph;\n * they are filled shapes rather than the stroked outlines used elsewhere in the toolbar, so they\n * carry `fill=\"currentColor\"` and no stroke.\n */\nexport interface Social {\n id: string;\n label: string;\n /** Inner SVG markup for a 24x24 viewBox, filled rather than stroked. */\n icon: string;\n href(url: string, title: string): string;\n}\n\nconst e = encodeURIComponent;\n\nexport const SOCIALS: readonly Social[] = [\n {\n id: 'facebook',\n label: 'Facebook',\n icon: '<path fill=\"currentColor\" stroke=\"none\" d=\"M22 12a10 10 0 1 0-11.56 9.88v-6.99H7.9V12h2.54V9.8c0-2.5 1.49-3.89 3.77-3.89 1.1 0 2.24.2 2.24.2v2.46h-1.26c-1.24 0-1.63.77-1.63 1.56V12h2.78l-.45 2.89h-2.33v6.99A10 10 0 0 0 22 12Z\"/>',\n href: (url) => `https://www.facebook.com/sharer/sharer.php?u=${e(url)}`,\n },\n {\n id: 'x',\n label: 'X (Twitter)',\n icon: '<path fill=\"currentColor\" stroke=\"none\" d=\"M17.53 3h3.06l-6.69 7.64L21.75 21h-6.16l-4.82-6.3L5.24 21H2.18l7.15-8.17L2.25 3h6.32l4.36 5.77L17.53 3Zm-1.07 16.17h1.69L7.62 4.73H5.8l10.66 14.44Z\"/>',\n href: (url, title) => `https://twitter.com/intent/tweet?url=${e(url)}&text=${e(title)}`,\n },\n {\n id: 'linkedin',\n label: 'LinkedIn',\n icon: '<path fill=\"currentColor\" stroke=\"none\" d=\"M20.45 20.45h-3.56v-5.57c0-1.33-.02-3.04-1.85-3.04-1.85 0-2.14 1.45-2.14 2.94v5.67H9.35V9h3.41v1.56h.05a3.74 3.74 0 0 1 3.37-1.85c3.6 0 4.27 2.37 4.27 5.46v6.28ZM5.34 7.43a2.07 2.07 0 1 1 0-4.13 2.07 2.07 0 0 1 0 4.13Zm1.78 13.02H3.55V9h3.57v11.45ZM22.22 0H1.77C.79 0 0 .77 0 1.72v20.56C0 23.23.79 24 1.77 24h20.45c.98 0 1.78-.77 1.78-1.72V1.72C24 .77 23.2 0 22.22 0Z\"/>',\n href: (url) => `https://www.linkedin.com/sharing/share-offsite/?url=${e(url)}`,\n },\n {\n id: 'whatsapp',\n label: 'WhatsApp',\n icon: '<path fill=\"currentColor\" stroke=\"none\" d=\"M12.04 2a9.9 9.9 0 0 0-8.5 14.9L2 22l5.25-1.38A9.9 9.9 0 1 0 12.04 2Zm5.8 14.06c-.25.7-1.44 1.33-1.99 1.38-.53.05-1.02.24-3.44-.72-2.9-1.14-4.73-4.1-4.87-4.29-.14-.19-1.16-1.54-1.16-2.94s.73-2.08 1-2.37c.26-.28.57-.35.76-.35h.55c.17 0 .42-.07.65.5.24.58.8 2 .87 2.14.07.14.12.31.02.5-.09.19-.14.3-.28.47l-.42.48c-.14.14-.28.29-.12.57.16.28.7 1.16 1.51 1.88 1.04.93 1.91 1.21 2.19 1.35.28.15.44.12.6-.07.17-.19.7-.81.88-1.09.19-.28.37-.23.63-.14.25.1 1.63.77 1.9.91.29.14.48.21.55.33.07.11.07.67-.18 1.37Z\"/>',\n href: (url, title) => `https://api.whatsapp.com/send?text=${e(`${title} ${url}`)}`,\n },\n {\n id: 'pinterest',\n label: 'Pinterest',\n icon: '<path fill=\"currentColor\" stroke=\"none\" d=\"M12 2a10 10 0 0 0-3.65 19.31c-.09-.78-.17-1.98.03-2.83.19-.78 1.2-4.98 1.2-4.98s-.3-.61-.3-1.52c0-1.42.82-2.48 1.85-2.48.87 0 1.3.66 1.3 1.44 0 .88-.56 2.2-.85 3.42-.24 1.02.51 1.86 1.52 1.86 1.83 0 3.23-1.93 3.23-4.7 0-2.46-1.77-4.18-4.29-4.18-2.92 0-4.64 2.19-4.64 4.46 0 .88.34 1.83.76 2.35a.3.3 0 0 1 .07.29l-.28 1.16c-.05.19-.15.23-.34.14-1.28-.6-2.08-2.47-2.08-3.98 0-3.24 2.35-6.21 6.79-6.21 3.56 0 6.33 2.54 6.33 5.93 0 3.54-2.23 6.39-5.32 6.39-1.04 0-2.02-.54-2.35-1.18l-.64 2.44c-.23.89-.86 2.01-1.28 2.69A10 10 0 1 0 12 2Z\"/>',\n href: (url, title) => `https://pinterest.com/pin/create/button/?url=${e(url)}&description=${e(title)}`,\n },\n {\n id: 'email',\n label: 'Email',\n icon: '<path fill=\"currentColor\" stroke=\"none\" d=\"M20 4H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2Zm0 4.24-7.47 4.67a1 1 0 0 1-1.06 0L4 8.24V6.4l8 5 8-5v1.84Z\"/>',\n href: (url, title) => `mailto:?subject=${e(title)}&body=${e(url)}`,\n },\n];\n","/**\n * A small QR encoder, enough to turn a URL into a scannable grid.\n *\n * Written rather than pulled in so the package keeps its \"no dependencies\" promise, and so no\n * reader's document URL is handed to a third-party image service to be logged. Deliberately\n * narrow: byte mode, error-correction level M, versions 1-10 (up to 271 characters at level M),\n * which covers any sane page URL. Longer input throws rather than silently producing a code that\n * will not scan.\n */\n\n/** Data codewords available per version at error-correction level M, versions 1-10. */\nconst DATA_CODEWORDS = [16, 28, 44, 64, 86, 108, 124, 154, 182, 216];\n/** Error-correction codewords per block, level M, versions 1-10. */\nconst EC_PER_BLOCK = [10, 16, 26, 18, 24, 16, 18, 22, 22, 26];\n/** How many EC blocks the data is split into, level M, versions 1-10. */\nconst BLOCKS = [1, 1, 1, 2, 2, 4, 4, 4, 5, 5];\n/** Where alignment patterns sit, per version (empty for version 1). */\nconst ALIGNMENT = [\n [],\n [6, 18],\n [6, 22],\n [6, 26],\n [6, 30],\n [6, 34],\n [6, 22, 38],\n [6, 24, 42],\n [6, 26, 46],\n [6, 28, 50],\n];\n\n// Galois field tables for GF(256), used by the Reed-Solomon error correction.\nconst EXP = new Uint8Array(512);\nconst LOG = new Uint8Array(256);\nfor (let i = 0, x = 1; i < 255; i++) {\n EXP[i] = x;\n LOG[x] = i;\n x <<= 1;\n if (x & 0x100) x ^= 0x11d; // the QR field's primitive polynomial\n}\nfor (let i = 255; i < 512; i++) EXP[i] = EXP[i - 255]!;\n\nconst mul = (a: number, b: number): number =>\n a === 0 || b === 0 ? 0 : EXP[(LOG[a]! + LOG[b]!) % 255]!;\n\n/** The generator polynomial for `degree` error-correction codewords. */\nfunction generator(degree: number): Uint8Array {\n let poly = new Uint8Array([1]);\n for (let i = 0; i < degree; i++) {\n const next = new Uint8Array(poly.length + 1);\n for (let j = 0; j < poly.length; j++) {\n // Written as explicit reads: a compound assignment on an indexed element is what\n // noUncheckedIndexedAccess objects to, since the target is possibly-undefined.\n next[j] = next[j]! ^ poly[j]!;\n next[j + 1] = next[j + 1]! ^ mul(poly[j]!, EXP[i]!);\n }\n poly = next;\n }\n return poly;\n}\n\n/** Reed-Solomon remainder: the error-correction codewords for one block. */\nfunction ecBlock(data: Uint8Array, count: number): Uint8Array {\n const gen = generator(count);\n const out = new Uint8Array(count);\n for (const byte of data) {\n const factor = byte ^ out[0]!;\n out.copyWithin(0, 1);\n out[count - 1] = 0;\n if (factor !== 0) {\n for (let i = 0; i < count; i++) out[i] = out[i]! ^ mul(gen[i + 1]!, factor);\n }\n }\n return out;\n}\n\n/** The 15-bit format string for level M and a mask, with its BCH check bits. */\nfunction formatBits(mask: number): number {\n const data = (0b00 << 3) | mask; // 00 = level M\n let rem = data;\n for (let i = 0; i < 10; i++) rem = (rem << 1) ^ ((rem >>> 9) * 0x537);\n return ((data << 10) | rem) ^ 0x5412;\n}\n\ntype Grid = { size: number; modules: Uint8Array; reserved: Uint8Array };\n\nconst at = (g: Grid, x: number, y: number): number => g.modules[y * g.size + x]!;\nconst set = (g: Grid, x: number, y: number, dark: boolean, reserve = true): void => {\n g.modules[y * g.size + x] = dark ? 1 : 0;\n if (reserve) g.reserved[y * g.size + x] = 1;\n};\n\nfunction placeFinder(g: Grid, cx: number, cy: number): void {\n for (let dy = -1; dy <= 7; dy++) {\n for (let dx = -1; dx <= 7; dx++) {\n const x = cx + dx;\n const y = cy + dy;\n if (x < 0 || y < 0 || x >= g.size || y >= g.size) continue;\n const d = Math.max(Math.abs(dx - 3), Math.abs(dy - 3));\n set(g, x, y, d !== 2 && d <= 3);\n }\n }\n}\n\n/** Encode `text` as a QR matrix: a square of 0/1 by row. */\nexport function encodeQr(text: string): { size: number; modules: Uint8Array } {\n const bytes = new TextEncoder().encode(text);\n const version = DATA_CODEWORDS.findIndex((cap) => bytes.length + 2 <= cap) + 1;\n if (version === 0) {\n throw new Error(`qr: ${bytes.length} bytes is too long for this encoder (max 214).`);\n }\n\n // --- bit stream: mode, length, payload, terminator, padding ---\n const capacity = DATA_CODEWORDS[version - 1]!;\n const bits: number[] = [];\n const push = (value: number, width: number): void => {\n for (let i = width - 1; i >= 0; i--) bits.push((value >>> i) & 1);\n };\n push(0b0100, 4); // byte mode\n push(bytes.length, version < 10 ? 8 : 16);\n for (const byte of bytes) push(byte, 8);\n push(0, Math.min(4, capacity * 8 - bits.length)); // terminator\n while (bits.length % 8) bits.push(0);\n const data = new Uint8Array(capacity);\n for (let i = 0; i < bits.length; i += 8) {\n let byte = 0;\n for (let b = 0; b < 8; b++) byte = (byte << 1) | bits[i + b]!;\n data[i / 8] = byte;\n }\n for (let i = bits.length / 8, pad = 0; i < capacity; i++, pad++) {\n data[i] = pad % 2 === 0 ? 0xec : 0x11; // the spec's alternating pad bytes\n }\n\n // --- split into blocks, compute EC, interleave ---\n const blockCount = BLOCKS[version - 1]!;\n const ecCount = EC_PER_BLOCK[version - 1]!;\n const shortLen = Math.floor(capacity / blockCount);\n const longCount = capacity % blockCount;\n const dataBlocks: Uint8Array[] = [];\n const ecBlocks: Uint8Array[] = [];\n for (let i = 0, offset = 0; i < blockCount; i++) {\n const len = shortLen + (i >= blockCount - longCount ? 1 : 0);\n const block = data.subarray(offset, offset + len);\n offset += len;\n dataBlocks.push(block);\n ecBlocks.push(ecBlock(block, ecCount));\n }\n const codewords: number[] = [];\n for (let i = 0; i < shortLen + 1; i++) {\n for (const block of dataBlocks) if (i < block.length) codewords.push(block[i]!);\n }\n for (let i = 0; i < ecCount; i++) {\n for (const block of ecBlocks) codewords.push(block[i]!);\n }\n\n // --- lay out the matrix ---\n const size = version * 4 + 17;\n const g: Grid = {\n size,\n modules: new Uint8Array(size * size),\n reserved: new Uint8Array(size * size),\n };\n placeFinder(g, 0, 0);\n placeFinder(g, size - 7, 0);\n placeFinder(g, 0, size - 7);\n for (let i = 8; i < size - 8; i++) {\n const dark = i % 2 === 0; // timing patterns\n set(g, i, 6, dark);\n set(g, 6, i, dark);\n }\n for (const ax of ALIGNMENT[version - 1]!) {\n for (const ay of ALIGNMENT[version - 1]!) {\n // Alignment patterns skip the corners already held by finders.\n if ((ax < 9 && ay < 9) || (ax < 9 && ay > size - 10) || (ax > size - 10 && ay < 9)) continue;\n for (let dy = -2; dy <= 2; dy++) {\n for (let dx = -2; dx <= 2; dx++) {\n set(g, ax + dx, ay + dy, Math.max(Math.abs(dx), Math.abs(dy)) !== 1);\n }\n }\n }\n }\n set(g, 8, size - 8, true); // the always-dark module\n // Reserve the format areas so data does not land in them.\n for (let i = 0; i < 9; i++) {\n if (i !== 6) set(g, i, 8, false), set(g, 8, i, false);\n }\n for (let i = 0; i < 8; i++) set(g, size - 1 - i, 8, false), set(g, 8, size - 1 - i, false);\n set(g, 8, size - 8, true);\n\n // --- weave the codewords in, masking as we go (mask 0: (x + y) % 2) ---\n let bit = 0;\n const total = codewords.length * 8;\n for (let right = size - 1; right >= 1; right -= 2) {\n if (right === 6) right = 5; // the vertical timing column is skipped\n for (let step = 0; step < size; step++) {\n for (let col = 0; col < 2; col++) {\n const x = right - col;\n const upward = ((right + 1) & 2) === 0;\n const y = upward ? size - 1 - step : step;\n if (g.reserved[y * size + x]) continue;\n let dark = false;\n if (bit < total) {\n dark = ((codewords[bit >>> 3]! >>> (7 - (bit & 7))) & 1) === 1;\n bit++;\n }\n if ((x + y) % 2 === 0) dark = !dark; // mask 0\n set(g, x, y, dark, false);\n }\n }\n }\n\n // --- format information, now that the mask is known ---\n const format = formatBits(0);\n for (let i = 0; i < 15; i++) {\n const dark = ((format >>> i) & 1) === 1;\n // Around the top-left finder...\n const a = i < 6 ? i : i < 8 ? i + 1 : 8;\n const b = i < 8 ? 8 : i < 9 ? 7 : 14 - i;\n set(g, a, 8, dark);\n set(g, 8, b, dark);\n // ...and the copy split between the other two.\n if (i < 8) set(g, size - 1 - i, 8, dark);\n else set(g, 8, size - 15 + i, dark);\n }\n set(g, 8, size - 8, true);\n\n return { size, modules: g.modules };\n}\n\n/** Render a QR matrix as an SVG string, sized to `px`. */\nexport function qrSvg(text: string, px: number): string {\n const { size, modules } = encodeQr(text);\n let path = '';\n for (let y = 0; y < size; y++) {\n for (let x = 0; x < size; x++) {\n if (modules[y * size + x]) path += `M${x} ${y}h1v1h-1z`;\n }\n }\n const quiet = 2; // the spec asks for 4; 2 scans fine and wastes less space on screen\n const box = size + quiet * 2;\n return `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${px}\" height=\"${px}\" viewBox=\"0 0 ${box} ${box}\" shape-rendering=\"crispEdges\"><rect width=\"${box}\" height=\"${box}\" fill=\"#fff\"/><g transform=\"translate(${quiet} ${quiet})\" fill=\"#000\">${`<path d=\"${path}\"/>`}</g></svg>`;\n}\n","/**\n * The toolbar's stylesheet, injected once per document.\n *\n * The rest of the library styles elements inline, but a toolbar needs `:hover`, `:focus-visible`\n * and `:disabled` — states inline styles cannot express — so this is the one place a `<style>`\n * element is warranted. Everything is scoped under `.zine-controls`.\n *\n * Colours come from custom properties so a consumer can retheme without overriding rules. Left\n * unset, each falls back to `light-dark(light, dark)`, which resolves against the element's used\n * `color-scheme`: the toolbar follows the host page's scheme with no configuration, and a host\n * with a theme toggle only has to set `color-scheme` on its root for the controls to match. The\n * `colorScheme` control option can force `light` or `dark` regardless of the page, and a set\n * `--zine-controls-*` token still wins over both.\n */\nconst STYLE_ID = 'zine-controls-style';\n\nexport const CSS = `\n/* Docked mode wraps the book so the bar can sit beside it without shrinking the container,\n whose measured box the renderer and hit-testing both depend on.\n The wrapper deliberately does not stretch or grow its children: the container keeps whatever\n width, aspect-ratio and auto-margins the consumer's own CSS gave it. */\n.zine-controls-wrap { display: flex; }\n/* Stacked: children keep their own width (so a percentage or auto-margin still resolves against\n the wrapper) and take only the height they ask for. */\n.zine-controls-wrap-top,\n.zine-controls-wrap-bottom { flex-direction: column; align-items: stretch; }\n/* Side by side: the bar is only as wide as its buttons, and both are vertically centred. */\n.zine-controls-wrap-left,\n.zine-controls-wrap-right { flex-direction: row; align-items: center; }\n/* min-height:0 matters as much as min-width here. A flex item's automatic minimum size floors it\n at its content, so once the canvas has grown for a taller layout the book cannot shrink back —\n its aspect-ratio is restored but ignored, leaving the extra height behind. */\n.zine-controls-wrap > * { flex: 0 0 auto; min-width: 0; min-height: 0; }\n\n.zine-controls {\n position: relative;\n display: flex;\n gap: 4px;\n padding: 5px;\n z-index: 2;\n box-sizing: border-box;\n font: 500 12px/1.2 system-ui, sans-serif;\n color: var(--zine-controls-fg, light-dark(#18181b, #f4f4f5));\n touch-action: auto;\n -webkit-user-select: none;\n user-select: none;\n}\n.zine-controls-docked { flex: 0 0 auto; justify-content: center; align-items: center; }\n.zine-controls-floating {\n position: absolute;\n /* Transparent to the book except on the buttons themselves, so gaps stay draggable. */\n pointer-events: none;\n}\n.zine-controls-bar {\n display: flex;\n align-items: center;\n gap: 1px;\n padding: 3px;\n border-radius: 8px;\n pointer-events: auto;\n background: var(--zine-controls-bg, light-dark(rgba(255, 255, 255, 0.94), rgba(24, 24, 27, 0.82)));\n backdrop-filter: blur(8px);\n}\n/* Floating: lifted off the page it covers. Docked: flat, with an outline so it reads as a\n control strip rather than a shadow hanging in empty space. */\n.zine-controls-floating .zine-controls-bar { box-shadow: 0 2px 12px rgba(0, 0, 0, 0.28); }\n.zine-controls-docked .zine-controls-bar {\n border: 1px solid var(--zine-controls-hover, light-dark(rgba(0, 0, 0, 0.08), rgba(255, 255, 255, 0.14)));\n}\n.zine-controls-floating.zine-controls-bottom { inset: auto 0 0 0; justify-content: center; }\n.zine-controls-floating.zine-controls-top { inset: 0 0 auto 0; justify-content: center; }\n.zine-controls-floating.zine-controls-left { inset: 0 auto 0 0; align-items: center; }\n.zine-controls-floating.zine-controls-right { inset: 0 0 0 auto; align-items: center; }\n/* A bar on a vertical edge stacks its buttons, docked or floating. */\n.zine-controls-left .zine-controls-bar,\n.zine-controls-right .zine-controls-bar { flex-direction: column; }\n\n.zine-controls-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 26px;\n height: 26px;\n padding: 0;\n border: 0;\n border-radius: 6px;\n background: transparent;\n color: inherit;\n cursor: pointer;\n font: inherit;\n}\n.zine-controls-btn svg { width: 15px; height: 15px; }\n.zine-controls-btn:hover:not(:disabled) { background: var(--zine-controls-hover, light-dark(rgba(0,0,0,0.08), rgba(255,255,255,0.14))); }\n.zine-controls-btn:focus-visible {\n outline: 2px solid var(--zine-controls-accent, light-dark(#0284c7, #7dd3fc));\n outline-offset: 1px;\n}\n.zine-controls-btn:disabled { opacity: 0.38; cursor: default; }\n/* A button mid-slow-action (fetching a file to save or print): a spinner, kept full-strength\n rather than dimmed so it reads as \"working\", not \"disabled\". */\n.zine-controls-btn.zine-controls-busy { opacity: 1; cursor: default; position: relative; }\n.zine-controls-btn.zine-controls-busy::after {\n content: '';\n box-sizing: border-box;\n width: 14px;\n height: 14px;\n border: 2px solid currentColor;\n border-top-color: transparent;\n border-radius: 50%;\n opacity: 0.7;\n animation: zine-controls-spin 0.7s linear infinite;\n}\n/* Icon-only bar button: hide the glyph and center the spinner over its place. */\n.zine-controls-btn.zine-controls-busy > svg { visibility: hidden; }\n.zine-controls-btn.zine-controls-busy::after {\n position: absolute;\n top: 50%;\n left: 50%;\n margin: -7px 0 0 -7px;\n}\n/* Menu item (glyph + label): let the glyph and label stand, and trail the spinner after them. */\n.zine-controls-menu .zine-controls-btn.zine-controls-busy > svg { visibility: visible; }\n.zine-controls-menu .zine-controls-btn.zine-controls-busy::after { position: static; }\n@keyframes zine-controls-spin { to { transform: rotate(360deg); } }\n@media (prefers-reduced-motion: reduce) {\n .zine-controls-btn.zine-controls-busy::after { animation-duration: 1.6s; }\n}\n/* A custom widget cannot always be disabled itself, so it is marked instead and reads the same. */\n.zine-controls-off { opacity: 0.38; }\n.zine-controls-off[aria-disabled='true'] { pointer-events: none; }\n.zine-controls-btn[aria-pressed=\"true\"],\n.zine-controls-btn[aria-expanded=\"true\"] { background: var(--zine-controls-hover, light-dark(rgba(0,0,0,0.08), rgba(255,255,255,0.14))); }\n\n.zine-controls-sep {\n width: 1px;\n align-self: stretch;\n margin: 3px 3px;\n background: currentColor;\n opacity: 0.22;\n}\n.zine-controls-left .zine-controls-sep,\n.zine-controls-right .zine-controls-sep { width: auto; height: 1px; margin: 3px 3px; }\n\n.zine-controls-page {\n display: inline-flex;\n align-items: center;\n gap: 3px;\n padding: 0 3px;\n pointer-events: auto;\n white-space: nowrap;\n}\n.zine-controls-page input {\n width: 2.2em;\n padding: 2px 3px;\n border: 1px solid var(--zine-controls-hover, light-dark(rgba(0,0,0,0.14), rgba(255,255,255,0.2)));\n border-radius: 4px;\n background: light-dark(rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.25));\n color: inherit;\n font: inherit;\n text-align: center;\n -moz-appearance: textfield;\n}\n.zine-controls-page input::-webkit-outer-spin-button,\n.zine-controls-page input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }\n.zine-controls-page input:focus-visible {\n outline: 2px solid var(--zine-controls-accent, light-dark(#0284c7, #7dd3fc));\n outline-offset: 0;\n}\n\n.zine-controls-menu {\n position: absolute;\n display: flex;\n flex-direction: column;\n gap: 1px;\n min-width: 152px;\n padding: 4px;\n border-radius: 8px;\n pointer-events: auto;\n background: var(--zine-controls-bg, light-dark(rgba(255, 255, 255, 0.94), rgba(24, 24, 27, 0.94)));\n box-shadow: 0 6px 22px rgba(0, 0, 0, 0.36);\n backdrop-filter: blur(8px);\n}\n.zine-controls-menu .zine-controls-btn {\n width: 100%;\n height: auto;\n gap: 8px;\n padding: 6px 8px;\n justify-content: flex-start;\n}\n\n/* The right-click menu opens in a fixed, viewport-spanning layer so the menu's absolute left/top\n are plain client coordinates — it lands under the cursor without the container having to be a\n positioning context. The layer itself is click-through; only the menu inside it takes events. */\n.zine-context-layer {\n position: fixed;\n inset: 0;\n z-index: 5;\n pointer-events: none;\n font: 500 12px/1.2 system-ui, sans-serif;\n color: var(--zine-controls-fg, light-dark(#18181b, #f4f4f5));\n -webkit-user-select: none;\n user-select: none;\n}\n\n/* Search shares the rail: a query field pinned at the top over a scrolling list of hits. */\n.zine-search { gap: 0; overflow: hidden; }\n.zine-search-input {\n flex: 0 0 auto;\n width: 100%;\n padding: 6px 8px;\n border: 1px solid var(--zine-controls-hover, light-dark(rgba(0,0,0,0.14), rgba(255,255,255,0.2)));\n border-radius: 6px;\n background: light-dark(rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.25));\n color: inherit;\n font: inherit;\n box-sizing: border-box;\n}\n/* Replace the browser's default (white) focus ring with the accent ring the other controls use. */\n.zine-search-input:focus-visible {\n outline: none;\n border-color: var(--zine-controls-accent, light-dark(#0284c7, #7dd3fc));\n box-shadow: 0 0 0 1px var(--zine-controls-accent, light-dark(#0284c7, #7dd3fc));\n}\n.zine-search-hits {\n flex: 1 1 auto;\n overflow-y: auto;\n /* Chain overscroll up to the page: once the list is at its end (or too short to scroll at all),\n a further wheel scrolls the page behind, the same as scrolling over the book itself does. */\n overscroll-behavior: auto;\n margin-top: 5px;\n scrollbar-width: thin;\n}\n.zine-search-hit {\n display: block;\n width: 100%;\n padding: 6px 8px;\n border: 0;\n border-radius: 5px;\n background: transparent;\n color: inherit;\n font: inherit;\n text-align: left;\n cursor: pointer;\n box-sizing: border-box;\n}\n.zine-search-hit:hover { background: var(--zine-controls-hover, light-dark(rgba(0,0,0,0.08), rgba(255,255,255,0.14))); }\n.zine-search-hit:focus-visible {\n outline: 2px solid var(--zine-controls-accent, light-dark(#0284c7, #7dd3fc));\n outline-offset: -2px;\n}\n.zine-search-page { display: block; font-size: 0.9em; opacity: 0.85; }\n.zine-search-hit small {\n display: block;\n margin-top: 2px;\n opacity: 0.62;\n line-height: 1.35;\n overflow-wrap: anywhere;\n}\n\n/* Page-turn arrows flanking the book. Like the docked toolbar and the side panels, they sit\n outside the container so they never shrink the book or intercept its gestures. */\n.zine-arrows-wrap {\n display: flex;\n align-items: center;\n gap: 4px;\n min-width: 0;\n}\n.zine-arrows-wrap > *:not(.zine-arrow) { flex: 1 1 auto; min-width: 0; min-height: 0; }\n.zine-arrow {\n flex: 0 0 auto;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 44px;\n height: 44px;\n padding: 0;\n border: 0;\n background: none;\n /* Light on a light page, dark on a dark one. The shadow below carries the contrast. */\n color: var(--zine-controls-fg, light-dark(#f4f4f5, #18181b));\n cursor: pointer;\n -webkit-user-select: none;\n user-select: none;\n /* No grey tap box on touch; the :active tint below is the press feedback instead. */\n -webkit-tap-highlight-color: transparent;\n}\n.zine-arrow svg {\n width: 36px;\n height: 36px;\n stroke-width: 2.25;\n /* No backing disc, so a subtle offset shadow keeps the chevron from disappearing into a\n matching background. It contrasts the fill: a light chevron (light mode) casts a soft dark\n shadow, a dark chevron (dark mode) a soft light one, offset rather than a glowing halo. */\n filter: drop-shadow(0 1px 2px light-dark(rgba(0, 0, 0, 0.5), rgba(255, 255, 255, 0.35)));\n}\n/* Feedback intensifies the chevron toward its extreme (white in light mode, black in dark)\n rather than tinting it, so no new colour is introduced. Not tied to --zine-controls-fg: that\n sets the resting fill, and hover has to differ from it to read as feedback. :active clears on\n release, so tap and click feel the same; :hover is behind (hover: hover) so it never sticks on\n touch. */\n.zine-arrow:active:not(:disabled) { color: light-dark(#ffffff, #000000); }\n@media (hover: hover) {\n .zine-arrow:hover:not(:disabled) { color: light-dark(#ffffff, #000000); }\n}\n.zine-arrow:focus-visible {\n outline: 2px solid var(--zine-controls-accent, light-dark(#0284c7, #7dd3fc));\n outline-offset: 2px;\n border-radius: 8px;\n}\n/* Nothing to turn to: invisible, but still occupying its slot so the book does not slide across\n as the reader reaches a cover. */\n.zine-arrow-hidden { visibility: hidden; }\n/* Device-scoped arrows (controls.arrows: 'desktop' | 'mobile'). The split follows the same 640px\n breakpoint as the flank/overlay layouts below, so the arrows appear and disappear live as the\n window crosses it. */\n@media (max-width: 640px) {\n .zine-arrows-desktop > .zine-arrow { display: none; }\n}\n@media (min-width: 641px) {\n .zine-arrows-mobile > .zine-arrow { display: none; }\n}\n/* Too narrow to flank without squeezing the pages, so overlay the arrows on the book's edges\n instead. The wrap sits outside the container, so painting them over it leaves the container's\n measured box (which sizes the book and maps taps) untouched. */\n@media (max-width: 640px) {\n .zine-arrows-wrap { position: relative; }\n .zine-arrow {\n position: absolute;\n top: 50%;\n transform: translateY(-50%);\n z-index: 2;\n /* Sit back over the page so the reader can look at the book. */\n opacity: 0.7;\n }\n .zine-arrow:first-child { left: -2px; }\n .zine-arrow:last-child { right: -2px; }\n}\n@media (prefers-reduced-motion: no-preference) {\n .zine-arrow { transition: background 120ms ease; }\n}\n\n/* Side panels (thumbnails, outline, search). The rail wraps the book and its docked toolbar so it\n sits beside them as a flex sibling. On a wide screen the book shrinks to make room (below); on a\n narrow one the rail becomes a drawer over the book.\n align-items:flex-start, not stretch: the book sizes its height from its width through its own\n aspect-ratio, and that height is 'auto' from flex's point of view, so a stretch would override it\n and blow the book (and the rail matched to it) up to the flex line's cross size. Left as auto, the\n book keeps its aspect height and the holder alone stretches down to meet it (below). */\n.zine-panel-wrap { display: flex; align-items: flex-start; gap: 8px; position: relative; }\n\n/* The book-side slot (the bare container, or the docked-toolbar wrapper around it) yields space to\n the fixed-width rail instead of overflowing: flex-shrink lets it fall below its own width, and\n min-width:0 removes the automatic content floor that would otherwise stop it. The book only\n actually shrinks if its width is elastic (a %, max-width, or the demo's min(...) with a % term);\n a hard-coded pixel width has nothing for the % to resolve smaller against. */\n.zine-panel-wrap > *:not(.zine-panel-holder):not(.zine-panel-scrim) {\n flex: 0 1 auto;\n min-width: 0;\n}\n/* With a docked toolbar the book lives inside .zine-controls-wrap, whose children are pinned\n flex:0 0 auto (so the bar keeps its size). Inside a panel wrap the book child — everything but\n the bar itself — must instead be allowed to shrink, so left/right docking flanks like the rest.\n Top/bottom docking already shrinks through the container's own % width. */\n.zine-panel-wrap .zine-controls-wrap > *:not(.zine-controls) {\n flex: 0 1 auto;\n min-width: 0;\n min-height: 0;\n}\n@media (prefers-reduced-motion: no-preference) {\n .zine-panel-wrap > *:not(.zine-panel-holder):not(.zine-panel-scrim) {\n transition: flex-basis 160ms ease, width 160ms ease;\n }\n}\n\n/* The holder stretches to the book's height; the rail is absolutely positioned inside it so a\n long list scrolls rather than growing the row and running past the bottom of the book. */\n.zine-panel-holder {\n position: relative;\n flex: 0 0 auto;\n align-self: stretch;\n min-height: 0;\n}\n/* The scrim dims the book behind the narrow-screen drawer. It exists on every screen but only\n shows under the breakpoint below, so on a wide screen it takes no flex slot and never intercepts\n a click. */\n.zine-panel-scrim { display: none; }\n.zine-panel {\n position: absolute;\n inset: 0;\n display: flex;\n flex-direction: column;\n gap: 6px;\n overflow-y: auto;\n /* Chain overscroll up to the page: once the list is at its end (or too short to scroll at all),\n a further wheel scrolls the page behind, the same as scrolling over the book itself does. */\n overscroll-behavior: auto;\n /* Reserve the scrollbar's lane whether or not it is showing, so rows never sit under the bar\n and the close button (inset past this lane below) has a fixed edge to clear. */\n scrollbar-gutter: stable;\n padding: 6px;\n box-sizing: border-box;\n border-radius: 8px;\n background: var(--zine-controls-bg, light-dark(rgba(255, 255, 255, 0.94), rgba(24, 24, 27, 0.82)));\n color: var(--zine-controls-fg, light-dark(#18181b, #f4f4f5));\n font: 500 11px/1.2 system-ui, sans-serif;\n scrollbar-width: thin;\n touch-action: auto;\n -webkit-user-select: none;\n user-select: none;\n}\n/* The rail sits flush against the book's reading-start edge (left, or right in RTL), so its outer\n corners there would round away from that edge and leave a gap. Square them; keep the interior\n corners, which face the page, rounded. */\n.zine-panel { border-radius: 0 8px 8px 0; }\n.zine-panel-wrap-rtl .zine-panel { border-radius: 8px 0 0 8px; }\n/* Overlay fallback, used when the container has no parent to wrap. */\n.zine-panel-overlay { position: absolute; inset: 0 auto 0 0; z-index: 3; }\n.zine-panel-active { background: var(--zine-controls-hover, light-dark(rgba(0,0,0,0.08), rgba(255,255,255,0.18))); }\n.zine-panel-note { padding: 8px; opacity: 0.66; line-height: 1.4; }\n/* Discoverable dismiss, floated in the rail's trailing-top (interior) corner, away from the book.\n It sits in the holder above the scrolling list so the list scrolls under it. A translucent\n backdrop keeps the glyph legible over a thumbnail or a line of text. */\n.zine-panel-close {\n position: absolute;\n top: 6px;\n /* Inset past the reserved scrollbar gutter so the button never overlaps the bar. */\n right: 12px;\n z-index: 1;\n display: inline-flex;\n width: 24px;\n height: 24px;\n align-items: center;\n justify-content: center;\n padding: 0;\n border: 0;\n border-radius: 6px;\n /* Opaque so a light thumbnail underneath never shows through and dims the glyph. */\n background: light-dark(#f4f4f5, #27272a);\n color: inherit;\n cursor: pointer;\n}\n.zine-panel-close svg { width: 15px; height: 15px; }\n/* Opaque hover, not the translucent hover token: a see-through backdrop over a light thumbnail\n would wash the glyph out. Solid neutrals keep the X readable in both themes over anything. */\n.zine-panel-close:hover { background: light-dark(#e4e4e7, #3f3f46); }\n.zine-panel-close:focus-visible {\n outline: 2px solid var(--zine-controls-accent, light-dark(#0284c7, #7dd3fc));\n outline-offset: -2px;\n}\n.zine-panel-wrap-rtl .zine-panel-close { right: auto; left: 12px; }\n/* End the search field just before the close button rather than running under it, so the two sit\n side by side and the field's border reads cleanly. The button's inner edge is ~30px from the\n panel's content edge (24px wide, its right edge 6px in once the reserved scrollbar gutter is\n accounted for), so 30px leaves them flush with a hair of breathing room. width:auto lets the\n column's stretch fill the rest, so the margin shortens the field instead of overflowing 100%. */\n.zine-search .zine-search-input { width: auto; align-self: stretch; margin-right: 30px; }\n.zine-panel-wrap-rtl .zine-search .zine-search-input { margin-right: 0; margin-left: 30px; }\n\n/* Overlay panels (outline, search) on a wide screen: float over the book's start edge instead of\n flanking it, so the book never resizes. The wrap drops to a block, laying the book out exactly as\n it was before the panel opened; the holder sits absolute over it at full book height, with a\n shadow so it reads as lifted off the page. thumbnails is not overlaid — it stays a flex flank and\n shrinks the book only when the two will not otherwise both fit. */\n@media (min-width: 641px) {\n .zine-panel-wrap-overlay { display: block; }\n .zine-panel-wrap-overlay .zine-panel-holder {\n position: absolute;\n inset: 0 auto 0 0;\n z-index: 4;\n box-shadow: 0 0 24px light-dark(rgba(0, 0, 0, 0.22), rgba(0, 0, 0, 0.55));\n }\n .zine-panel-wrap-overlay.zine-panel-wrap-rtl .zine-panel-holder { inset: 0 0 0 auto; }\n /* Opaque over the page it floats on, unlike a flanking rail that sits against empty margin. */\n .zine-panel-wrap-overlay .zine-panel {\n background: var(--zine-controls-bg, light-dark(#ffffff, #18181b));\n }\n /* A click-away layer over the book, so pressing the page behind the floating rail closes it the\n way tapping the scrim does on a narrow screen. Transparent here — the wide-screen rail only\n covers an edge, so there is no need to dim the page the reader is still looking at. It sits\n under the holder (z-index) so the rail's own rows still take their clicks. */\n .zine-panel-wrap-overlay .zine-panel-scrim {\n display: block;\n position: absolute;\n inset: 0;\n z-index: 3;\n background: transparent;\n }\n}\n@media (min-width: 641px) and (prefers-reduced-motion: no-preference) {\n .zine-panel-wrap-overlay .zine-panel-holder { animation: zine-drawer-in 180ms ease; }\n .zine-panel-wrap-overlay.zine-panel-wrap-rtl .zine-panel-holder { animation-name: zine-drawer-in-rtl; }\n}\n\n/* Narrow screen: no room to flank, so the rail becomes a drawer over the book and the scrim dims\n the page behind it. The wrap is position:relative, so the absolute holder and scrim below are\n measured against it — i.e. against the book's own box. */\n@media (max-width: 640px) {\n /* No flanking here — the rail and scrim both overlay absolutely — so drop the flex row entirely.\n As a plain block wrap the book keeps the exact box it had before a panel opened (its own width,\n margin and aspect-ratio height); a flex row would instead re-resolve the book's width and, with\n it, its aspect-derived height, enlarging the canvas and pushing the page down. */\n .zine-panel-wrap {\n display: block;\n }\n .zine-panel-holder {\n position: absolute;\n inset: 0 auto 0 0;\n z-index: 4;\n /* Cap the drawer so it never eats the whole book; its inline width still applies under this. */\n max-width: 80%;\n box-shadow: 0 0 24px light-dark(rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.6));\n }\n /* RTL reads from the right, so the drawer enters from there. */\n .zine-panel-wrap-rtl .zine-panel-holder { inset: 0 0 0 auto; }\n /* Opaque over the page, unlike the wide-screen rail which can sit against empty margin. */\n .zine-panel {\n background: var(--zine-controls-bg, light-dark(#ffffff, #18181b));\n }\n .zine-panel-scrim {\n display: block;\n position: absolute;\n inset: 0;\n z-index: 3;\n background: rgba(0, 0, 0, 0.4);\n }\n}\n@media (max-width: 640px) and (prefers-reduced-motion: no-preference) {\n .zine-panel-holder { animation: zine-drawer-in 180ms ease; }\n .zine-panel-wrap-rtl .zine-panel-holder { animation-name: zine-drawer-in-rtl; }\n .zine-panel-scrim { animation: zine-scrim-in 180ms ease; }\n}\n@keyframes zine-drawer-in { from { transform: translateX(-100%); } to { transform: translateX(0); } }\n@keyframes zine-drawer-in-rtl { from { transform: translateX(100%); } to { transform: translateX(0); } }\n@keyframes zine-scrim-in { from { opacity: 0; } to { opacity: 1; } }\n\n.zine-thumbs { align-items: center; }\n\n.zine-thumbs-row {\n display: flex;\n flex-wrap: wrap;\n justify-content: center;\n gap: 2px;\n width: 100%;\n padding: 4px;\n border: 0;\n border-radius: 6px;\n background: transparent;\n color: inherit;\n font: inherit;\n cursor: pointer;\n box-sizing: border-box;\n}\n.zine-thumbs-row:hover { background: var(--zine-controls-hover, light-dark(rgba(0,0,0,0.08), rgba(255,255,255,0.14))); }\n.zine-thumbs-row:focus-visible {\n outline: 2px solid var(--zine-controls-accent, light-dark(#0284c7, #7dd3fc));\n outline-offset: -2px;\n}\n.zine-thumbs-row.zine-panel-active .zine-thumbs-cell {\n outline: 1px solid var(--zine-controls-accent, light-dark(#0284c7, #7dd3fc));\n}\n\n/* Outline: a single column of headings, indented by depth. */\n/* No gap: a hairline between rows is the separator instead, so a title that wraps to two lines\n still reads as one entry rather than blending into its neighbours. */\n.zine-outline { gap: 0; }\n.zine-outline-row {\n display: block;\n width: 100%;\n padding: 6px 8px;\n border: 0;\n border-bottom: 1px solid var(--zine-controls-divider, light-dark(rgba(0, 0, 0, 0.08), rgba(255, 255, 255, 0.1)));\n border-radius: 5px;\n background: transparent;\n color: inherit;\n font: inherit;\n text-align: left;\n line-height: 1.35;\n cursor: pointer;\n box-sizing: border-box;\n overflow-wrap: anywhere;\n}\n.zine-outline-row:last-child { border-bottom: 0; }\n.zine-outline-row:hover:not(:disabled) {\n background: var(--zine-controls-hover, light-dark(rgba(0,0,0,0.08), rgba(255,255,255,0.14)));\n}\n.zine-outline-row:focus-visible {\n outline: 2px solid var(--zine-controls-accent, light-dark(#0284c7, #7dd3fc));\n outline-offset: -2px;\n}\n/* A heading whose destination could not be resolved: shown, but nothing to click through to. */\n.zine-outline-row:disabled { opacity: 0.5; cursor: default; }\n\n.zine-thumbs-cell {\n flex: 1 1 0;\n min-width: 0;\n min-height: 24px;\n display: flex;\n background: light-dark(rgba(0, 0, 0, 0.08), rgba(0, 0, 0, 0.3));\n border-radius: 2px;\n overflow: hidden;\n}\n/* A lone page in a book that pairs elsewhere (a cover, a back page) keeps one page's width and\n centres, instead of stretching across both columns. */\n.zine-thumbs-row-lone .zine-thumbs-cell { flex: 0 0 calc(50% - 1px); }\n.zine-thumbs-img { display: block; width: 100%; height: auto; }\n.zine-thumbs-caption { flex: 1 0 100%; text-align: center; opacity: 0.7; }\n\n/* Share dialog. Centred over the page rather than in the side rail: a QR and six destinations\n need more room than the rail gives, and sharing is a brief interruption, not a browsing mode. */\n.zine-share-backdrop {\n position: fixed;\n inset: 0;\n z-index: 2147483000;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 16px;\n background: rgba(0, 0, 0, 0.55);\n font: 500 13px/1.4 system-ui, sans-serif;\n color: var(--zine-controls-fg, light-dark(#18181b, #f4f4f5));\n}\n.zine-share {\n width: min(320px, 100%);\n max-height: 100%;\n overflow-y: auto;\n display: flex;\n flex-direction: column;\n gap: 12px;\n padding: 14px;\n border-radius: 12px;\n background: var(--zine-share-bg, light-dark(#ffffff, #1c1c20));\n box-shadow: 0 18px 50px rgba(0, 0, 0, 0.45);\n box-sizing: border-box;\n}\n.zine-share-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }\n.zine-share-head strong { font-size: 15px; }\n.zine-share-close {\n display: inline-flex;\n width: 26px;\n height: 26px;\n align-items: center;\n justify-content: center;\n padding: 0;\n border: 0;\n border-radius: 6px;\n background: transparent;\n color: inherit;\n cursor: pointer;\n}\n.zine-share-close svg { width: 16px; height: 16px; }\n.zine-share-close:hover { background: var(--zine-controls-hover, light-dark(rgba(0,0,0,0.08), rgba(255,255,255,0.14))); }\n\n.zine-share-qr {\n align-self: center;\n line-height: 0;\n padding: 8px;\n border-radius: 8px;\n background: #fff;\n}\n.zine-share-qr svg { display: block; }\n\n.zine-share-link { display: flex; gap: 6px; }\n.zine-share-link input {\n flex: 1 1 auto;\n min-width: 0;\n padding: 7px 9px;\n border: 1px solid var(--zine-controls-hover, light-dark(rgba(0,0,0,0.14), rgba(255,255,255,0.2)));\n border-radius: 7px;\n background: light-dark(rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.3));\n color: inherit;\n font: inherit;\n}\n.zine-share-copy {\n flex: 0 0 auto;\n min-width: 74px;\n padding: 7px 12px;\n border: 0;\n border-radius: 7px;\n background: var(--zine-controls-accent, light-dark(#0284c7, #7dd3fc));\n color: light-dark(#ffffff, #06202c);\n font: inherit;\n font-weight: 600;\n cursor: pointer;\n}\n\n.zine-share-socials { display: flex; flex-wrap: wrap; gap: 6px; justify-content: center; }\n.zine-share-social {\n display: inline-flex;\n width: 38px;\n height: 38px;\n align-items: center;\n justify-content: center;\n border-radius: 8px;\n background: light-dark(rgba(0, 0, 0, 0.06), rgba(255, 255, 255, 0.08));\n color: inherit;\n text-decoration: none;\n}\n.zine-share-social svg { width: 19px; height: 19px; }\n.zine-share-social:hover { background: var(--zine-controls-hover, light-dark(rgba(0,0,0,0.12), rgba(255,255,255,0.18))); }\n.zine-share-close:focus-visible,\n.zine-share-copy:focus-visible,\n.zine-share-social:focus-visible,\n.zine-share-link input:focus-visible {\n outline: 2px solid var(--zine-controls-accent, light-dark(#0284c7, #7dd3fc));\n outline-offset: 2px;\n}\n\n@media (prefers-reduced-motion: no-preference) {\n .zine-controls-btn { transition: background 120ms ease; }\n}\n`;\n\n/** Add the stylesheet to `doc` if it is not already there. */\nexport function ensureStyles(doc: Document): void {\n if (doc.getElementById(STYLE_ID)) return;\n const style = doc.createElement('style');\n style.id = STYLE_ID;\n style.textContent = CSS;\n (doc.head ?? doc.documentElement)?.appendChild(style);\n}\n\n/**\n * Pin an element's colour scheme so the `light-dark()` fallbacks resolve to one palette.\n *\n * `'auto'` does nothing: `color-scheme` inherits, so the element keeps the page's used scheme and\n * `light-dark()` follows it. `'light'` / `'dark'` set the property on the element itself, which its\n * themed descendants (buttons, menu, dialog) inherit in turn. An unset `--zine-controls-*` reads\n * the forced palette; a set one still overrides it, since the tokens sit outside `light-dark()`.\n */\nexport function applyColorScheme(el: HTMLElement, scheme: 'light' | 'dark' | 'auto'): void {\n if (scheme !== 'auto') el.style.colorScheme = scheme;\n}\n","import { createIcon, ICONS } from './icons';\nimport { SOCIALS } from './socials';\nimport { qrSvg } from './qr';\nimport { applyColorScheme } from './styles';\nimport type { ControlsColorScheme } from './types';\nimport type { Zine } from '../zine';\nimport type { ZineStrings } from '../strings';\n\n/** QR edge length in CSS px. */\nconst QR_SIZE = 132;\n/** How long the copy button confirms for. */\nconst COPIED_MS = 1600;\n\n/**\n * The share dialog: a QR code, the link, a copy button and the usual social destinations.\n *\n * Loaded on demand — the whole module, its icons and the QR encoder arrive only when a reader\n * presses Share, so a book nobody shares pays nothing for it.\n */\nexport class ShareDialog {\n #doc: Document;\n #backdrop: HTMLElement;\n #dialog: HTMLElement;\n #lastFocus: Element | null;\n #timer: ReturnType<typeof setTimeout> | null = null;\n #onKey: (e: KeyboardEvent) => void;\n #strings: ZineStrings;\n\n constructor(zine: Zine, container: HTMLElement, colorScheme: ControlsColorScheme = 'auto') {\n const doc = container.ownerDocument!;\n this.#doc = doc;\n this.#lastFocus = doc.activeElement;\n this.#strings = zine.strings;\n\n const url = zine.pageLink();\n const title = doc.title || this.#strings.shareFallbackTitle;\n\n this.#backdrop = doc.createElement('div');\n this.#backdrop.className = 'zine-share-backdrop';\n // The dialog is mounted on <body>, outside the toolbar, so it does not inherit the stamp;\n // apply it here. color-scheme then inherits down to the dialog and its fields.\n applyColorScheme(this.#backdrop, colorScheme);\n this.#backdrop.addEventListener('pointerdown', (e) => {\n if (e.target === this.#backdrop) this.close();\n });\n\n this.#dialog = doc.createElement('div');\n this.#dialog.className = 'zine-share';\n this.#dialog.setAttribute('role', 'dialog');\n this.#dialog.setAttribute('aria-modal', 'true');\n this.#dialog.setAttribute('aria-label', this.#strings.share);\n this.#backdrop.appendChild(this.#dialog);\n\n const head = doc.createElement('div');\n head.className = 'zine-share-head';\n const heading = doc.createElement('strong');\n heading.textContent = this.#strings.share;\n const close = doc.createElement('button');\n close.type = 'button';\n close.className = 'zine-share-close';\n close.setAttribute('aria-label', this.#strings.close);\n close.appendChild(createIcon(doc, ICONS.close));\n close.addEventListener('click', () => this.close());\n head.append(heading, close);\n\n this.#dialog.append(head, this.#linkRow(doc, url), this.#socialRow(doc, url, title));\n const qr = this.#qr(doc, url);\n if (qr) this.#dialog.insertBefore(qr, this.#dialog.children[1]!);\n\n container.ownerDocument.body.appendChild(this.#backdrop);\n\n this.#onKey = (e): void => {\n if (e.key === 'Escape') this.close();\n if (e.key === 'Tab') this.#trapFocus(e);\n };\n this.#backdrop.addEventListener('keydown', this.#onKey);\n close.focus();\n }\n\n /** The QR, or nothing if the URL is too long to encode — better no code than a broken one. */\n #qr(doc: Document, url: string): HTMLElement | null {\n if (!url) return null;\n try {\n const wrap = doc.createElement('div');\n wrap.className = 'zine-share-qr';\n wrap.innerHTML = qrSvg(url, QR_SIZE);\n wrap.setAttribute('aria-label', this.#strings.qrLabel);\n wrap.setAttribute('role', 'img');\n return wrap;\n } catch {\n return null;\n }\n }\n\n #linkRow(doc: Document, url: string): HTMLElement {\n const row = doc.createElement('div');\n row.className = 'zine-share-link';\n const input = doc.createElement('input');\n input.type = 'text';\n input.readOnly = true;\n input.value = url;\n input.setAttribute('aria-label', this.#strings.linkLabel);\n input.addEventListener('focus', () => input.select());\n\n const copy = doc.createElement('button');\n copy.type = 'button';\n copy.className = 'zine-share-copy';\n copy.textContent = this.#strings.copy;\n copy.addEventListener('click', () => {\n void this.#copy(url, copy);\n });\n row.append(input, copy);\n return row;\n }\n\n async #copy(url: string, button: HTMLButtonElement): Promise<void> {\n try {\n await navigator.clipboard?.writeText(url);\n button.textContent = this.#strings.copied;\n } catch {\n button.textContent = this.#strings.copyManual;\n }\n if (this.#timer) clearTimeout(this.#timer);\n this.#timer = setTimeout(() => {\n button.textContent = this.#strings.copy;\n }, COPIED_MS);\n }\n\n #socialRow(doc: Document, url: string, title: string): HTMLElement {\n const row = doc.createElement('div');\n row.className = 'zine-share-socials';\n for (const social of SOCIALS) {\n const link = doc.createElement('a');\n link.className = 'zine-share-social';\n link.href = social.href(url, title);\n // Brand names stay as-is; \"Email\" is the one social label that translates.\n const label = social.id === 'email' ? this.#strings.email : social.label;\n link.title = label;\n link.setAttribute('aria-label', this.#strings.shareOn(label));\n if (social.id !== 'email') {\n link.target = '_blank';\n link.rel = 'noopener noreferrer';\n }\n link.appendChild(createIcon(doc, social.icon));\n row.appendChild(link);\n }\n return row;\n }\n\n /** Keep Tab inside the dialog while it is open. */\n #trapFocus(event: KeyboardEvent): void {\n const focusable = [\n ...this.#dialog.querySelectorAll<HTMLElement>('button, a[href], input'),\n ].filter((el) => !el.hasAttribute('disabled'));\n if (focusable.length === 0) return;\n const first = focusable[0]!;\n const last = focusable.at(-1)!;\n const active = this.#doc.activeElement;\n if (event.shiftKey && active === first) {\n last.focus();\n event.preventDefault();\n } else if (!event.shiftKey && active === last) {\n first.focus();\n event.preventDefault();\n }\n }\n\n close(): void {\n this.destroy();\n }\n\n destroy(): void {\n if (this.#timer) clearTimeout(this.#timer);\n this.#timer = null;\n this.#backdrop.removeEventListener('keydown', this.#onKey);\n this.#backdrop.remove();\n // Hand focus back to whatever opened the dialog.\n (this.#lastFocus as HTMLElement | null)?.focus?.();\n }\n}\n","import { ICONS } from './icons';\nimport { defineControl } from './registry';\nimport type { ControlContext } from './types';\n\n\n/** Zoom step per click: two clicks double the scale. */\nconst ZOOM_STEP = Math.SQRT2;\n\nconst isFullscreen = (ctx: ControlContext): boolean => {\n const doc = ctx.zine.container.ownerDocument;\n return doc?.fullscreenElement === ctx.zine.container;\n};\n\n/**\n * Register the stock controls. Called once as the toolbar chunk loads, so the icon set and these\n * definitions stay out of the base bundle.\n *\n * `pageInput` and `search` are not here: both need the toolbar instance itself (one builds an\n * input, the other opens an anchored panel), so the toolbar registers them.\n */\nexport function registerBuiltins(): void {\n defineControl({\n id: 'prev',\n title: (ctx) => ctx.strings.prevPage,\n icon: ICONS.prev,\n isDisabled: (ctx) => !ctx.zine.canFlipPrev(),\n action: (ctx) => ctx.zine.flipPrev(),\n });\n\n defineControl({\n id: 'next',\n title: (ctx) => ctx.strings.nextPage,\n icon: ICONS.next,\n isDisabled: (ctx) => !ctx.zine.canFlipNext(),\n action: (ctx) => ctx.zine.flipNext(),\n });\n\n // The chevrons point the way the book travels, which reverses in RTL — same as the page\n // arrows flanking the book.\n const rtl = (ctx: ControlContext): boolean => ctx.zine.getDirection() === 'rtl';\n\n defineControl({\n id: 'first',\n title: (ctx) => ctx.strings.firstPage,\n icon: (ctx) => (rtl(ctx) ? ICONS.last : ICONS.first),\n isDisabled: (ctx) => !ctx.zine.canFlipPrev(),\n action: (ctx) => {\n ctx.zine.flipTo(0);\n ctx.close();\n },\n });\n\n defineControl({\n id: 'last',\n title: (ctx) => ctx.strings.lastPage,\n icon: (ctx) => (rtl(ctx) ? ICONS.first : ICONS.last),\n isDisabled: (ctx) => !ctx.zine.canFlipNext(),\n action: (ctx) => {\n ctx.zine.flipTo(ctx.zine.getPageCount() - 1);\n ctx.close();\n },\n });\n\n defineControl({\n id: 'zoomIn',\n title: (ctx) => ctx.strings.zoomIn,\n icon: ICONS.zoomIn,\n isDisabled: (ctx) => ctx.zine.getZoom() >= ctx.zine.getMaxZoom() - 1e-6,\n action: (ctx) => ctx.zine.setZoom(ctx.zine.getZoom() * ZOOM_STEP),\n });\n\n defineControl({\n id: 'zoomOut',\n title: (ctx) => ctx.strings.zoomOut,\n icon: ICONS.zoomOut,\n isDisabled: (ctx) => ctx.zine.getZoom() <= 1 + 1e-6,\n action: (ctx) => ctx.zine.setZoom(ctx.zine.getZoom() / ZOOM_STEP),\n });\n\n defineControl({\n id: 'fullscreen',\n title: (ctx) => ctx.strings.fullscreen,\n icon: ICONS.fullscreen,\n isVisible: (ctx) => typeof ctx.zine.container.requestFullscreen === 'function',\n isActive: isFullscreen,\n action: (ctx) => {\n const doc = ctx.zine.container.ownerDocument;\n if (isFullscreen(ctx)) void doc?.exitFullscreen?.();\n else void ctx.zine.container.requestFullscreen?.();\n ctx.close();\n },\n });\n\n defineControl({\n id: 'mute',\n title: (ctx) => (ctx.zine.isSoundMuted() ? ctx.strings.unmute : ctx.strings.mute),\n icon: (ctx) => (ctx.zine.isSoundMuted() ? ICONS.soundOff : ICONS.soundOn),\n // Only meaningful when flip sound is on; hidden otherwise so existing layouts are unchanged.\n isVisible: (ctx) => ctx.zine.isSoundEnabled(),\n isActive: (ctx) => ctx.zine.isSoundMuted(),\n action: (ctx) => ctx.zine.setSoundMuted(!ctx.zine.isSoundMuted()),\n });\n\n defineControl({\n id: 'share',\n title: (ctx) => ctx.strings.share,\n icon: ICONS.share,\n action: (ctx) => {\n ctx.close();\n // The dialog, its brand marks and the QR encoder are a chunk of their own: a book nobody\n // shares never downloads any of it.\n void import('./share')\n .then(({ ShareDialog }) => new ShareDialog(ctx.zine, ctx.zine.container, ctx.colorScheme))\n .catch(() => {\n // No dialog, so fall back to the plain behaviour rather than doing nothing.\n void navigator.clipboard?.writeText(ctx.zine.pageLink()).catch(() => {});\n });\n },\n });\n\n defineControl({\n id: 'download',\n title: (ctx) => ctx.strings.downloadPdf,\n icon: ICONS.download,\n // Hidden for image books and for PDFs opened from a caller-owned pdf.js document, which\n // have no file of their own to save.\n isVisible: (ctx) => ctx.zine.canDownload(),\n // Async: a cross-origin PDF is fetched into a blob first, which takes a beat. Returning the\n // promise lets the toolbar hold the menu open with a spinner until the save starts, then close.\n action: async (ctx) => {\n await ctx.zine.download();\n ctx.close();\n },\n });\n\n defineControl({\n id: 'spread',\n // Says what pressing it will do, not what the book is doing now, and in the reader's terms —\n // \"spread mode\" is the library's word for it, not theirs.\n title: (ctx) => (ctx.zine.isSinglePage() ? ctx.strings.showTwoPages : ctx.strings.showOnePage),\n icon: (ctx) => (ctx.zine.isSinglePage() ? ICONS.twoPages : ICONS.onePage),\n // Hidden while a narrow container is forcing one page: pressing it could not honour two.\n isVisible: (ctx) => !ctx.zine.isResponsiveSingle(),\n action: (ctx) => {\n ctx.zine.toggleSpreadMode();\n ctx.close();\n },\n });\n\n defineControl({\n id: 'print',\n title: (ctx) => ctx.strings.print,\n icon: ICONS.print,\n // Same reach as download: there has to be an original document to print.\n isVisible: (ctx) => ctx.zine.canPrint(),\n // Async like download: the print frame needs a same-origin blob for a cross-origin file, so\n // hold the menu open with a spinner until the frame is ready, then close.\n action: async (ctx) => {\n await ctx.zine.print();\n ctx.close();\n },\n });\n\n defineControl({\n id: 'menu',\n title: (ctx) => ctx.strings.more,\n icon: ICONS.menu,\n // Share and fullscreen sit on the bar itself by default, so the overflow holds what is left.\n // `mute` only appears here when sound is enabled (its isVisible), so it costs nothing otherwise.\n children: ['first', 'last', 'spread', 'thumbnails', 'outline', 'mute', 'print', 'download'],\n });\n}\n","import { createIcon } from './icons';\nimport { resolveControl } from './registry';\nimport type { ControlContext, ControlDef, ControlItem } from './types';\n\n/** Shared empty set, so a menu built without a hide-list allocates nothing. */\nconst NONE_HIDDEN: ReadonlySet<string> = new Set();\n\n/** The id a layout item resolves to, or null for a separator (which has none). */\nfunction itemId(item: ControlItem): string | null {\n if (item === '|') return null;\n return typeof item === 'string' ? item : item.id;\n}\n\n/**\n * Drop every control whose id is in `hidden`, the one filter behind the top-level `hideControls`\n * option. Applied at each build point so a named control disappears from the toolbar, its overflow\n * menu and the right-click menu at once. Separators are left in place; menus drop them anyway and\n * the toolbar tidies its own runs after filtering.\n */\nexport function filterHidden(\n items: readonly ControlItem[],\n hidden: ReadonlySet<string>,\n): readonly ControlItem[] {\n if (!hidden.size) return items;\n return items.filter((item) => {\n const id = itemId(item);\n return id === null || !hidden.has(id);\n });\n}\n\n/**\n * The shared menu machinery.\n *\n * A `.zine-controls-menu` is built the same way in two places: the toolbar's overflow (the ⋮\n * button) and the right-click context menu. Rather than let those drift, both build their items,\n * read their state, and run their actions through the helpers here, so a menu looks and behaves\n * identically wherever it is anchored. Placement and dismissal stay with each owner, because a\n * popover hanging off a toolbar button and one opening at the cursor differ only there.\n */\n\n/** A control's label, which may depend on the current state. */\nexport function titleOf(def: ControlDef, ctx: ControlContext): string {\n return typeof def.title === 'function' ? def.title(ctx) : def.title;\n}\n\n/** A control's glyph, which may likewise depend on state (an RTL mirror, say). */\nexport function iconOf(def: ControlDef, ctx: ControlContext): string | undefined {\n return typeof def.icon === 'function' ? def.icon(ctx) : def.icon;\n}\n\n/** The element itself if it can be disabled, else any inputs/buttons it wraps. */\nexport function formFields(el: HTMLElement): (HTMLButtonElement | HTMLInputElement)[] {\n if (el instanceof HTMLButtonElement || el instanceof HTMLInputElement) return [el];\n return [...el.querySelectorAll<HTMLButtonElement | HTMLInputElement>('button, input')];\n}\n\n/** Toggle the pending-action state on a button: a trailing spinner, disabled while it spins. */\nexport function setBusy(btn: HTMLButtonElement, busy: boolean): void {\n btn.classList.toggle('zine-controls-busy', busy);\n btn.setAttribute('aria-busy', String(busy));\n btn.disabled = busy;\n}\n\n/**\n * Run a control's action, holding the button busy while a slow one (a cross-origin file fetched to\n * save or print) settles. `onAfter` re-reads state once the action has run, and again when a\n * pending one finishes. A disabled or already-busy button does nothing.\n */\nexport function activateControl(\n def: ControlDef,\n trigger: HTMLButtonElement,\n ctx: ControlContext,\n onAfter: () => void,\n): void {\n if (def.isDisabled?.(ctx)) return;\n if (trigger.classList.contains('zine-controls-busy')) return;\n const result = def.action?.(ctx);\n if (result && typeof result.then === 'function') {\n setBusy(trigger, true);\n void result.finally(() => {\n setBusy(trigger, false);\n onAfter();\n });\n }\n onAfter();\n}\n\n/**\n * A control button. Bar buttons are icon-only with the title as tooltip; menu items and any\n * control without an icon also carry a visible label. `onActivate` is left to the caller so the\n * toolbar can route a press through its submenu logic while a menu item just acts.\n */\nexport function buildControlButton(\n doc: Document,\n def: ControlDef,\n className: string,\n ctx: ControlContext,\n onActivate: (def: ControlDef, btn: HTMLButtonElement) => void,\n withLabel = false,\n): HTMLButtonElement {\n const btn = doc.createElement('button');\n const title = titleOf(def, ctx);\n const icon = iconOf(def, ctx);\n btn.type = 'button';\n btn.className = className;\n btn.title = title;\n btn.setAttribute('aria-label', title);\n if (icon) btn.appendChild(createIcon(doc, icon));\n if (withLabel || !icon) {\n const label = doc.createElement('span');\n label.className = 'zine-controls-label';\n label.textContent = title;\n btn.appendChild(label);\n }\n btn.addEventListener('click', () => onActivate(def, btn));\n return btn;\n}\n\n/**\n * Re-read one control's state after the book changes and reflect it on its element.\n *\n * Applies to custom `render` widgets as well as plain buttons: the predicates live on `ControlDef`,\n * so a widget may be any element and each piece of state is applied however that element supports\n * it. `visible` overrides the `isVisible` predicate for a control whose visibility is decided\n * elsewhere (a submenu shown only when it has visible children).\n */\nexport function applyControlItemState(\n el: HTMLElement,\n def: ControlDef,\n ctx: ControlContext,\n doc: Document,\n visible?: boolean,\n): void {\n const applies = visible ?? def.isVisible?.(ctx);\n if (applies !== undefined) el.style.display = applies ? '' : 'none';\n\n // A button running a slow action stays disabled until it settles, whatever its predicate says.\n const busy = el.classList.contains('zine-controls-busy');\n const disabled = (def.isDisabled?.(ctx) ?? false) || busy;\n const fields = formFields(el);\n // A wrapper element cannot be disabled, so mark it and disable the fields inside.\n for (const field of fields) field.disabled = disabled;\n if (!fields.length) el.setAttribute('aria-disabled', String(disabled));\n el.classList.toggle('zine-controls-off', disabled);\n\n if (def.isActive) el.setAttribute('aria-pressed', String(def.isActive(ctx)));\n if (typeof def.title === 'function') {\n const title = def.title(ctx);\n el.title = title;\n el.setAttribute('aria-label', title);\n const label = el.querySelector('.zine-controls-label');\n if (label) label.textContent = title;\n }\n if (typeof def.icon === 'function') {\n el.querySelector('svg')?.replaceWith(createIcon(doc, def.icon(ctx)));\n }\n}\n\n/**\n * A `.zine-controls-menu`: a column of control items with `role=menu`. Owns the items it builds so\n * it can re-read their state and clean up after itself. The owner supplies the context (whose\n * `close()` dismisses this menu) and drives when to build, refresh, and destroy; where the menu is\n * placed and how it is dismissed are the owner's business.\n */\nexport class ControlMenu {\n readonly el: HTMLElement;\n #doc: Document;\n #context: () => ControlContext;\n #onAfterAction: () => void;\n #hidden: ReadonlySet<string>;\n /** Every item on screen, buttons and `render` widgets alike, so {@link refresh} can reach them. */\n #items: { el: HTMLElement; def: ControlDef }[] = [];\n\n constructor(\n doc: Document,\n context: () => ControlContext,\n onAfterAction: () => void,\n hidden: ReadonlySet<string> = NONE_HIDDEN,\n ) {\n this.#doc = doc;\n this.#context = context;\n this.#onAfterAction = onAfterAction;\n this.#hidden = hidden;\n this.el = doc.createElement('div');\n this.el.className = 'zine-controls-menu';\n this.el.setAttribute('role', 'menu');\n }\n\n /** Fill the menu from a list of controls, dropping separators and anything not applicable. */\n build(children: readonly ControlItem[]): void {\n this.el.replaceChildren();\n this.#items = [];\n const ctx = this.#context();\n for (const child of filterHidden(children, this.#hidden)) {\n if (child === '|') continue;\n const def = resolveControl(child);\n if (def.isVisible && !def.isVisible(ctx)) continue;\n let item: HTMLElement;\n if (def.render) {\n item = def.render(this.#context());\n } else {\n item = buildControlButton(\n this.#doc,\n def,\n 'zine-controls-btn',\n ctx,\n (d, btn) => activateControl(d, btn, this.#context(), this.#onAfterAction),\n true,\n );\n }\n item.setAttribute('role', 'menuitem');\n this.el.appendChild(item);\n this.#items.push({ el: item, def });\n }\n }\n\n /** Re-read every item's state. */\n refresh(): void {\n const ctx = this.#context();\n for (const { el, def } of this.#items) applyControlItemState(el, def, ctx, this.#doc);\n }\n\n /** Whether the menu (or something inside it) contains `node`. */\n contains(node: Node | null): boolean {\n return node !== null && this.el.contains(node);\n }\n\n focusFirst(): void {\n this.el.querySelector('button')?.focus();\n }\n\n destroy(): void {\n this.el.remove();\n this.#items = [];\n }\n}\n\n/** A submenu is only worth showing if something inside it is. Keeps a menu trigger from opening\n * onto nothing when every entry has hidden itself. */\nexport function hasVisibleChildren(\n def: ControlDef,\n ctx: ControlContext,\n hidden: ReadonlySet<string> = NONE_HIDDEN,\n): boolean {\n if (def.isVisible && !def.isVisible(ctx)) return false;\n return filterHidden(def.children ?? [], hidden).some((child) => {\n if (child === '|') return false;\n const childDef = resolveControl(child);\n return childDef.isVisible ? childDef.isVisible(ctx) : true;\n });\n}\n","import { createIcon, ICONS } from './icons';\n\n/**\n * The rail beside the book that the thumbnail and outline lists both sit in.\n *\n * It exists to solve one problem shared by every such panel: the container's measured box is what\n * the renderer sizes the book from and what pointer hit-testing maps into, so a panel must not\n * live inside it. Instead the book (and its docked toolbar, if any) is wrapped in a flex row with\n * the panel as its sibling, and `destroy` unwinds that again.\n *\n * The rail is absolutely positioned inside a holder that stretches to the book's height, so a\n * long list scrolls internally instead of running past the bottom of the book.\n *\n * On a wide screen the rail flanks the book and the book shrinks to make room (see styles.ts). On\n * a narrow one there is no room to flank, so the rail becomes a drawer over the book and a scrim\n * dims the page behind it; tapping the scrim or pressing Escape dismisses it through `onDismiss`.\n */\n/** Shared options a toolbar threads into every panel constructor. */\nexport interface PanelOptions {\n /** Called when the reader dismisses the narrow-screen drawer (scrim tap or Escape). */\n onDismiss?: () => void;\n}\n\n/** The book's painted box inside the container, in container px (see Zine.getPageBox). */\ntype PageBox = { x: number; y: number; width: number; height: number };\n\nexport class Sidebar {\n /** The scrolling list; panels append their rows here. */\n readonly root: HTMLElement;\n #holder: HTMLElement;\n #wrap: HTMLElement | null = null;\n #scrim: HTMLElement | null = null;\n #close: HTMLElement | null = null;\n #onKeydown: ((e: KeyboardEvent) => void) | null = null;\n #doc: Document;\n #pageBox: (() => PageBox | null) | null = null;\n #resizeObserver: ResizeObserver | null = null;\n\n constructor(\n doc: Document,\n container: HTMLElement,\n options: {\n className: string;\n label: string;\n width: number;\n /** Called when the reader dismisses the drawer (scrim tap or Escape). Omit to disable both. */\n onDismiss?: () => void;\n /** Reading direction; the drawer slides in from the right instead of the left when true. */\n rtl?: boolean;\n /** Float over the book's edge instead of flanking it (so the book never resizes). Panels\n * whose width is fixed and modest — the outline and search rails — prefer this; the wider\n * thumbnail rail flanks so both stay fully visible. Only affects wide screens: below the\n * breakpoint every panel is a drawer regardless. */\n overlay?: boolean;\n /** The book's painted box in container px. The rail stands exactly as tall as the page rather\n * than the whole container, which is taller whenever the page is letterboxed (its aspect not\n * matching the container's) or a docked toolbar padded the wrap. Re-read on every resize. */\n pageBox?: () => PageBox | null;\n /** aria-label for the drawer's close button. Defaults to 'Close'. */\n closeLabel?: string;\n },\n ) {\n this.#doc = doc;\n this.#holder = doc.createElement('div');\n this.#holder.className = 'zine-panel-holder';\n this.#holder.style.width = `${options.width}px`;\n this.#pageBox = options.pageBox ?? null;\n\n this.root = doc.createElement('div');\n this.root.className = `zine-panel ${options.className}`;\n this.root.setAttribute('role', 'listbox');\n this.root.setAttribute('aria-label', options.label);\n this.#holder.appendChild(this.root);\n\n // In the overlay fallback the rail sits inside the book, where the book's own gesture\n // handlers would otherwise read a click on a row as a page tap.\n for (const type of ['pointerdown', 'pointerup', 'pointermove', 'click', 'dblclick', 'wheel']) {\n this.root.addEventListener(type, (e) => e.stopPropagation());\n }\n\n this.#place(doc, container, options.rtl ?? false, options.overlay ?? false);\n\n if (options.onDismiss) this.#armDismiss(doc, options.onDismiss, options.closeLabel ?? 'Close');\n\n this.#trackPage(container);\n }\n\n /**\n * Keep the rail sized to the painted page. The page box changes whenever the container resizes\n * (window, flanking shrink) or its aspect-ratio does (spread-mode toggle), and a ResizeObserver\n * on the container fires on all of those, so re-fitting there catches every case with no event\n * wiring. Falls back to the full-height CSS when there is no observer or no box yet (e.g. tests).\n */\n #trackPage(container: HTMLElement): void {\n if (!this.#pageBox) return;\n this.#fitToPage();\n if (typeof ResizeObserver === 'undefined') return;\n this.#resizeObserver = new ResizeObserver(() => this.#fitToPage());\n this.#resizeObserver.observe(container);\n }\n\n #fitToPage(): void {\n const box = this.#pageBox?.();\n if (!box) return;\n // top+height (and bottom:auto so an absolute drawer is not over-constrained back to full height)\n // work whether the holder is a relative flex item (flank) or absolutely placed (overlay/drawer).\n this.#holder.style.top = `${box.y}px`;\n this.#holder.style.bottom = 'auto';\n this.#holder.style.height = `${box.height}px`;\n }\n\n /** Resize the rail, e.g. once a thumbnail list knows whether it needs one column or two. */\n setWidth(px: number): void {\n this.#holder.style.width = `${px}px`;\n }\n\n #place(doc: Document, container: HTMLElement, rtl: boolean, overlay: boolean): void {\n const parent = container.parentNode;\n if (!parent) {\n // Not in a document yet: overlay rather than lose the panel.\n this.#holder.classList.add('zine-panel-overlay');\n container.appendChild(this.#holder);\n return;\n }\n // A docked toolbar already wrapped the book; wrap that whole assembly so the rail sits\n // beside book *and* toolbar rather than between them.\n const existing = container.closest('.zine-controls-wrap');\n const target = (existing ?? container) as HTMLElement;\n const wrap = doc.createElement('div');\n wrap.className = 'zine-panel-wrap';\n if (rtl) wrap.classList.add('zine-panel-wrap-rtl');\n // An overlay panel floats over the book's edge rather than flanking it, so the book keeps its\n // size on a wide screen too — the flex-row shrink is off for this wrap.\n if (overlay) wrap.classList.add('zine-panel-wrap-overlay');\n target.parentNode!.insertBefore(wrap, target);\n wrap.append(this.#holder, target);\n this.#wrap = wrap;\n }\n\n /**\n * Wire every way to dismiss the rail. A close button in the rail's trailing-top corner is the\n * discoverable one, present on every screen — the toolbar button and Escape always toggle too,\n * but neither is obvious, and the flanking thumbnail rail has no scrim to click away on. The\n * scrim (drawer only, under the narrow-screen media query) and the Escape key are the other two.\n *\n * The scrim is a sibling of the holder so it can dim the book area without the flex row reserving\n * a column for it. When there is no wrap (the parentless overlay fallback), there is no book\n * beside it to dim, so the scrim is skipped and only the button and Escape are armed.\n */\n #armDismiss(doc: Document, onDismiss: () => void, closeLabel: string): void {\n const close = doc.createElement('button');\n close.type = 'button';\n close.className = 'zine-panel-close';\n close.setAttribute('aria-label', closeLabel);\n close.appendChild(createIcon(doc, ICONS.close));\n // Floats over the scrolling list (see styles.ts); stop the click reaching the book's tap handler\n // in the overlay fallback, where the rail sits inside the container.\n close.addEventListener('click', (e) => {\n e.stopPropagation();\n onDismiss();\n });\n this.#holder.appendChild(close);\n this.#close = close;\n\n if (this.#wrap) {\n const scrim = doc.createElement('div');\n scrim.className = 'zine-panel-scrim';\n // Before the holder so the drawer paints on top of the dim; z-index in CSS makes it explicit.\n this.#wrap.insertBefore(scrim, this.#holder);\n for (const type of ['pointerdown', 'click']) {\n scrim.addEventListener(type, (e) => {\n e.stopPropagation();\n onDismiss();\n });\n }\n this.#scrim = scrim;\n }\n this.#onKeydown = (e: KeyboardEvent): void => {\n if (e.key === 'Escape') onDismiss();\n };\n doc.addEventListener('keydown', this.#onKeydown);\n }\n\n destroy(): void {\n this.#resizeObserver?.disconnect();\n this.#resizeObserver = null;\n if (this.#onKeydown) {\n this.#doc.removeEventListener('keydown', this.#onKeydown);\n this.#onKeydown = null;\n }\n this.#scrim?.remove();\n this.#scrim = null;\n this.#close = null; // lives inside #holder, removed with it below\n this.#holder.remove();\n const wrap = this.#wrap;\n if (wrap?.parentNode) {\n // The rail is gone, so what is left is the book; put it back where the wrapper was.\n while (wrap.firstChild) wrap.parentNode.insertBefore(wrap.firstChild, wrap);\n wrap.remove();\n }\n this.#wrap = null;\n }\n}\n","import { Sidebar, type PanelOptions } from './sidebar';\nimport type { Zine } from '../zine';\n\n/** Width of one page thumbnail, in px. A paired rail fits two of these side by side. */\nconst THUMB_WIDTH = 78;\n/** Padding inside the rail, per side. */\nconst RAIL_PAD = 6;\n/** Gap between the two pages of a spread. */\nconst CELL_GAP = 2;\n\n/**\n * A rail of page thumbnails beside the book.\n *\n * Rows mirror the book's own spread model, so a `double` book shows two pages per row and a\n * `single` book one — including the responsive fallback that forces single-page on a narrow\n * container. A book that mixes the two (`cover`, `book`) keeps two columns so pairs line up, and\n * centres its lone first/last page between them. Clicking a row turns to that spread.\n *\n * Pages are drawn only as they scroll into view: a long PDF would otherwise decode every page at\n * once the moment the rail opened.\n */\nexport class Thumbnails {\n /** The panel's themed root, so the toolbar can stamp its colour scheme. */\n readonly root: HTMLElement;\n #zine: Zine;\n #doc: Document;\n #bar: Sidebar;\n #rows: { el: HTMLElement; spread: number; pages: number[] }[] = [];\n #observer: IntersectionObserver | null = null;\n #unsubscribe: (() => void)[] = [];\n\n constructor(zine: Zine, container: HTMLElement, options: PanelOptions = {}) {\n this.#zine = zine;\n const doc = container.ownerDocument!;\n this.#doc = doc;\n this.#bar = new Sidebar(doc, container, {\n className: 'zine-thumbs',\n label: zine.strings.thumbnailsLabel,\n closeLabel: zine.strings.close,\n width: THUMB_WIDTH + RAIL_PAD * 2,\n onDismiss: options.onDismiss,\n rtl: zine.getDirection() === 'rtl',\n pageBox: () => zine.getPageBox(),\n });\n this.root = this.#bar.root;\n\n this.#build();\n\n const onPage = (): void => this.#markCurrent();\n this.#unsubscribe.push(zine.on('pageChanged', onPage));\n this.#markCurrent();\n }\n\n #build(): void {\n const spreads = this.#zine.getSpreads();\n const rtl = this.#zine.getDirection() === 'rtl';\n // Two columns only if the book actually pairs pages somewhere. A wholly single-page book\n // gets a one-page-wide rail rather than a column of half-empty rows.\n const paired = spreads.some((s) => s.left !== null && s.right !== null);\n this.#bar.root.classList.toggle('zine-thumbs-solo', !paired);\n if (paired) this.#bar.setWidth(THUMB_WIDTH * 2 + CELL_GAP + RAIL_PAD * 2);\n\n for (const [index, spread] of spreads.entries()) {\n const row = this.#doc.createElement('button');\n row.type = 'button';\n row.className = 'zine-thumbs-row';\n row.setAttribute('role', 'option');\n if (rtl) row.style.flexDirection = 'row-reverse';\n\n const pages = [spread.left, spread.right].filter((p): p is number => p !== null);\n // A lone page in a book that pairs elsewhere keeps a single page's width and sits in the\n // middle, rather than stretching across both columns or hugging one side.\n if (paired && pages.length === 1) row.classList.add('zine-thumbs-row-lone');\n\n for (const page of pages) {\n const cell = this.#doc.createElement('span');\n cell.className = 'zine-thumbs-cell';\n cell.dataset.page = String(page);\n row.appendChild(cell);\n }\n\n const labels = [...pages].sort((a, b) => a - b).map((p) => p + 1);\n row.setAttribute('aria-label', this.#zine.strings.thumbnailAria(labels.map(String)));\n\n const caption = this.#doc.createElement('span');\n caption.className = 'zine-thumbs-caption';\n caption.textContent = labels.join('–');\n row.appendChild(caption);\n\n row.addEventListener('click', () => {\n if (pages.length > 0) this.#zine.flipTo(Math.min(...pages));\n });\n this.#bar.root.appendChild(row);\n this.#rows.push({ el: row, spread: index, pages });\n }\n this.#observeRows();\n }\n\n /** Decode a row's pages the first time it comes into view. */\n #observeRows(): void {\n const cells = [...this.#bar.root.querySelectorAll<HTMLElement>('.zine-thumbs-cell[data-page]')];\n if (typeof IntersectionObserver === 'undefined') {\n for (const cell of cells) void this.#paint(cell);\n return;\n }\n this.#observer = new IntersectionObserver(\n (entries, obs) => {\n for (const entry of entries) {\n if (!entry.isIntersecting) continue;\n obs.unobserve(entry.target);\n void this.#paint(entry.target as HTMLElement);\n }\n },\n { root: this.#bar.root, rootMargin: '200px' },\n );\n for (const cell of cells) this.#observer.observe(cell);\n }\n\n async #paint(cell: HTMLElement): Promise<void> {\n const page = Number(cell.dataset.page);\n const content = await this.#zine.getPageImage(page);\n if (!content || !cell.isConnected) return;\n const w = (content as { width: number }).width;\n const h = (content as { height: number }).height;\n if (!w || !h) return;\n // One page's worth of pixels, whatever the rail's column count, so thumbs stay a consistent\n // size between modes. Scaled by dpr so they are not soft on a retina screen.\n const dpr = Math.min(2, globalThis.devicePixelRatio || 1);\n const canvas = this.#doc.createElement('canvas');\n canvas.width = Math.max(1, Math.round(THUMB_WIDTH * dpr));\n canvas.height = Math.max(1, Math.round(((THUMB_WIDTH * h) / w) * dpr));\n canvas.className = 'zine-thumbs-img';\n const ctx = canvas.getContext('2d');\n if (!ctx) return;\n try {\n ctx.drawImage(content as CanvasImageSource, 0, 0, canvas.width, canvas.height);\n } catch {\n return; // an unusable raster is not worth breaking the rail over\n }\n cell.replaceChildren(canvas);\n }\n\n /** Highlight the spread on screen and keep it scrolled into view. */\n #markCurrent(): void {\n // Key off the current page, not getSpreadIndex(): a `pageChanged` fires during the flip's\n // lead, a beat before the landing updates the book's spread index, so reading that index here\n // would highlight the spread we are leaving until the next flip. The page number is already\n // fresh at lead time, so the row that contains it is always the right one.\n const page = this.#zine.getPage();\n for (const { el, pages } of this.#rows) {\n const active = pages.includes(page);\n el.classList.toggle('zine-panel-active', active);\n el.setAttribute('aria-selected', String(active));\n if (active) el.scrollIntoView?.({ block: 'nearest' });\n }\n }\n\n destroy(): void {\n for (const off of this.#unsubscribe) off();\n this.#unsubscribe = [];\n this.#observer?.disconnect();\n this.#observer = null;\n this.#bar.destroy();\n }\n}\n","import { Sidebar, type PanelOptions } from './sidebar';\nimport type { OutlineItem } from '../source/types';\nimport type { Zine } from '../zine';\n\n/** Rail width in px. Wider than the thumbnail rail because it holds headings, not pictures. */\nconst RAIL_WIDTH = 208;\n/** Indent per nesting level, in px. */\nconst INDENT = 12;\n\n/**\n * The document's table of contents beside the book.\n *\n * One row per heading, nested entries indented under their parent. Clicking a row turns to the\n * page it points at; a heading whose destination could not be resolved is still listed, just not\n * clickable. Unlike the thumbnail rail this is a single column — headings are text, and a second\n * column would only make them narrower.\n */\nexport class Outline {\n /** The panel's themed root, so the toolbar can stamp its colour scheme. */\n readonly root: HTMLElement;\n #zine: Zine;\n #doc: Document;\n #bar: Sidebar;\n #rows: { el: HTMLElement; page: number }[] = [];\n #unsubscribe: (() => void)[] = [];\n #onDismiss?: () => void;\n\n constructor(zine: Zine, container: HTMLElement, options: PanelOptions = {}) {\n this.#zine = zine;\n const doc = container.ownerDocument!;\n this.#doc = doc;\n this.#onDismiss = options.onDismiss;\n this.#bar = new Sidebar(doc, container, {\n className: 'zine-outline',\n label: zine.strings.outlineLabel,\n closeLabel: zine.strings.close,\n width: RAIL_WIDTH,\n onDismiss: options.onDismiss,\n rtl: zine.getDirection() === 'rtl',\n overlay: true, // floats over the book's edge on a wide screen rather than shrinking it\n pageBox: () => zine.getPageBox(),\n });\n this.root = this.#bar.root;\n\n this.#note(zine.strings.outlineLoading);\n void this.#build();\n\n this.#unsubscribe.push(zine.on('pageChanged', () => this.#markCurrent()));\n }\n\n #note(text: string): void {\n const note = this.#doc.createElement('div');\n note.className = 'zine-panel-note';\n note.textContent = text;\n this.#bar.root.replaceChildren(note);\n }\n\n async #build(): Promise<void> {\n const items = await this.#zine.getOutline();\n if (!this.#bar.root.isConnected) return; // closed while the outline was loading\n if (items.length === 0) {\n // The toolbar hides the control when there is nothing to list, so this is only reachable\n // through a hand-written layout that names 'outline' regardless.\n this.#note(this.#zine.strings.outlineEmpty);\n return;\n }\n this.#bar.root.replaceChildren();\n this.#addLevel(items, 0);\n this.#markCurrent();\n }\n\n #addLevel(items: OutlineItem[], depth: number): void {\n for (const item of items) {\n const row = this.#doc.createElement('button');\n row.type = 'button';\n row.className = 'zine-outline-row';\n row.setAttribute('role', 'option');\n row.style.paddingLeft = `${8 + depth * INDENT}px`;\n // The entry text is document content; only the empty-title fallback is a translatable label.\n const title = item.title.trim() || this.#zine.strings.untitled;\n row.textContent = title;\n row.title = title;\n if (item.page === null) {\n // Keep the heading visible but inert: it still tells the reader what is in the document.\n row.disabled = true;\n } else {\n const page = item.page;\n row.setAttribute('aria-label', this.#zine.strings.outlineEntryAria(title, page + 1));\n row.addEventListener('click', () => {\n this.#zine.flipTo(page);\n this.#onDismiss?.(); // clicking a heading has served its purpose; get the rail out of the way\n });\n this.#rows.push({ el: row, page });\n }\n this.#bar.root.appendChild(row);\n if (item.children.length > 0) this.#addLevel(item.children, depth + 1);\n }\n }\n\n /**\n * Mark the entry the reader is inside — the last one at or before the current page, rather\n * than an exact match, since a section spans many pages and rarely starts on the one shown.\n */\n #markCurrent(): void {\n const current = this.#zine.getPage();\n let best: HTMLElement | null = null;\n let bestPage = -1;\n for (const { el, page } of this.#rows) {\n el.classList.remove('zine-panel-active');\n el.removeAttribute('aria-selected');\n if (page <= current && page >= bestPage) {\n best = el;\n bestPage = page;\n }\n }\n if (!best) return;\n best.classList.add('zine-panel-active');\n best.setAttribute('aria-selected', 'true');\n best.scrollIntoView?.({ block: 'nearest' });\n }\n\n destroy(): void {\n for (const off of this.#unsubscribe) off();\n this.#unsubscribe = [];\n this.#bar.destroy();\n }\n}\n","import { Sidebar, type PanelOptions } from './sidebar';\nimport type { SearchHit, Zine } from '../zine';\n\n/** Rail width in px. Matches the outline: both hold text, not pictures. */\nconst RAIL_WIDTH = 208;\n/** Wait after the last keystroke before searching, so typing does not scan per character. */\nconst DEBOUNCE_MS = 180;\n/** Shorter queries match too much to be useful. */\nconst MIN_QUERY = 2;\n\n/** The query and its results, so a closed panel can be reopened where the reader left it rather\n * than blank. Held by the toolbar (which outlives the panel) and handed back on reopen. */\nexport interface SearchState {\n query: string;\n results: SearchHit[];\n}\n\n/** Panel options plus the optional state to reopen with. */\nexport interface SearchOptions extends PanelOptions {\n /** Query and results from a previous open, put back so the reader continues where they left off. */\n state?: SearchState;\n}\n\n/**\n * Full-text search beside the book: a query field over its results.\n *\n * It lives in the same rail as the thumbnails and outline rather than a popover, because results\n * are an open-ended list — a floating panel over the book would either cover the page being\n * searched or clip the very results it found.\n */\nexport class Search {\n /** The panel's themed root, so the toolbar can stamp its colour scheme. */\n readonly root: HTMLElement;\n #zine: Zine;\n #doc: Document;\n #bar: Sidebar;\n #input: HTMLInputElement;\n #hits: HTMLElement;\n /** Increments per query so a slow search that resolves late cannot overwrite a newer one. */\n #run = 0;\n #timer: ReturnType<typeof setTimeout> | null = null;\n #onDismiss?: () => void;\n /** The last query and results, kept so the toolbar can restore them on reopen. */\n #results: SearchHit[] = [];\n\n constructor(zine: Zine, container: HTMLElement, options: SearchOptions = {}) {\n this.#zine = zine;\n const doc = container.ownerDocument!;\n this.#doc = doc;\n this.#onDismiss = options.onDismiss;\n this.#bar = new Sidebar(doc, container, {\n className: 'zine-search',\n label: zine.strings.searchResultsLabel,\n closeLabel: zine.strings.close,\n width: RAIL_WIDTH,\n onDismiss: options.onDismiss,\n rtl: zine.getDirection() === 'rtl',\n overlay: true, // floats over the book's edge on a wide screen rather than shrinking it\n pageBox: () => zine.getPageBox(),\n });\n this.root = this.#bar.root;\n\n this.#input = doc.createElement('input');\n this.#input.type = 'search';\n this.#input.className = 'zine-search-input';\n this.#input.placeholder = zine.strings.searchPlaceholder;\n this.#input.setAttribute('aria-label', zine.strings.searchInputLabel);\n this.#input.addEventListener('input', () => {\n if (this.#timer) clearTimeout(this.#timer);\n this.#timer = setTimeout(() => void this.#search(), DEBOUNCE_MS);\n });\n // Enter searches at once rather than waiting out the debounce.\n this.#input.addEventListener('keydown', (e) => {\n if ((e as KeyboardEvent).key !== 'Enter') return;\n if (this.#timer) clearTimeout(this.#timer);\n void this.#search();\n });\n\n this.#hits = doc.createElement('div');\n this.#hits.className = 'zine-search-hits';\n\n this.#bar.root.append(this.#input, this.#hits);\n\n // Reopened where the reader left off: put the query and its results straight back, without\n // re-scanning the document. A later keystroke supersedes them the usual way.\n const initial = options.state;\n if (initial && initial.query) {\n this.#input.value = initial.query;\n this.#run++; // any restored render belongs to this run, not a stale one\n this.#renderResults(initial.query, initial.results);\n }\n\n this.#input.focus();\n }\n\n /** The query and results as they stand, for the toolbar to stash across a close. */\n getState(): SearchState {\n return { query: this.#input.value, results: this.#results };\n }\n\n #note(text: string): void {\n const note = this.#doc.createElement('div');\n note.className = 'zine-panel-note';\n note.textContent = text;\n this.#hits.replaceChildren(note);\n }\n\n async #search(): Promise<void> {\n const query = this.#input.value.trim();\n const mine = ++this.#run;\n if (query.length < MIN_QUERY) {\n this.#results = [];\n this.#hits.replaceChildren();\n return;\n }\n this.#note(this.#zine.strings.searching);\n const results = await this.#zine.search(query);\n if (mine !== this.#run || !this.#bar.root.isConnected) return; // superseded, or closed\n this.#renderResults(query, results);\n }\n\n /** Paint a completed search: a note when empty, one clickable row per hit otherwise. Shared by\n * a live search and the restore of a reopened panel, so both render identically. */\n #renderResults(query: string, results: SearchHit[]): void {\n this.#results = results;\n if (results.length === 0) {\n this.#note(this.#zine.strings.noMatches(query));\n return;\n }\n this.#hits.replaceChildren(\n ...results.map((hit) => {\n const row = this.#doc.createElement('button');\n row.type = 'button';\n row.className = 'zine-search-hit';\n row.setAttribute('role', 'option');\n row.setAttribute('aria-label', this.#zine.strings.searchHitAria(hit.page + 1, hit.excerpt));\n const label = this.#doc.createElement('span');\n label.className = 'zine-search-page';\n label.textContent = this.#zine.strings.searchHitLabel(hit.page + 1);\n const excerpt = this.#doc.createElement('small');\n excerpt.textContent = hit.excerpt;\n row.append(label, excerpt);\n row.addEventListener('click', () => {\n this.#zine.flipTo(hit.page);\n this.#onDismiss?.(); // jump to the hit, then get the rail out of the way\n });\n return row;\n }),\n );\n }\n\n destroy(): void {\n if (this.#timer) clearTimeout(this.#timer);\n this.#timer = null;\n this.#run++; // any search still in flight is now stale\n this.#bar.destroy();\n }\n}\n","import { createIcon, ICONS } from './icons';\nimport { applyColorScheme } from './styles';\nimport type { ControlsColorScheme } from './types';\nimport type { Zine } from '../zine';\n\n/**\n * Big page-turn arrows flanking the book.\n *\n * They sit *beside* the book rather than over it, for the same reason the docked toolbar does:\n * the container's measured box is what the renderer sizes pages from and what pointer\n * hit-testing maps into, so anything inside it would shrink the book and skew every tap. The\n * book (with whatever the toolbar already wrapped around it) goes in a flex row between them.\n *\n * An arrow disables itself at the end of the book rather than disappearing, so the layout does\n * not jump as the reader reaches the covers.\n */\nexport class Arrows {\n #zine: Zine;\n #prev: HTMLButtonElement;\n #next: HTMLButtonElement;\n #wrap: HTMLElement | null = null;\n #device: 'desktop' | 'mobile' | null;\n #unsubscribe: (() => void)[] = [];\n\n constructor(\n zine: Zine,\n container: HTMLElement,\n colorScheme: ControlsColorScheme = 'auto',\n // null: show everywhere. 'desktop'/'mobile': show only on that device; a class on the wrap\n // lets CSS hide the arrows on the other side of the layout breakpoint (see styles.ts).\n device: 'desktop' | 'mobile' | null = null,\n ) {\n this.#zine = zine;\n this.#device = device;\n const doc = container.ownerDocument!;\n\n const rtl = zine.getDirection() === 'rtl';\n // The arrows point where the page goes, so in RTL they swap: \"back\" is to the right.\n this.#prev = this.#button(doc, rtl ? ICONS.next : ICONS.prev, zine.strings.prevPage, () =>\n zine.flipPrev(),\n );\n this.#next = this.#button(doc, rtl ? ICONS.prev : ICONS.next, zine.strings.nextPage, () =>\n zine.flipNext(),\n );\n\n this.#place(doc, container);\n // The wrap holds both arrow buttons; color-scheme inherits, so stamping it themes both. In\n // the parentless fallback there is no wrap, so stamp the buttons directly instead.\n if (this.#wrap) applyColorScheme(this.#wrap, colorScheme);\n else for (const btn of [this.#prev, this.#next]) applyColorScheme(btn, colorScheme);\n\n // pageChanged fires at the animation lead and carries the landed page; flipEnd is the safety\n // net for a landing that never led (reduced motion, a canceled lead). See #refresh for why the\n // page matters.\n this.#unsubscribe.push(zine.on('pageChanged', ({ page }) => this.#refresh(page)));\n this.#unsubscribe.push(zine.on('flipEnd', () => this.#refresh()));\n this.#refresh();\n }\n\n #button(\n doc: Document,\n icon: string,\n label: string,\n onClick: () => void,\n ): HTMLButtonElement {\n const btn = doc.createElement('button');\n btn.type = 'button';\n btn.className = 'zine-arrow';\n btn.title = label;\n btn.setAttribute('aria-label', label);\n btn.appendChild(createIcon(doc, icon));\n btn.addEventListener('click', onClick);\n // The book's gesture handlers sit on the container; an arrow press is not a page tap.\n for (const type of ['pointerdown', 'pointerup', 'click']) {\n btn.addEventListener(type, (e) => e.stopPropagation());\n }\n return btn;\n }\n\n #place(doc: Document, container: HTMLElement): void {\n // Wrap the outermost thing the library has already built around the book — a docked toolbar\n // or a side panel — so the arrows flank the whole assembly, not just the pages.\n const outer =\n container.closest('.zine-panel-wrap') ??\n container.closest('.zine-controls-wrap') ??\n container;\n const parent = outer.parentNode;\n if (!parent) return; // not in a document; nothing sensible to flank\n const wrap = doc.createElement('div');\n wrap.className = 'zine-arrows-wrap';\n if (this.#device) wrap.classList.add(`zine-arrows-${this.#device}`);\n parent.insertBefore(wrap, outer);\n wrap.append(this.#prev, outer, this.#next);\n this.#wrap = wrap;\n }\n\n /**\n * Hide an arrow that has nowhere to go — no back arrow on the first page.\n *\n * Hidden rather than removed so its slot stays reserved: dropping it from the flow would slide\n * the book sideways every time the reader reached a cover. `disabled` comes along for the ride\n * so the button is inert even if a consumer's own CSS makes it visible again.\n */\n #refresh(page?: number): void {\n const { prev, next } = this.#edges(page);\n for (const [btn, canTurn] of [\n [this.#prev, prev],\n [this.#next, next],\n ] as const) {\n btn.disabled = !canTurn;\n btn.classList.toggle('zine-arrow-hidden', !canTurn);\n }\n }\n\n /**\n * Which arrows have somewhere to go.\n *\n * `canFlipPrev`/`canFlipNext` read the committed spread index, which only updates once a flip\n * lands — so refreshing off them at the animation lead (when `pageChanged` fires, a few ms\n * before the commit) leaves the arrow a step behind until the fold fully settles. When the\n * event hands us the page that just landed, resolve its spread from the public spread model so\n * the arrow corrects the instant the fold reads as landed. Fall back to the committed edge state\n * when there is no page in hand (initial paint, `flipEnd`) or the page is not in any spread.\n */\n #edges(page?: number): { prev: boolean; next: boolean } {\n if (page !== undefined) {\n const spreads = this.#zine.getSpreads();\n const index = spreads.findIndex((s) => s.left === page || s.right === page);\n if (index >= 0) return { prev: index > 0, next: index + 1 < spreads.length };\n }\n return { prev: this.#zine.canFlipPrev(), next: this.#zine.canFlipNext() };\n }\n\n destroy(): void {\n for (const off of this.#unsubscribe) off();\n this.#unsubscribe = [];\n this.#prev.remove();\n this.#next.remove();\n const wrap = this.#wrap;\n if (wrap?.parentNode) {\n // The arrows are gone, so what is left is the book; put it back where the wrapper was.\n while (wrap.firstChild) wrap.parentNode.insertBefore(wrap.firstChild, wrap);\n wrap.remove();\n }\n this.#wrap = null;\n }\n}\n","import { ICONS } from './icons';\nimport { DEFAULT_ITEMS, defineControl, resolveControl } from './registry';\nimport { registerBuiltins } from './builtins';\nimport {\n activateControl,\n applyControlItemState,\n buildControlButton,\n ControlMenu,\n filterHidden,\n hasVisibleChildren,\n} from './menu';\nimport { applyColorScheme, ensureStyles } from './styles';\nimport { Thumbnails } from './thumbnails';\nimport { Outline } from './outline';\nimport { Search, type SearchState } from './search';\nimport { Arrows } from './arrows';\n\n/** The side panels, all of which share the one rail beside the book. */\nconst PANELS = { thumbnails: Thumbnails, outline: Outline, search: Search };\ntype PanelKind = keyof typeof PANELS;\n/** What every panel has in common: build itself on construction, and clean up after itself. */\ntype Panel = InstanceType<(typeof PANELS)[PanelKind]>;\nimport type {\n ControlContext,\n ControlDef,\n ControlItem,\n ControlsColorScheme,\n ControlsOptions,\n ControlsPosition,\n} from './types';\nimport type { Zine } from '../zine';\n\n/**\n * Events the container listens to for flipping, zooming and dragging. The gesture layer binds\n * these on the container and never checks `event.target`, so anything the toolbar handles has to\n * be stopped here or a button press would also register as a page tap.\n */\nconst SWALLOWED = ['pointerdown', 'pointerup', 'pointermove', 'click', 'dblclick', 'wheel', 'keydown'];\n\ninterface Popover {\n menu: ControlMenu;\n trigger: HTMLElement;\n}\n\n/**\n * Tidy the separator runs a hide-list can leave behind on the bar: drop leading and trailing\n * separators, and collapse consecutive ones to a single divider. The overflow menu drops all\n * separators anyway, so this only matters for the bar itself.\n */\nfunction collapseSeparators(items: readonly ControlItem[]): ControlItem[] {\n const out: ControlItem[] = [];\n for (const item of items) {\n if (item === '|' && (out.length === 0 || out[out.length - 1] === '|')) continue;\n out.push(item);\n }\n if (out[out.length - 1] === '|') out.pop();\n return out;\n}\n\nexport class Toolbar {\n #zine: Zine;\n #doc: Document;\n #root: HTMLElement;\n #bar: HTMLElement;\n /** Every control on screen, buttons and `render` widgets alike, so #refresh can reach them. */\n #buttons: { el: HTMLElement; def: ControlDef }[] = [];\n #popover: Popover | null = null;\n #unsubscribe: (() => void)[] = [];\n #pageInput: HTMLInputElement | null = null;\n /** The flex wrapper docked mode inserts around the container; unwound on destroy. */\n #wrap: HTMLElement | null = null;\n #container: HTMLElement;\n /** The side panel currently in the rail. Only one at a time — they share the space. */\n #panel: Panel | null = null;\n #panelKind: PanelKind | null = null;\n /** The last search's query and results, so reopening the search rail restores them rather than\n * starting blank. Panels are rebuilt on every open, so this outlives the panel that holds it. */\n #searchState: SearchState | null = null;\n /** Whether the document turned out to have any outline entries. Null until known: the answer\n * is async, and the outline control stays hidden rather than flash in and out. */\n #hasOutline: boolean | null = null;\n #arrows: Arrows | null = null;\n /** Which palette the built-in colours resolve to. Passed on to the arrows, panels and share\n * dialog, each of which mounts its own root outside this one and so must be stamped too. */\n #colorScheme: ControlsColorScheme;\n /** The edge the bar sits on, kept so a popover can open away from it. */\n #position: ControlsPosition;\n /** Control ids hidden everywhere by the top-level `hideControls` option: off the bar, out of the\n * overflow menu. Empty when nothing is hidden. */\n #hidden: ReadonlySet<string>;\n\n constructor(\n zine: Zine,\n container: HTMLElement,\n options: ControlsOptions,\n hidden: ReadonlySet<string> = new Set(),\n ) {\n this.#zine = zine;\n this.#container = container;\n this.#hidden = hidden;\n const doc = container.ownerDocument!;\n this.#doc = doc;\n ensureStyles(doc);\n\n this.#colorScheme = options.colorScheme ?? 'auto';\n const position = options.position ?? 'bottom';\n this.#position = position;\n const docked = options.docked ?? true;\n this.#root = doc.createElement('div');\n this.#root.className = [\n 'zine-controls',\n `zine-controls-${position}`,\n docked ? 'zine-controls-docked' : 'zine-controls-floating',\n options.className,\n ]\n .filter(Boolean)\n .join(' ');\n this.#root.setAttribute('role', 'toolbar');\n this.#root.setAttribute('aria-label', this.#zine.strings.controlsLabel);\n applyColorScheme(this.#root, this.#colorScheme);\n\n this.#bar = doc.createElement('div');\n this.#bar.className = 'zine-controls-bar';\n this.#root.appendChild(this.#bar);\n\n // Toolbar keys are handled first, then every listed event is stopped so none of it reaches\n // the book's gesture, zoom or keyboard handlers on the container.\n this.#root.addEventListener('keydown', (e) => this.#onKeyDown(e as KeyboardEvent));\n for (const event of SWALLOWED) {\n this.#root.addEventListener(event, (e) => this.#isolate(e));\n }\n this.#place(container, position, docked);\n\n const refresh = (): void => this.#refresh();\n this.#unsubscribe.push(zine.on('pageChanged', refresh));\n this.#unsubscribe.push(zine.on('zoomChanged', refresh));\n this.#unsubscribe.push(zine.on('flipEnd', refresh));\n const onDocPointer = (e: Event): void => {\n if (!this.#popover) return;\n const target = e.target as Node;\n // Leave a press on the trigger to its own click handler, which toggles the menu shut —\n // closing here first would let that click reopen it, so it would never close.\n if (this.#popover.menu.contains(target) || this.#popover.trigger.contains(target)) return;\n this.#closePopover();\n };\n doc.addEventListener('pointerdown', onDocPointer, true);\n this.#unsubscribe.push(() => doc.removeEventListener('pointerdown', onDocPointer, true));\n }\n\n /**\n * Put the toolbar in the page.\n *\n * Floating is simple: a child of the container, absolutely positioned over the book.\n *\n * Docked has to sit *outside* the container, because the container's measured box is what the\n * renderer sizes the book from and what pointer hit-testing maps into — a bar inside it would\n * shrink the book and skew every tap. So the container is wrapped in a flex column (or row),\n * with the toolbar as its sibling. The wrapper inherits the container's own layout box so the\n * arrangement the consumer's CSS set up is preserved.\n */\n #place(container: HTMLElement, position: ControlsPosition, docked: boolean): void {\n if (!docked) {\n container.appendChild(this.#root);\n return;\n }\n const parent = container.parentNode;\n if (!parent) {\n // Not in a document yet: fall back to overlaying rather than losing the toolbar.\n this.#root.classList.replace('zine-controls-docked', 'zine-controls-floating');\n container.appendChild(this.#root);\n return;\n }\n const wrap = this.#doc.createElement('div');\n wrap.className = `zine-controls-wrap zine-controls-wrap-${position}`;\n parent.insertBefore(wrap, container);\n wrap.appendChild(container);\n // 'top'/'left' put the bar first; flex-direction (set in CSS) handles the axis.\n if (position === 'top' || position === 'left') wrap.insertBefore(this.#root, container);\n else wrap.appendChild(this.#root);\n this.#wrap = wrap;\n }\n\n /** Build the layout. Separate from construction so instance-bound widgets (the page field, the\n * search panel) can be registered against this toolbar first. */\n mount(options: ControlsOptions): void {\n this.#build(options.items ?? DEFAULT_ITEMS);\n // After #place, so the arrows can wrap whatever it built around the book. A string value picks\n // the device the arrows show on; only an explicit false suppresses them entirely.\n const arrows = options.arrows ?? true;\n if (arrows !== false)\n this.#arrows = new Arrows(\n this.#zine,\n this.#container,\n this.#colorScheme,\n arrows === true ? null : arrows,\n );\n this.#refresh();\n this.#probeOutline();\n }\n\n destroy(): void {\n for (const off of this.#unsubscribe) off();\n this.#unsubscribe = [];\n this.#closePopover();\n this.#panel?.destroy();\n this.#panel = null;\n this.#panelKind = null;\n this.#root.remove();\n // Unwrap from the inside out: the arrows enclose the toolbar's wrapper, so removing theirs\n // first would leave this one orphaned outside the document.\n const wrap = this.#wrap;\n if (wrap?.parentNode) {\n wrap.parentNode.insertBefore(wrap.firstChild!, wrap);\n wrap.remove();\n }\n this.#wrap = null;\n this.#arrows?.destroy();\n this.#arrows = null;\n }\n\n /** Keep toolbar interaction from reaching the book's own gesture/zoom/keyboard handlers. */\n #isolate(event: Event): void {\n event.stopPropagation();\n }\n\n #context(close: () => void = () => this.#closePopover()): ControlContext {\n return { zine: this.#zine, close, colorScheme: this.#colorScheme, strings: this.#zine.strings };\n }\n\n #build(items: readonly ControlItem[]): void {\n for (const item of collapseSeparators(filterHidden(items, this.#hidden))) {\n if (item === '|') {\n const sep = this.#doc.createElement('div');\n sep.className = 'zine-controls-sep';\n this.#bar.appendChild(sep);\n continue;\n }\n const def = resolveControl(item);\n let el: HTMLElement;\n if (def.render) {\n el = def.render(this.#context());\n } else {\n el = buildControlButton(this.#doc, def, 'zine-controls-btn', this.#context(), (d, btn) =>\n this.#activate(d, btn),\n );\n }\n // Registered so its predicates are re-read on every change, whichever way it draws itself.\n this.#buttons.push({ el, def });\n this.#bar.appendChild(el);\n }\n }\n\n #activate(def: ControlDef, trigger: HTMLButtonElement): void {\n if (def.isDisabled?.(this.#context())) return;\n // A submenu toggles its popover; a second press on the same trigger closes it.\n if (def.children) {\n if (this.#popover?.trigger === trigger) this.#closePopover();\n else this.#openMenu(def, trigger);\n return;\n }\n activateControl(def, trigger, this.#context(), () => this.#refresh());\n }\n\n #openMenu(def: ControlDef, trigger: HTMLButtonElement): void {\n this.#closePopover();\n const menu = new ControlMenu(this.#doc, () => this.#context(), () => this.#refresh(), this.#hidden);\n menu.build(def.children ?? []);\n this.#root.appendChild(menu.el);\n this.#popover = { menu, trigger };\n trigger.setAttribute('aria-expanded', 'true');\n this.#placePopover(menu.el, trigger);\n // The entries were only just built, so nothing has evaluated their state yet: without this\n // a menu opens with every entry enabled until the next page turn happens to refresh it.\n menu.refresh();\n menu.focusFirst();\n }\n\n /** Anchor a popover to its trigger, flipped to stay inside the book. */\n #placePopover(el: HTMLElement, trigger: HTMLElement): void {\n const root = this.#root.getBoundingClientRect();\n const anchor = trigger.getBoundingClientRect();\n const box = el.getBoundingClientRect();\n if (!root.width || !box.width) return; // not laid out (e.g. jsdom): leave to CSS\n let left = anchor.left - root.left + anchor.width / 2 - box.width / 2;\n left = Math.max(4, Math.min(left, root.width - box.width - 4));\n el.style.left = `${left}px`;\n const below = anchor.bottom - root.top + 6;\n const above = anchor.top - root.top - box.height - 6;\n // A bottom bar opens its menu upward (above the button), so it never spills off the bottom\n // edge — the book above always has the room. Every other edge keeps the pick-whichever-fits\n // behaviour, preferring upward only when there is space for it.\n el.style.top = `${this.#position === 'bottom' ? above : above >= 0 ? above : below}px`;\n }\n\n #closePopover(): void {\n if (!this.#popover) return;\n const { menu, trigger } = this.#popover;\n this.#popover = null;\n trigger.removeAttribute('aria-expanded');\n const hadFocus = this.#doc.activeElement !== null && menu.contains(this.#doc.activeElement);\n // The menu owns its own items, so tearing it down keeps #refresh off detached nodes.\n menu.destroy();\n if (hadFocus) trigger.focus();\n }\n\n /** Arrow keys move between controls; Escape dismisses an open popover. */\n #onKeyDown(event: KeyboardEvent): void {\n if (event.key === 'Escape' && this.#popover) {\n const { trigger } = this.#popover;\n this.#closePopover();\n trigger.focus();\n event.preventDefault();\n return;\n }\n const focusables = [...this.#root.querySelectorAll<HTMLElement>('button, input')];\n const at = focusables.indexOf(this.#doc.activeElement as HTMLElement);\n if (at < 0) return;\n // Let the page-number field use its own arrows for text editing.\n if (this.#doc.activeElement instanceof HTMLInputElement) return;\n const step = event.key === 'ArrowRight' || event.key === 'ArrowDown' ? 1 : event.key === 'ArrowLeft' || event.key === 'ArrowUp' ? -1 : 0;\n if (step === 0) return;\n const next = focusables[(at + step + focusables.length) % focusables.length];\n next?.focus();\n event.preventDefault();\n }\n\n /**\n * Re-read every control's state after the book changes.\n *\n * Applies to custom widgets built with `render` as well as to plain buttons: the predicates\n * are on `ControlDef`, so honouring them only for button-shaped controls would make the same\n * field mean different things depending on how a control chose to draw itself. A widget may be\n * any element, so each piece of state is applied in whatever way that element supports.\n */\n #refresh(): void {\n const ctx = this.#context();\n for (const { el, def } of this.#buttons) {\n // A submenu trigger shows only while something inside it does; every other control follows\n // its own isVisible. Undefined leaves the element's display alone.\n const visible = def.children ? hasVisibleChildren(def, ctx, this.#hidden) : def.isVisible?.(ctx);\n applyControlItemState(el, def, ctx, this.#doc, visible);\n }\n // The open overflow menu owns its items, so refresh them through it.\n this.#popover?.menu.refresh();\n if (this.#pageInput && this.#doc.activeElement !== this.#pageInput) {\n this.#pageInput.value = String(this.#zine.getPage() + 1);\n }\n }\n\n /** Registered lazily by {@link registerWidgets} so the widget can reach this instance. */\n makePageInput(): HTMLElement {\n const wrap = this.#doc.createElement('div');\n wrap.className = 'zine-controls-page';\n const input = this.#doc.createElement('input');\n input.type = 'number';\n input.min = '1';\n input.max = String(this.#zine.getPageCount());\n input.value = String(this.#zine.getPage() + 1);\n input.setAttribute('aria-label', this.#zine.strings.pageNumberLabel);\n const total = this.#doc.createElement('span');\n total.textContent = this.#zine.strings.pageTotal(this.#zine.getPageCount());\n\n // Enter commits and then blurs, and the blur commits again; a click-away blur commits on its\n // own. Guard so only the first call per editing session navigates: flipTo animates, so a\n // second commit right after would read the still-old getPage() and turn the book back to it —\n // the page that is (still) in the URL hash. Re-armed on focus for the next edit.\n let committed = false;\n const commit = (): void => {\n if (committed) return;\n committed = true;\n const n = Number(input.value);\n const last = Math.max(0, this.#zine.getPageCount() - 1);\n const target = Number.isFinite(n)\n ? Math.min(Math.max(Math.round(n) - 1, 0), last)\n : this.#zine.getPage();\n if (target !== this.#zine.getPage()) {\n this.#zine.flipTo(target); // #refresh (pageChanged/flipEnd) resyncs the field once it lands\n } else {\n input.value = String(this.#zine.getPage() + 1); // no navigation: restore the display now\n }\n };\n input.addEventListener('focus', () => {\n committed = false;\n });\n input.addEventListener('keydown', (e) => {\n if (e.key === 'Enter') {\n commit();\n input.blur();\n }\n });\n input.addEventListener('blur', commit);\n wrap.append(input, total);\n this.#pageInput = input;\n return wrap;\n }\n\n /** Which side panel is showing, if any. */\n openPanel(): PanelKind | null {\n return this.#panelKind;\n }\n\n /**\n * Whether this document has a table of contents worth offering.\n *\n * Many PDFs have none, and a control that can only report its own emptiness is a dead end, so\n * the outline button stays hidden until the outline is known to be non-empty. The first call\n * kicks off the lookup and refreshes the toolbar when it lands; `Zine.getOutline` memoizes, so\n * the panel does not pay for it twice.\n */\n hasOutline(): boolean {\n return this.#hasOutline === true;\n }\n\n /** Ask the document whether it has an outline, and refresh once the answer lands. Started as\n * the toolbar mounts so the button settles into place immediately rather than on first use. */\n #probeOutline(): void {\n if (this.#hasOutline !== null || !this.#zine.canOutline()) return;\n this.#hasOutline = false; // assume not, until we hear otherwise\n void this.#zine.getOutline().then((items) => {\n if (items.length === 0) return;\n this.#hasOutline = true;\n this.#refresh();\n // The menu may already be open with the entry missing; rebuild it so it appears.\n const popover = this.#popover;\n if (popover) {\n const def = this.#buttons.find((b) => b.el === popover.trigger)?.def;\n this.#closePopover();\n if (def) this.#openMenu(def, popover.trigger as HTMLButtonElement);\n }\n });\n }\n\n /**\n * Show one of the side panels, or close it if it is already up. They share the rail beside the\n * book, so opening one closes the others. Rebuilt each time, so the thumbnail rail always\n * matches the current spread grouping (which `spreadMode` and the responsive fallback change).\n */\n togglePanel(kind: PanelKind): void {\n const wasOpen = this.#panelKind;\n // Remember the query and results before tearing the search rail down, so its next open picks\n // up where the reader left it instead of blank. Only search carries state worth keeping;\n // thumbnails and the outline are rebuilt from the document each time by design.\n if (this.#panel instanceof Search) this.#searchState = this.#panel.getState();\n this.#panel?.destroy();\n this.#panel = null;\n this.#panelKind = null;\n if (wasOpen !== kind) {\n // Reopening the same kind toggles it shut, the same path the toolbar button takes, so the\n // scrim and Escape can dismiss the narrow-screen drawer without any new close machinery.\n this.#panel = new PANELS[kind](this.#zine, this.#container, {\n onDismiss: () => this.togglePanel(kind),\n ...(kind === 'search' && this.#searchState ? { state: this.#searchState } : {}),\n });\n applyColorScheme(this.#panel.root, this.#colorScheme);\n this.#panelKind = kind;\n }\n this.#refresh();\n }\n\n}\n\n/**\n * Controls that need the toolbar itself (a DOM widget, or a panel to open) are registered per\n * instance, since the registry is module-level and shared.\n */\nexport function registerWidgets(toolbar: Toolbar): void {\n registerBuiltins();\n defineControl({\n id: 'thumbnails',\n title: (ctx) =>\n toolbar.openPanel() === 'thumbnails' ? ctx.strings.hideThumbnails : ctx.strings.showThumbnails,\n icon: ICONS.thumbnails,\n // Documents only, for now: an image book is already a short, flat list of pictures.\n isVisible: (ctx) => ctx.zine.isDocument(),\n isActive: () => toolbar.openPanel() === 'thumbnails',\n action: (ctx) => {\n toolbar.togglePanel('thumbnails');\n ctx.close();\n },\n });\n defineControl({\n id: 'outline',\n title: (ctx) =>\n toolbar.openPanel() === 'outline' ? ctx.strings.hideOutline : ctx.strings.showOutline,\n icon: ICONS.outline,\n // Only when the document actually has entries to show.\n isVisible: () => toolbar.hasOutline(),\n isActive: () => toolbar.openPanel() === 'outline',\n action: (ctx) => {\n toolbar.togglePanel('outline');\n ctx.close();\n },\n });\n defineControl({\n id: 'pageInput',\n title: (ctx) => ctx.strings.pageWidgetLabel,\n render: () => toolbar.makePageInput(),\n });\n defineControl({\n id: 'search',\n title: (ctx) => (toolbar.openPanel() === 'search' ? ctx.strings.searchClose : ctx.strings.searchOpen),\n icon: ICONS.search,\n isVisible: (ctx) => ctx.zine.canSearch(),\n isActive: () => toolbar.openPanel() === 'search',\n action: () => toolbar.togglePanel('search'),\n });\n}\n","import { getControl } from './registry';\nimport { registerBuiltins } from './builtins';\nimport { ControlMenu } from './menu';\nimport { applyColorScheme, ensureStyles } from './styles';\nimport type {\n ContextMenuOptions,\n ControlContext,\n ControlItem,\n ControlsColorScheme,\n} from './types';\nimport type { Zine } from '../zine';\n\n/** The controls a right-click offers by default: the reading and viewing actions that suit a\n * pointer, each hidden by its own `isVisible` when it does not apply to this book. */\nconst DEFAULT_ITEMS: readonly ControlItem[] = [\n 'zoomIn',\n 'zoomOut',\n '|',\n 'prev',\n 'next',\n '|',\n 'fullscreen',\n 'mute',\n '|',\n 'print',\n 'download',\n 'share',\n];\n\n/** Toolbar-only controls: they need the Toolbar instance registered per-book, so a context menu\n * running without a toolbar cannot resolve them. Silently dropped rather than throwing. */\nconst TOOLBAR_ONLY = new Set(['pageInput', 'thumbnails', 'outline', 'search']);\n\n/**\n * A right-click menu of reading controls, opened at the cursor over the book.\n *\n * It reuses the toolbar's control definitions and its {@link ControlMenu} body, so the same icons\n * and actions appear whether a reader reaches them from the toolbar or a right-click. It is opt-in\n * (`contextMenu` off by default), because overriding the browser's own menu is a real cost to pay\n * only when asked. Works with `controls: false` too, which is the point: a bare book with no\n * toolbar can still offer a right-click menu.\n */\nexport class ContextMenu {\n #zine: Zine;\n #container: HTMLElement;\n #doc: Document;\n #items: readonly ControlItem[];\n #colorScheme: ControlsColorScheme;\n #hidden: ReadonlySet<string>;\n #menu: ControlMenu | null = null;\n #unbind: (() => void)[] = [];\n\n constructor(\n zine: Zine,\n container: HTMLElement,\n options: ContextMenuOptions = {},\n hidden: ReadonlySet<string> = new Set(),\n ) {\n this.#zine = zine;\n this.#container = container;\n this.#hidden = hidden;\n this.#doc = container.ownerDocument!;\n // The built-ins have to exist before the layout resolves ids; harmless if the toolbar has\n // already registered them (the registry replaces by id).\n registerBuiltins();\n ensureStyles(this.#doc);\n this.#colorScheme = options.colorScheme ?? 'auto';\n // Drop toolbar-only controls a bare book cannot resolve, so a custom list never throws for one.\n this.#items = (options.items ?? DEFAULT_ITEMS).filter(\n (item) => typeof item !== 'string' || !TOOLBAR_ONLY.has(item) || getControl(item) !== undefined,\n );\n\n const onContextMenu = (event: MouseEvent): void => {\n event.preventDefault(); // replace the browser's own menu with ours\n this.#open(event.clientX, event.clientY);\n };\n this.#container.addEventListener('contextmenu', onContextMenu);\n this.#unbind.push(() => this.#container.removeEventListener('contextmenu', onContextMenu));\n\n // Dismiss on a press anywhere outside the menu, and on Escape. Captured on the document so a\n // press on the book (which the gesture layer would otherwise swallow) still closes the menu.\n const onDocPointer = (event: Event): void => {\n if (this.#menu && !this.#menu.contains(event.target as Node)) this.#close();\n };\n const onKey = (event: KeyboardEvent): void => {\n if (event.key === 'Escape' && this.#menu) {\n this.#close();\n event.preventDefault();\n }\n };\n this.#doc.addEventListener('pointerdown', onDocPointer, true);\n this.#doc.addEventListener('keydown', onKey, true);\n this.#unbind.push(() => this.#doc.removeEventListener('pointerdown', onDocPointer, true));\n this.#unbind.push(() => this.#doc.removeEventListener('keydown', onKey, true));\n\n // Keep an open menu honest if the book changes under it (a keyboard turn, a programmatic zoom):\n // the disabled and active states are re-read, just as the toolbar does.\n const refresh = (): void => this.#menu?.refresh();\n this.#unbind.push(zine.on('pageChanged', refresh));\n this.#unbind.push(zine.on('zoomChanged', refresh));\n this.#unbind.push(zine.on('flipEnd', refresh));\n }\n\n #context(): ControlContext {\n return {\n zine: this.#zine,\n close: () => this.#close(),\n colorScheme: this.#colorScheme,\n strings: this.#zine.strings,\n };\n }\n\n #open(clientX: number, clientY: number): void {\n this.#close();\n // A right-click menu closes on any selection, the way a native context menu does — whether the\n // action was terminal (print, share, fullscreen) or incremental (a zoom or page step). This is\n // the one place it differs from the toolbar's overflow dropdown, which stays open so a step can\n // be repeated. Terminal controls that call ctx.close() themselves just close a beat sooner.\n const menu = new ControlMenu(\n this.#doc,\n () => this.#context(),\n () => this.#close(),\n this.#hidden,\n );\n menu.build(this.#items);\n // Nothing applies to this book (an image book with zoom off, say): no menu rather than an empty\n // box. The native menu is already suppressed, but an empty popup would be worse than none.\n if (!menu.el.querySelector('button')) return;\n // Fixed to the viewport so it lands under the cursor without depending on the container being a\n // positioning context; the layer isolates its own events from the book's gesture handlers.\n const layer = this.#doc.createElement('div');\n layer.className = 'zine-context-layer';\n applyColorScheme(layer, this.#colorScheme);\n layer.appendChild(menu.el);\n this.#container.appendChild(layer);\n this.#menu = menu;\n menu.refresh(); // read state before first paint, so a disabled entry shows disabled at once\n this.#place(menu.el, clientX, clientY);\n menu.focusFirst();\n }\n\n /** Pin the menu at the cursor, nudged to stay inside the book's box. */\n #place(el: HTMLElement, clientX: number, clientY: number): void {\n const rect = this.#container.getBoundingClientRect();\n const box = el.getBoundingClientRect();\n if (!box.width) {\n // Not laid out (e.g. happy-dom has no layout): just pin it at the cursor.\n el.style.left = `${clientX}px`;\n el.style.top = `${clientY}px`;\n return;\n }\n const left = Math.min(clientX, rect.right - box.width - 4);\n const top = Math.min(clientY, rect.bottom - box.height - 4);\n el.style.left = `${Math.max(rect.left + 4, left)}px`;\n el.style.top = `${Math.max(rect.top + 4, top)}px`;\n }\n\n #close(): void {\n if (!this.#menu) return;\n const layer = this.#menu.el.parentElement;\n const hadFocus = this.#doc.activeElement !== null && this.#menu.contains(this.#doc.activeElement);\n this.#menu.destroy();\n layer?.remove();\n this.#menu = null;\n // Return focus to the book so keyboard control is not stranded on a removed node.\n if (hadFocus && typeof this.#container.focus === 'function') this.#container.focus();\n }\n\n destroy(): void {\n this.#close();\n for (const off of this.#unbind) off();\n this.#unbind = [];\n }\n}\n","import { registerWidgets, Toolbar } from './toolbar';\nimport { ContextMenu } from './contextMenu';\nimport type { ContextMenuOptions, ControlsOptions } from './types';\nimport type { Zine } from '../zine';\n\nexport { defineControl, getControl, DEFAULT_ITEMS } from './registry';\nexport { ICONS, createIcon } from './icons';\nexport type {\n ContextMenuOptions,\n ControlContext,\n ControlDef,\n ControlItem,\n ControlsColorScheme,\n ControlsOptions,\n ControlsPosition,\n} from './types';\n\n/**\n * Build the toolbar inside `container` and return its teardown.\n *\n * This module is the lazy chunk `zine.ts` reaches through a dynamic `import()`, so a book built\n * with `controls: false` never downloads the toolbar, its icons or its stylesheet. The file is\n * named `controls.ts` rather than `index.ts` so the emitted chunk is `controls-<hash>.js`, which\n * is what the size-limit budget watches.\n */\nexport function mountControls(\n zine: Zine,\n container: HTMLElement,\n options: ControlsOptions = {},\n hidden: ReadonlySet<string> = new Set(),\n): () => void {\n const toolbar = new Toolbar(zine, container, options, hidden);\n // Built-ins and the instance-bound widgets have to exist before the layout resolves ids.\n registerWidgets(toolbar);\n toolbar.mount(options);\n return () => toolbar.destroy();\n}\n\n/**\n * Attach the right-click menu to `container` and return its teardown.\n *\n * Shares this lazy chunk with {@link mountControls}, so a book that wants only the context menu\n * (`controls: false`, `contextMenu: true`) still reuses the same icons and control definitions.\n */\nexport function mountContextMenu(\n zine: Zine,\n container: HTMLElement,\n options: ContextMenuOptions = {},\n hidden: ReadonlySet<string> = new Set(),\n): () => void {\n const menu = new ContextMenu(zine, container, options, hidden);\n return () => menu.destroy();\n}\n","import {\n buildSpreads,\n shouldSinglePage,\n type Direction,\n type Spread,\n type SpreadMode,\n} from './engine/spread';\nimport { Virtualizer } from './engine/virtualizer';\nimport { Emitter, type ZineEventMap } from './engine/emitter';\nimport { FlipMachine } from './engine/stateMachine';\nimport { PointerRecognizer, PinchRecognizer, bindGestures, type GestureEnd } from './engine/input';\nimport {\n bindDeepLink,\n claimHash,\n hashWithPage,\n pageFromHash,\n releaseHash,\n type DeepLinkHandle,\n} from './engine/deeplink';\nimport {\n CURL_TYPES,\n DEFAULT_CURL,\n IMPORTABLE_CURLS,\n type CurlSpec,\n type CurlType,\n} from './geometry/curls/types';\nimport { selectRenderer, type RendererOption } from './renderer/select';\nimport type { FlipDirection, PageContent, Renderer, SpreadContent } from './renderer/types';\nimport type { DownloadInfo, LoadProgress, OutlineItem, Source } from './source/types';\nimport { composeSource } from './source/compose';\nimport type { ContextMenuOptions, ControlsOptions } from './controls/types';\nimport type { LoaderHandle } from './loading/loading';\nimport type { FlipSound } from './sound/flipSound';\nimport { resolveStrings, type ZineStrings } from './strings';\n\n/** Grab-zone size as a fraction of the smaller container dimension. */\nconst CORNER_FRACTION = 0.25;\n\n/** Wheel-zoom sensitivity: scale multiplies by exp(-deltaY * this) per wheel event. */\nconst WHEEL_ZOOM_SENSITIVITY = 0.0015;\n\n/** Pointer movement (px) beyond which a press becomes a drag rather than a tap. */\nconst DRAG_THRESHOLD = 6;\n\n/** Window (ms) a single click waits to rule out a double-click before flipping. Also the window\n * within which the library pairs two clicks into its own double-click (see #bindDoubleClickZoom). */\nconst DOUBLE_CLICK_MS = 250;\n\n/** How far (px) the second click of a pair may land from the first and still count as the same\n * spot. Forgiving enough for a wobble, tight enough that two different zones don't pair. */\nconst DOUBLE_CLICK_MOVE = 24;\n\n/** Grace period after a flip lands during which a paired double-click in a *dead* edge zone (the\n * first/last spread, where the turn can't happen) is read as the tail of a rapid flipping streak\n * and swallowed rather than zooming. Two streak clicks pair within DOUBLE_CLICK_MS of each other,\n * so two of them span this window; a deliberate zoom comes after the reader pauses on the end\n * page, well outside it. */\nconst FLIP_STREAK_MS = 2 * DOUBLE_CLICK_MS;\n\n/** A lone page fills the container, so it sweeps the full width where a spread leaf only covers\n * its half. `flipDuration` is the spread timing; lone pages stretch it slightly so twice the\n * travel still reads at a comparable speed instead of whipping across. */\nconst LONE_PAGE_FLIP_SCALE = 1.2;\n\n/** Characters of surrounding text shown on either side of a search match. */\nconst EXCERPT_PAD = 32;\n\n/** Quiet period before re-rasterizing at a new zoom, so a pinch does not rasterize every frame. */\nconst ZOOM_TILE_DELAY = 60;\n\n/** Ceiling on the zoomed page raster. Memory grows with the square of this, so a letter page at\n * 4x would run past 100 MB; 2.5x keeps a deep zoom far sharper than fit-to-screen for a\n * fraction of that. */\nconst MAX_PAGE_UPGRADE = 2.5;\n\n/** Raster texels we want per CSS px for crisp text (roughly retina density). A raster matched 1:1\n * to CSS px looks soft: glyph antialiasing is baked at exactly the on-screen size and then\n * bilinear-sampled under a fractional transform, with nothing to downsample. Rendering ~2x and\n * letting the GPU minify supersamples the edges. A retina display already paints its raster at\n * ~2x CSS px, so this floor is scaled by 1/dpr and only lifts low-dpr screens. */\nconst TEXT_SUPERSAMPLE = 1.5;\n\n/** Flip time under `prefers-reduced-motion`. Short enough not to read as animation, long enough\n * to show which way the page went — an instant swap leaves the reader guessing. */\nconst REDUCED_MOTION_DURATION = 120;\n\n/** Eased fold pose (0..1) at which the page number is announced, while the fold keeps animating to\n * its full duration. The eases decelerate into the finish, so by this pose the turn already reads\n * as landed; announcing here updates the number on time without shortening the visible flip. This\n * is a pure timing lead for the number: it does not touch the animation clock or the end pose. */\nconst PAGE_LEAD = 0.85;\n\n/** Lone-page turns dissolve rather than land on a facing half; announce a touch earlier than\n * a spread so the number updates as the under-page takes over, not after the fade tail. */\nconst PAGE_LEAD_LONE = 0.82;\n\n/** How far the corner peek hint lifts the leading page (fold progress 0..1) before settling back —\n * enough to read as a liftable page, well short of committing a turn. */\nconst PEEK_PROGRESS = 0.12;\n\n/** Duration of each leg (out, then back) of the corner peek, in ms. */\nconst PEEK_LEG_MS = 360;\n\n/** How far the whole spread slides toward the edge on a dead boundary tap, in px, before it springs\n * back — enough to read as \"nothing past here\", well short of looking like a turn. */\nconst EDGE_NUDGE_PX = 28;\n\n/** Duration of the boundary rubber-band nudge (out and back together), in ms. */\nconst EDGE_NUDGE_MS = 260;\n\n/** How long the reader may sit on the opening spread without turning a page before the peek replays\n * once as a nudge. Long enough not to interrupt someone who is simply reading the first page. */\nconst IDLE_NUDGE_MS = 7000;\n\n/** How long the first-zoom \"drag to move\" caption stays up before fading on its own. */\nconst CAPTION_MS = 2200;\n\n/** localStorage key for learned gestures under `hints.persist`. Shared across books on a site so a\n * gesture learned on one is not re-taught on the next. */\nconst HINTS_STORAGE_KEY = 'zine:hints-learned';\n\n/** localStorage key for the reader's mute choice under `sound.persist`. Shared across books on a\n * site so a reader who silenced (or enabled) flip sound once is remembered everywhere. */\nconst SOUND_MUTED_STORAGE_KEY = 'zine:sound-muted';\n\n/** Which gestures the reader has demonstrated, so the matching discoverability hint stays quiet. */\ninterface Learned {\n turn: boolean;\n zoom: boolean;\n pan: boolean;\n}\n\n/** One page that matched a {@link Zine.search} query. */\nexport interface SearchHit {\n /** Zero-based page index; pass straight to `flipTo`. */\n page: number;\n /** The match with a little text either side, ellipsised where it was cut. */\n excerpt: string;\n}\n\n/** Pull a readable snippet around a match, collapsing the whitespace PDFs are full of. */\nfunction excerptAround(text: string, at: number, length: number): string {\n const start = Math.max(0, at - EXCERPT_PAD);\n const end = Math.min(text.length, at + length + EXCERPT_PAD);\n const slice = text.slice(start, end).replace(/\\s+/g, ' ').trim();\n return `${start > 0 ? '…' : ''}${slice}${end < text.length ? '…' : ''}`;\n}\n\ninterface DragState {\n direction: FlipDirection;\n targetIndex: number;\n toPage: number;\n toContent: SpreadContent;\n width: number;\n t: number;\n}\n\nexport interface ZoomOptions {\n /** Whether zooming is allowed at all; default true. */\n enabled?: boolean;\n /** Maximum zoom scale; default 4. */\n max?: number;\n /** Zoom on Ctrl/⌘ + wheel (also how trackpad pinch arrives on desktop); default true. */\n wheel?: boolean;\n /** Zoom levels cycled by double-click (wraps to the first); `false` disables. Default [1, 2, 4]. */\n doubleClick?: number[] | false;\n /**\n * Listen for double-click zoom inside the click-to-flip zones. Default: off in 'edge' mode\n * (edges flip instantly, no delay), on in 'half' mode. Turning it on in 'edge' mode makes\n * clicks wait out `clickFlipDelay` so a double-click can preempt them with a zoom.\n */\n doubleClickInFlipZone?: boolean;\n}\n\nexport interface ZineOptions {\n /** Content source (e.g. an ImageSource). Required. */\n source: Source;\n /** Renderer selection; default 'auto'. */\n renderer?: RendererOption;\n /**\n * Page-curl model for the WebGL2 renderer. Default `'cone'`.\n *\n * `'cone'` and `'simple'` are bundled and can be named directly. The others are imported and\n * passed as models, so a book that never uses them never carries their math:\n *\n * ```ts\n * import { silk } from '@zinejs/core/curls';\n * new Zine(el, { source, curl: silk });\n * ```\n *\n * Any object of the same shape works, so a custom curl needs no registration.\n */\n curl?: CurlSpec;\n /** Fixed container width in px; omit to let the container/CSS drive the size. */\n width?: number;\n /** Fixed container height in px; omit to let the container/CSS drive the size. */\n height?: number;\n /** Reading direction; default 'ltr'. */\n direction?: Direction;\n /** How pages group into spreads: 'double' | 'single' | 'cover' | 'book'. Default 'cover'. */\n spreadMode?: SpreadMode;\n /** Image URL prepended as a lone front cover (adds a page). */\n frontCover?: string;\n /** Image URL appended as a lone back cover (adds a page). */\n backCover?: string;\n /** Replace source pages with image URLs, keyed by 0-based source index (negative = from the end). */\n pages?: Record<number, string>;\n /** Page to open on; default 0. */\n startPage?: number;\n /** Flip animation duration in ms; default 500. */\n flipDuration?: number;\n /** How a tap/click turns pages: near an edge ('edge'), by page half ('half'), or 'off'. Default 'edge'. */\n clickToFlip?: 'edge' | 'half' | 'off';\n /** How a page whose size differs from the book fits: 'contain' (default) keeps its aspect and\n * centers it (a small margin shows); 'fill' stretches it to the book's shape. Either way the\n * book keeps one shape for the whole document, so mixed-size pages never cause a layout shift. */\n fit?: 'contain' | 'fill';\n /** Override any reader-facing text (control labels, screen-reader announcements, panel and\n * loading text) for localization. English by default; pass only the keys you want to change.\n * Interpolated entries (e.g. the page announcement) are functions. See {@link ZineStrings}. */\n strings?: Partial<ZineStrings>;\n /** Edge-zone size in px per side, used when clickToFlip is 'edge'. Default 64. */\n clickZoneSize?: number;\n /**\n * On a mouse, change the cursor over the book to hint what a press would do: `pointer` where a\n * click turns the page, `zoom-in` where a double-click zooms (including the edge peel band, so a\n * sweep stays calm), `grab` over the whole page once zoomed (a drag pans), and `grabbing` while a\n * peel or pan is actually in flight. Default true. Set false when a changing cursor would be noise\n * in the embedding design. No effect on touch.\n */\n cursorHints?: boolean;\n /**\n * Subtle, one-shot discoverability hints shown on the book itself (so they reach touch readers,\n * unlike {@link cursorHints}): the leading page corner peeks and settles on first open, nudges\n * once more if the reader sits idle without turning a page, a small \"drag to move\" caption appears\n * the first time they zoom in, and (on a mouse) a \"double-click or Ctrl/⌘-scroll to zoom\" caption\n * appears if a lone click lands in a dead zone where only a double-click would do anything. Each\n * hint stops for good the moment the reader performs the gesture it teaches. Honors\n * `prefers-reduced-motion` (the motion cues are skipped). Default true. Pass an object to keep what\n * the reader has learned across visits: `{ persist: true }` records it in localStorage so a\n * returning reader is not re-taught (default is in-memory, per page load).\n */\n hints?: boolean | { enabled?: boolean; persist?: boolean };\n /**\n * Play a short sound on each page turn. Off by default — web audio is unexpected, so it is\n * strictly opt-in. `true` uses the bundled clip at the default volume; an object overrides it:\n * `url` points at your own clip (fetched at runtime), `volume` is 0..1, and `muted: true` offers\n * the sound but starts it silent so the reader turns it on themselves (via the `mute` control),\n * and `persist: true` remembers the reader's mute choice across visits in localStorage (otherwise\n * it resets each load). Playback is best-effort and never throws: a browser that gates audio behind\n * a gesture, lacks Web Audio, or cannot decode the clip just stays silent. Whenever sound is\n * enabled — audible or muted — a `mute` control appears in the toolbar and right-click menu.\n */\n sound?: boolean | { url?: string; volume?: number; muted?: boolean; persist?: boolean };\n /**\n * Delay (ms) a click waits before flipping, so a double-click can preempt it with a zoom.\n * Omit for auto: 0 when double-click zoom is inactive, 250 when it's active.\n */\n clickFlipDelay?: number;\n /** Zoom behavior. */\n zoom?: ZoomOptions;\n /** Container widths below this (px) switch to one page per spread; default 640. */\n singlePageThreshold?: number;\n /**\n * Whether a narrow container may override `spreadMode` and show one page at a time. Default\n * true.\n *\n * Set false to hold the configured mode at every width — a two-page spread stays two pages on\n * a phone, however small. (`singlePageThreshold: 0` has the same effect; this says it out\n * loud, and can be changed at runtime with {@link Zine.setResponsiveSpread}.)\n */\n responsiveSpread?: boolean;\n /**\n * The built-in toolbar. Shown by default; pass `false` to render none, or an object to choose\n * its position and which controls appear.\n */\n controls?: boolean | ControlsOptions;\n /**\n * The built-in loading indicator, shown while a source that reports download progress (a PDF\n * from a URL) fetches and prepares its first spread. On by default; pass `false` to render none\n * and drive your own from the `progress` event and the `ready` promise. Retheme the default with\n * the `--zine-loading-bg`, `--zine-loading-fg`, `--zine-loading-accent`, and `--zine-loading-track`\n * CSS variables.\n */\n loading?: boolean;\n /**\n * Keep the current page in the URL hash (`#page=12`), so a link opens where the reader was and\n * back/forward move through the book. Default true.\n *\n * Only a `page` key is read or written — anything else in the hash is left alone — and updates\n * use `replaceState`, so turning pages does not fill the back button. Pass `false` if your app\n * owns the hash.\n */\n deepLink?: boolean;\n /**\n * Suppress the browser's right-click menu over the book. Default false.\n *\n * A mild deterrent for published documents, not protection: the pages are still in the DOM and\n * reachable by anyone who wants them. It also takes away Inspect and \"Open image in new tab\"\n * for everyone, so it is off unless asked for.\n *\n * Mutually exclusive with {@link contextMenu}: one replaces the native menu, the other only\n * removes it, so enabling both is a contradiction and throws.\n */\n disableContextMenu?: boolean;\n /**\n * A right-click menu of reading controls (zoom, page turns, fullscreen, print, download, share),\n * opened at the cursor over the book. On by default. Over a canvas the native menu offers only\n * the generic page actions (Back, Reload, View source), so replacing it with the book's own\n * controls is the better trade; pass `false` to keep the browser's menu.\n *\n * Reuses the toolbar's icons and actions, so the same controls appear whether reached from the\n * toolbar or a right-click, and it works even with `controls: false` — a bare book can still\n * offer one. `true` uses the default set; an object with `items` replaces the layout. Mouse\n * affordance only; touch never raises it.\n *\n * Turned off automatically when {@link disableContextMenu} is set (that option removes the native\n * menu with nothing in its place); setting both explicitly throws.\n */\n contextMenu?: boolean | ContextMenuOptions;\n /**\n * Control ids to hide from both surfaces at once: off the toolbar (bar and overflow menu) and out\n * of the right-click menu. `['print', 'download']` removes those two everywhere, wherever they are\n * nested, without having to edit `controls.items` and `contextMenu.items` separately.\n *\n * A blunt cross-cutting filter: it only removes, and it applies to whatever layout each surface\n * ends up with (default or custom). To rearrange or add controls, set the per-surface `items`\n * instead.\n */\n hideControls?: readonly string[];\n}\n\n/**\n * The public flipbook. Composes the engine (spreads, virtualizer, events), a\n * lazily-selected renderer, and a content source. Construction is synchronous\n * but setup is async — await `ready` (or listen for the `ready` event) before\n * driving it.\n */\nexport class Zine {\n #container: HTMLElement;\n #source: Source;\n #emitter = new Emitter<ZineEventMap>();\n #virtualizer = new Virtualizer(2);\n #machine = new FlipMachine();\n #direction: Direction;\n #spreadMode: SpreadMode;\n /** The mode the book was built with, so toggling away from one page can return to it — a\n * `cover` book comes back as `cover`, keeping its lone first page, not as plain `double`. */\n #configuredMode: SpreadMode;\n #curl: CurlSpec;\n #anchorY = 1; // where the last tap/drag grabbed (0=top, 1=bottom); drives anchored curls\n #clickToFlip: 'edge' | 'half' | 'off';\n #fit: 'contain' | 'fill';\n #strings: ZineStrings;\n #clickZoneSize: number;\n #cursorHints: boolean;\n /** Last mouse position over the container in client px, so the cursor can be re-derived after a\n * page turn, zoom, or resize changes what a press there would do — without the pointer moving. */\n #pointerClient: { x: number; y: number } | null = null;\n #unbindCursor: (() => void) | null = null;\n #hintsEnabled: boolean;\n #hintsPersist: boolean;\n /** Page-flip sound. Off unless `sound` is set; the controller and its clip load lazily (dynamic\n * import) only then, so a silent book pays no audio bytes. Muting is remembered even before the\n * controller finishes loading, via #soundMuted. */\n #soundEnabled: boolean;\n #soundOptions: { url?: string; volume?: number };\n #sound: FlipSound | null = null;\n #soundMuted = false;\n /** Remember the reader's mute choice across visits (localStorage) under `sound.persist`. */\n #soundPersist = false;\n /** Guards the one-time lazy import of the sound chunk. Deferred until sound is actually audible,\n * so a book that starts muted pays nothing until the reader unmutes. */\n #soundLoadStarted = false;\n /** What the reader has demonstrated they already know, so a hint teaching it stays quiet. Set by\n * the gesture itself (a turn, a zoom, a pan); persisted to localStorage when `hints.persist`. */\n #learned: Learned = { turn: false, zoom: false, pan: false };\n /** The one-shot idle nudge timer (armed on ready, cleared by any real gesture). */\n #idleTimer: ReturnType<typeof setTimeout> | null = null;\n /** Tags the peek's own rAF loop so a real flip (or teardown) can cancel a peek mid-play without\n * the stale frame repainting a half-folded page. Distinct from #activeAnim, which is flip-only. */\n #peekAnim: object | null = null;\n /** Tags the boundary rubber-band's rAF loop, so a real flip/drag/zoom (or teardown) cancels a nudge\n * in flight without a stale frame leaving the spread shifted off-center. Shares #raf, like #peekAnim. */\n #nudgeAnim: object | null = null;\n /** The lazily-built hint caption element (null until a caption shows it). */\n #captionEl: HTMLElement | null = null;\n #captionTimer: ReturnType<typeof setTimeout> | null = null;\n /** Delays the \"double-click to zoom\" caption past the pairing window, so a real double-click zooms\n * (which teaches zoom) instead of flashing the hint first. Cleared if the click pairs or zoom is learned. */\n #zoomHintTimer: ReturnType<typeof setTimeout> | null = null;\n /** The zoom caption teaches at most once a session; a reader who ignores it is not nagged on every\n * dead click. (The peek and pan captions are one-shot for the same reason.) */\n #zoomHintShown = false;\n /** pointerType of the last press, so the zoom caption stays mouse-only: touch has no \"double-click\"\n * and double-tap/pinch are already well known (this mirrors why cursorHints is mouse-only). */\n #lastPointerType = '';\n #honorDoubleClickInFlipZone: boolean;\n #clickFlipDelayValue: number;\n #singlePageThreshold: number;\n #singlePage = false;\n /** Last layout announced via `spreadChanged`, so a rebuild that changes nothing stays quiet. */\n #effectiveModeShown: SpreadMode | null = null;\n #narrow = false; // responsive: container currently below singlePageThreshold\n #responsiveSpread: boolean;\n #lastAspect: number | null = null; // last container aspect-ratio written (avoids redundant style writes)\n #startPageOption: number | undefined;\n #deepLinkEnabled: boolean;\n #disableContextMenu: boolean;\n #deepLink: DeepLinkHandle | null = null;\n #unsubscribeDeepLink: (() => void) | null = null;\n #spreads: Spread[] = [];\n #current = 0;\n #currentPage = 0;\n #currentContent: SpreadContent = { left: null, right: null };\n #flipDuration: number;\n #zoomEnabled: boolean;\n #wheelZoom: boolean;\n #doubleClickLevels: number[] | null;\n #maxZoom: number;\n #resizeObserver: ResizeObserver | null = null;\n #updateScheduled = false;\n #scale = 1;\n #tx = 0;\n #ty = 0;\n #renderer: Renderer | null = null;\n #fellBack = false;\n #destroyed = false;\n #raf: number | null = null;\n #drag: DragState | null = null;\n #pendingGrab: { direction: FlipDirection; targetIndex: number; toPage: number; width: number } | null = null;\n #press: { x: number; y: number } | null = null;\n #pendingClickTimer: ReturnType<typeof setTimeout> | null = null;\n // Latest flip intent requested while one was already animating; replayed on settle so\n // clicks/keys during a turn aren't dropped and continuous flipping keeps advancing.\n #queuedFlip: (() => void) | null = null;\n // The fold animation currently on screen, so a new flip request can land it instantly\n // (snap to its end pose + commit) and start immediately instead of waiting it out.\n #activeAnim: { toT: number; direction: FlipDirection; onDone: () => void } | null = null;\n // Bumped per flip so an async #runFlip that resumes after being superseded can bail.\n #flipGeneration = 0;\n #pan: { baseTx: number; baseTy: number } | null = null;\n #pinching = false;\n #pinchBaseScale = 1;\n #reducedMotion = false;\n #liveRegion: HTMLElement | null = null;\n #unbindInput: (() => void) | null = null;\n #unbindWheel: (() => void) | null = null;\n #unbindContextMenu: (() => void) | null = null;\n #unbindDblClick: (() => void) | null = null;\n /** First click of a possible library-detected double-click, and when/where it landed. Kept\n * because the browser's own `dblclick` misfires on rapid streaks (the counter resets). */\n #lastClick: { t: number; x: number; y: number } | null = null;\n /** When the last flip committed (performance.now()). A dead-edge double-click landing soon after\n * is the tail of a flipping streak, not a zoom request; see #zoomAt and FLIP_STREAK_MS. */\n #lastFlipAt = Number.NEGATIVE_INFINITY;\n #a11yCleanup: (() => void) | null = null;\n /** Resolved once and reused; see {@link getOutline}. */\n #outline: Promise<OutlineItem[]> | null = null;\n /** Pages whose content improved while a flip was running, to repaint once it settles. */\n #pendingUpgrades = new Set<number>();\n #controlsOption: boolean | ControlsOptions;\n #controlsCleanup: (() => void) | null = null;\n #contextMenuOption: boolean | ContextMenuOptions;\n #contextMenuCleanup: (() => void) | null = null;\n /** Control ids hidden from every surface by {@link ZineOptions.hideControls}. Empty when unset. */\n #hiddenControls: ReadonlySet<string>;\n #loadingOption: boolean;\n #loader: LoaderHandle | null = null;\n #lastProgress: LoadProgress | null = null;\n /** 'download' while bytes arrive, 'preparing' while the first spread rasterizes, 'done' after\n * it paints. Guards a late-arriving loader chunk from mounting once the book is already up. */\n #loaderPhase: 'download' | 'preparing' | 'done' = 'download';\n /** Resolution multiplier the current spread is rasterized at; 1 while not zoomed. */\n #zoomedAt = 1;\n /** Guards against an out-of-date tile landing after the view has already moved on. */\n #tileGeneration = 0;\n #tileTimer: ReturnType<typeof setTimeout> | null = null;\n #ready: Promise<void>;\n\n /** Stable seek API: drive the fold to a fixed progress without animating (visual regression). */\n readonly debug = {\n setFlipProgress: (t: number, direction: FlipDirection): void => {\n this.#renderer?.setFlipProgress(t, direction);\n },\n };\n\n constructor(container: HTMLElement, options: ZineOptions) {\n validateOptions(container, options);\n this.#container = container;\n if (options.width !== undefined) container.style.width = `${options.width}px`;\n if (options.height !== undefined) container.style.height = `${options.height}px`;\n this.#source = composeSource(options.source, {\n frontCover: options.frontCover,\n backCover: options.backCover,\n pages: options.pages,\n });\n const direction = options.direction ?? 'ltr';\n this.#direction = direction;\n // Set before the spread model is built: that is where a page in the URL is honoured.\n // Claimed before the spread model is built, which is where the hash is read: a second book\n // on the same page must not open on the first one's page.\n this.#deepLinkEnabled = (options.deepLink ?? true) && claimHash();\n this.#disableContextMenu = options.disableContextMenu ?? false;\n this.#spreadMode = options.spreadMode ?? 'cover';\n this.#configuredMode = this.#spreadMode;\n this.#curl = options.curl ?? DEFAULT_CURL;\n this.#clickToFlip = options.clickToFlip ?? 'edge';\n this.#fit = options.fit ?? 'contain';\n this.#strings = resolveStrings(options.strings);\n this.#clickZoneSize = options.clickZoneSize ?? 64;\n this.#cursorHints = options.cursorHints ?? true;\n const hints = options.hints ?? true;\n this.#hintsEnabled = hints === true || (hints !== false && (hints.enabled ?? true));\n this.#hintsPersist = hints !== true && hints !== false && (hints.persist ?? false);\n if (this.#hintsPersist) this.#loadLearned();\n const sound = options.sound ?? false;\n this.#soundEnabled = sound !== false;\n this.#soundOptions = sound === true || sound === false ? {} : sound;\n // `{ muted: true }` offers the sound but starts it silent: the controller loads, the mute\n // control shows, and the reader unmutes to turn it on.\n this.#soundMuted = typeof sound === 'object' && sound !== null ? (sound.muted ?? false) : false;\n this.#soundPersist = typeof sound === 'object' && sound !== null ? (sound.persist ?? false) : false;\n // A remembered choice from a previous visit overrides the configured default.\n if (this.#soundEnabled && this.#soundPersist) this.#loadSoundMuted();\n this.#singlePageThreshold = options.singlePageThreshold ?? 640;\n this.#responsiveSpread = options.responsiveSpread ?? true;\n this.#controlsOption = options.controls ?? true;\n // On by default, but a book that asked to suppress the native menu with nothing in its place\n // (disableContextMenu) means it: don't hand it our menu unasked. Setting both explicitly is a\n // contradiction and throws in validateOptions.\n this.#contextMenuOption = options.contextMenu ?? (options.disableContextMenu ? false : true);\n this.#hiddenControls = new Set(options.hideControls ?? []);\n this.#loadingOption = options.loading ?? true;\n this.#startPageOption = options.startPage;\n // Sync sources (a known page count) build spreads now — so bad pageCount/startPage\n // throw immediately from `new Zine`. Async sources (an `open()`) defer to #init.\n if (typeof this.#source.open !== 'function') {\n this.#buildSpreadModel();\n }\n this.#flipDuration = options.flipDuration ?? 800;\n this.#zoomEnabled = options.zoom?.enabled ?? true;\n this.#wheelZoom = options.zoom?.wheel ?? true;\n const dbl = options.zoom?.doubleClick;\n const levels = dbl === false ? [] : [...(dbl ?? [1, 2, 4])].sort((a, b) => a - b);\n this.#doubleClickLevels = levels.length > 0 ? levels : null;\n this.#maxZoom = options.zoom?.max ?? 4;\n\n // Arbitrate click-to-flip vs double-click zoom inside the flip zones.\n // Listen for double-click zoom in flip zones: default off in 'edge', on in 'half'.\n // clickFlipDelay === 0 hard-disables it. When honored, a delay lets a click wait\n // out a possible double-click; auto default 250 ms, overridable.\n const doubleClickZoomActive = this.#zoomEnabled && this.#doubleClickLevels !== null;\n const listenInFlipZone = options.zoom?.doubleClickInFlipZone ?? (this.#clickToFlip === 'half');\n this.#honorDoubleClickInFlipZone =\n this.#clickToFlip !== 'off' &&\n doubleClickZoomActive &&\n listenInFlipZone &&\n options.clickFlipDelay !== 0;\n this.#clickFlipDelayValue = this.#honorDoubleClickInFlipZone\n ? (options.clickFlipDelay ?? DOUBLE_CLICK_MS)\n : 0;\n\n this.#ready = this.#init(options.renderer ?? 'auto');\n }\n\n /** Resolves once the renderer is mounted and the first spread is painted. */\n get ready(): Promise<void> {\n return this.#ready;\n }\n\n getPageCount(): number {\n return this.#source.pageCount;\n }\n\n getPage(): number {\n return this.#currentPage;\n }\n\n /** The element this flipbook was mounted into. */\n get container(): HTMLElement {\n return this.#container;\n }\n\n /**\n * Where the book is actually painted inside the container, in container pixels: the\n * aspect-fitted, centred region, which is shorter (or narrower) than the container whenever the\n * two aspect-ratios do not match. A side rail uses it to stand exactly as tall as the page rather\n * than the whole container. Null before the first paint, when there is nothing to measure yet.\n */\n getPageBox(): { x: number; y: number; width: number; height: number } | null {\n return this.#renderer?.measure().book ?? null;\n }\n\n /**\n * How pages are currently grouped, one entry per spread — the same model the renderer paints,\n * so it already reflects `spreadMode` and the responsive single-page fallback. Useful for\n * building a page list or thumbnail rail that matches the book.\n */\n getSpreads(): readonly Spread[] {\n return this.#spreads;\n }\n\n /** The spread index currently on screen; indexes into {@link getSpreads}. */\n getSpreadIndex(): number {\n return this.#current;\n }\n\n /** Reading direction, which mirrors each spread's left/right sides. */\n getDirection(): Direction {\n return this.#direction;\n }\n\n /** How pages are grouped: the configured mode, not what a narrow container may be forcing. */\n getSpreadMode(): SpreadMode {\n return this.#spreadMode;\n }\n\n /** Whether the book is showing one page at a time, for whatever reason. */\n isSinglePage(): boolean {\n return this.#singlePage;\n }\n\n /**\n * Regroup the pages, keeping the reader on the page they are looking at.\n *\n * A narrow container may still override this with one page at a time; see\n * {@link setResponsiveSpread}.\n */\n setSpreadMode(mode: SpreadMode): void {\n if (mode === this.#spreadMode) return;\n this.#spreadMode = mode;\n // The narrow measurement was taken under the layout we are leaving, and a book showing one\n // page is narrower than the same book showing two. Holding on to it would let a container\n // that only shrank *because* of one-page mode veto the way back out. Drop it and let the\n // next measurement decide afresh.\n this.#narrow = false;\n this.#rebuildSpreads();\n void this.#renderCurrent();\n }\n\n /**\n * Switch between one page and two, and back again.\n *\n * Returning to two pages restores the mode the book was built with, so a `cover` book gets its\n * lone first page back rather than becoming a plain `double`.\n */\n toggleSpreadMode(): void {\n this.setSpreadMode(\n this.#spreadMode === 'single'\n ? this.#configuredMode === 'single'\n ? 'double' // built as single: there is no other mode to return to\n : this.#configuredMode\n : 'single',\n );\n }\n\n /** Whether a narrow container is allowed to override `spreadMode` with one page at a time. */\n getResponsiveSpread(): boolean {\n return this.#responsiveSpread;\n }\n\n /**\n * Whether the book is showing one page at a time *because the container is narrow*, rather\n * than because it was asked to. False when `responsiveSpread` is off.\n *\n * A book already set to `single` is not being overridden, however narrow it is: asking for one\n * page makes the book itself narrower, so reporting that as an override would strand the reader\n * in a layout they could no longer switch out of.\n */\n isResponsiveSingle(): boolean {\n return this.#narrow && this.#spreadMode !== 'single';\n }\n\n /**\n * Allow or forbid the narrow-container override, and re-lay out at once.\n *\n * Turning it off holds the configured `spreadMode` at every width; turning it back on\n * re-measures, so a book on a narrow screen collapses to single again straight away.\n */\n setResponsiveSpread(enabled: boolean): void {\n if (enabled === this.#responsiveSpread) return;\n this.#responsiveSpread = enabled;\n if (!enabled) {\n if (!this.#narrow) return; // nothing was being overridden\n this.#narrow = false;\n this.#rebuildSpreads();\n } else {\n const width = this.#renderer?.measure().containerWidth;\n if (width === undefined) return;\n this.#narrow = false; // let #applySinglePage see a real change and rebuild\n this.#applySinglePage(width);\n }\n void this.#renderCurrent();\n }\n\n /**\n * Resolve one page's raster, for drawing a thumbnail or exporting. Returns null if the page\n * cannot be decoded — the failure is reported through `sourceError` rather than thrown, as it\n * is for the pages the renderer paints.\n */\n getPageImage(index: number): Promise<PageContent | null> {\n return this.#getPage(index);\n }\n\n /** Whether there is a spread after the current one (false on the last). */\n canFlipNext(): boolean {\n return this.#current + 1 < this.#spreads.length;\n }\n\n /** Whether there is a spread before the current one (false on the first). */\n canFlipPrev(): boolean {\n return this.#current > 0;\n }\n\n /** The ceiling `setZoom` clamps to. */\n getMaxZoom(): number {\n return this.#maxZoom;\n }\n\n /** Whether this book's source can produce text — false for image books. */\n canSearch(): boolean {\n return typeof this.#source.getText === 'function';\n }\n\n /** Whether the original document can be downloaded — true for a PDF from a URL or bytes. */\n canDownload(): boolean {\n return typeof this.#source.getDownload === 'function';\n }\n\n /**\n * Whether the book is a document rather than a set of loose images — true for a PDF, false for\n * an `ImageSource`. Document-shaped features (the page rail, the outline) key off this.\n *\n * Inferred from the source implementing document-only capabilities, so a custom source opts in\n * simply by implementing them.\n */\n isDocument(): boolean {\n return typeof this.#source.getOutline === 'function' || typeof this.#source.getText === 'function';\n }\n\n /**\n * Whether this book can be printed: the source must have an original document, and the browser\n * must render PDFs inline. {@link print} loads the file into an offscreen iframe and drives it\n * with `window.print()`, which needs a built-in PDF viewer; Android Chrome has none, so the\n * frame never lays out and the print silently does nothing. `pdfViewerEnabled` reports exactly\n * that capability, so hide the control when it is explicitly false. It is only hidden on that\n * explicit false — a browser too old to report the property (undefined) keeps the button.\n */\n canPrint(): boolean {\n if (typeof this.#source.getDownload !== 'function') return false;\n const nav = this.#container.ownerDocument?.defaultView?.navigator;\n return nav?.pdfViewerEnabled !== false;\n }\n\n /**\n * Print the document.\n *\n * The original file is handed to the browser in an offscreen frame rather than printing the\n * host page: printing the page would capture the toolbar and whatever single spread happens to\n * be on screen, while the browser paginates a PDF properly on its own. Resolves false when\n * there is no original to print — an image book, or a caller-owned pdf.js document.\n */\n async print(): Promise<boolean> {\n const info = await this.getSourceFile();\n const doc = this.#container.ownerDocument;\n if (!info || !doc) return false;\n\n // A cross-origin PDF loads into the frame but the frame is then cross-origin, so calling\n // print() on its window throws and the dialog never opens. Pull it into a same-origin blob so\n // the frame is scriptable — the same fetch the download path uses.\n const { url, revoke } = await this.#sameOriginFile(info, doc);\n\n const frame = doc.createElement('iframe');\n // Offscreen rather than display:none: a hidden frame does not always lay out its document,\n // and an unlaid-out frame has nothing to print.\n frame.style.cssText =\n 'position:fixed;right:0;bottom:0;width:1px;height:1px;opacity:0;border:0;';\n frame.setAttribute('aria-hidden', 'true');\n frame.src = url;\n\n return new Promise<boolean>((resolve) => {\n let settled = false;\n const finish = (ok: boolean): void => {\n if (settled) return;\n settled = true;\n // Outlive the print dialog, which is modal and synchronous in some browsers; removing\n // the frame while it is open cancels the job.\n setTimeout(() => {\n frame.remove();\n if (revoke) URL.revokeObjectURL(url);\n }, 60_000);\n resolve(ok);\n };\n frame.addEventListener('load', () => {\n try {\n const win = frame.contentWindow;\n if (!win) return finish(false);\n win.focus();\n win.print();\n finish(true);\n } catch {\n finish(false); // a cross-origin PDF cannot be driven from here\n }\n });\n frame.addEventListener('error', () => finish(false));\n doc.body.appendChild(frame);\n });\n }\n\n /** Whether the source can supply a table of contents. True for PDFs; the document may still\n * turn out to have no outline, in which case {@link getOutline} resolves empty. */\n canOutline(): boolean {\n return typeof this.#source.getOutline === 'function';\n }\n\n /**\n * The document's table of contents, or an empty list when it has none.\n *\n * Memoized: an outline does not change for the life of the document, and resolving one means\n * following every entry's destination to a page. The toolbar asks for it up front to decide\n * whether to offer the outline panel at all, and the panel itself then gets it for free.\n */\n getOutline(): Promise<OutlineItem[]> {\n if (!this.#outline) {\n const getOutline = this.#source.getOutline;\n this.#outline =\n typeof getOutline === 'function'\n ? Promise.resolve(getOutline.call(this.#source)).catch(() => [])\n : Promise.resolve([]);\n }\n return this.#outline;\n }\n\n /**\n * Save the original document. Resolves to false when the source has nothing to hand over\n * (an image book, or a PDF opened from a caller-owned pdf.js document).\n */\n /**\n * Where the original document lives, if the source has one — a PDF's URL or a blob of its\n * bytes. Null for an image book, or a PDF opened from a caller-owned pdf.js document.\n *\n * Saving and printing both start here: the original is better than anything reassembled from\n * page rasters, and the browser already knows how to paginate it.\n */\n async getSourceFile(): Promise<DownloadInfo | null> {\n const getDownload = this.#source.getDownload;\n if (typeof getDownload !== 'function') return null;\n return (await getDownload.call(this.#source)) ?? null;\n }\n\n async download(): Promise<boolean> {\n const info = await this.getSourceFile();\n if (!info) return false;\n const doc = this.#container.ownerDocument;\n if (!doc) return false;\n // The `download` attribute is honoured only for same-origin (and blob/data) URLs; for a\n // cross-origin one the browser ignores it and just navigates to the file, opening the PDF in\n // a tab instead of saving it. Pulling the file into a same-origin blob forces the save with\n // its filename.\n const { url, revoke } = await this.#sameOriginFile(info, doc);\n const link = doc.createElement('a');\n link.href = url;\n link.download = info.filename;\n link.rel = 'noopener';\n doc.body.appendChild(link);\n link.click();\n link.remove();\n // An object URL is ours to clean up; give the click a tick to start first.\n if (revoke) setTimeout(() => URL.revokeObjectURL(url), 10_000);\n return true;\n }\n\n /**\n * A same-origin URL for the source file, so an `<a download>` saves it and a print frame can be\n * scripted. A file that is already same-origin, or a blob we made ourselves (`revoke`), is\n * returned unchanged; a cross-origin one is fetched into a blob. The server must allow CORS,\n * which it already does — the viewer fetched the same PDF to render it. On a fetch failure the\n * raw URL is returned (opens in a tab, no worse than before).\n */\n async #sameOriginFile(info: DownloadInfo, doc: Document): Promise<{ url: string; revoke: boolean }> {\n if (info.revoke || !this.#isCrossOrigin(info.url, doc)) {\n return { url: info.url, revoke: info.revoke ?? false };\n }\n try {\n const blob = await (await fetch(info.url)).blob();\n return { url: URL.createObjectURL(blob), revoke: true };\n } catch {\n return { url: info.url, revoke: false };\n }\n }\n\n /** Whether `url` resolves to a different origin than the page, so an `<a download>` would be\n * ignored for it. A relative or same-origin URL, or one that fails to parse, counts as same. */\n #isCrossOrigin(url: string, doc: Document): boolean {\n const here = doc.defaultView?.location?.href;\n if (!here) return false;\n try {\n return new URL(url, here).origin !== new URL(here).origin;\n } catch {\n return false;\n }\n }\n\n /**\n * Find `query` in the book's text, case-insensitively.\n *\n * Pages are read on demand and cached by the source, so the first search over a long document\n * costs one text extraction per page and later ones are cheap. Returns at most one hit per\n * page, in page order. Always empty when {@link canSearch} is false.\n */\n async search(query: string, options: { limit?: number } = {}): Promise<SearchHit[]> {\n const getText = this.#source.getText;\n const needle = query.trim().toLowerCase();\n if (typeof getText !== 'function' || needle === '') return [];\n const limit = options.limit ?? 50;\n const hits: SearchHit[] = [];\n for (let page = 0; page < this.#source.pageCount && hits.length < limit; page++) {\n let text: string;\n try {\n text = await getText.call(this.#source, page);\n } catch (error) {\n this.#emitter.emit('sourceError', { index: page, error });\n continue;\n }\n const at = text.toLowerCase().indexOf(needle);\n if (at < 0) continue;\n hits.push({ page, excerpt: excerptAround(text, at, needle.length) });\n }\n return hits;\n }\n\n flipNext(): void {\n this.#stepFlip(1);\n }\n\n flipPrev(): void {\n this.#stepFlip(-1);\n }\n\n /** Turn one spread in `step`'s direction, counted from wherever the book lands. Interrupting\n * commits the running turn first, so this steps on from there — repeat taps keep advancing,\n * and a reversal goes to the neighbour of the spread that just landed. */\n #stepFlip(step: 1 | -1): void {\n this.#requestFlip(() => this.#startFlip(this.#current + step));\n }\n\n flipTo(page: number): void {\n const target = clamp(page, 0, Math.max(0, this.#source.pageCount - 1));\n this.#requestFlip(() => this.#startFlip(this.#spreadIndexForPage(target)));\n }\n\n /** Run a flip now. If a fold is animating, land it instantly first so the new turn starts\n * immediately on this click (thunks read `#current` lazily, so it steps on from where the\n * interrupted turn landed). A drag in flight can't be interrupted this way, so its request\n * is queued and replayed on settle instead. */\n #requestFlip(run: () => void): void {\n if (this.#machine.state === 'idle') {\n run();\n } else if (this.#activeAnim) {\n // This request supersedes anything still waiting: landing the current turn commits it,\n // and that commit drains the queue. Without clearing it first, an older tap would fire\n // from inside the commit and take the book somewhere the reader has since changed their\n // mind about — a reversal would be undone by the forward tap it was meant to replace.\n this.#queuedFlip = null;\n this.#finishActiveAnim(); // lands the current turn → machine back to idle\n run();\n } else {\n this.#queuedFlip = run; // dragging: no animation to cut short, so wait for release\n }\n }\n\n /** Snap the in-flight fold to its end pose and commit it now, cancelling its RAF loop. */\n #finishActiveAnim(): void {\n const anim = this.#activeAnim;\n if (!anim) return;\n this.#activeAnim = null;\n if (this.#raf !== null) {\n cancelAnimationFrame(this.#raf);\n this.#raf = null;\n }\n this.#renderer?.setFlipProgress(anim.toT, anim.direction);\n anim.onDone();\n }\n\n /** After an animation settles, fire whatever flip was requested mid-turn. */\n #drainQueuedFlip(): void {\n const queued = this.#queuedFlip;\n if (!queued) {\n // Nothing further to turn to, so this is where the book comes to rest: a good moment to\n // apply any page content that improved while it was moving.\n this.#flushPendingUpgrades();\n return;\n }\n this.#queuedFlip = null;\n queued();\n }\n\n getZoom(): number {\n return this.#scale;\n }\n\n /** Zoom to `scale` (clamped to [1, zoom.max]), keeping `center` (container-local) fixed. */\n setZoom(scale: number, center?: { x: number; y: number }): void {\n if (!this.#renderer || !this.#zoomEnabled) return;\n this.#cancelPeek(); // a zoom takes over the view; don't leave a peek folding under it\n this.#cancelNudge(); // and don't let a boundary rubber-band fight the zoom's own transform\n const s2 = clamp(scale, 1, this.#maxZoom);\n const m = this.#renderer.measure();\n // Default to the middle of the pages, not of the container: on a lone page those differ, and\n // zooming toward the container centre would drift the page off toward the gutter.\n const box = m.screenAt?.(this.#scale);\n const focal = center ??\n (box\n ? { x: box.x + this.#tx + box.width / 2, y: box.y + this.#ty + box.height / 2 }\n : null) ?? { x: m.containerWidth / 2, y: m.containerHeight / 2 };\n const s1 = this.#scale;\n // Solve for the translate that keeps the focal screen point fixed as s1→s2.\n let tx = s2 === 1 ? 0 : focal.x - (s2 / s1) * (focal.x - this.#tx);\n let ty = s2 === 1 ? 0 : focal.y - (s2 / s1) * (focal.y - this.#ty);\n [tx, ty] = this.#clampPan(tx, ty, s2, m.containerWidth, m.containerHeight);\n this.#scale = s2;\n this.#tx = tx;\n this.#ty = ty;\n this.#renderer.setViewTransform(s2, tx, ty);\n // Crossing in/out of zoom flips who owns vertical panning: browser scroll at rest, us when zoomed.\n if ((s1 > 1) !== (s2 > 1)) this.#applyTouchAction();\n this.#refreshZoomTiles();\n this.#updateCursor(); // crossing in/out of zoom swaps the flip hint for the zoom hint\n // First zoom in: teach panning, since a zoomed page has no visual cue that it can be dragged.\n if (s1 <= 1 && s2 > 1) {\n this.#markLearned('zoom');\n this.#showPanCaption();\n }\n this.#emitter.emit('zoomChanged', { scale: s2 });\n }\n\n resetZoom(): void {\n this.setZoom(1);\n }\n\n /** The resolved reader-facing text (English defaults merged with the `strings` option). Read by\n * the controls and panels so a translation reaches every label and announcement. */\n get strings(): ZineStrings {\n return this.#strings;\n }\n\n /** Whether page-flip sound is enabled for this book (the `sound` option was set). */\n isSoundEnabled(): boolean {\n return this.#soundEnabled;\n }\n\n /** Whether flip sound is currently muted. Always false when sound is not enabled. */\n isSoundMuted(): boolean {\n return this.#soundEnabled && this.#soundMuted;\n }\n\n /** Mute or unmute the flip sound. Remembered even if the sound controller is still loading, and\n * a no-op when `sound` was never enabled. */\n setSoundMuted(muted: boolean): void {\n if (!this.#soundEnabled) return;\n this.#soundMuted = muted;\n this.#sound?.setMuted(muted);\n if (this.#soundPersist) this.#saveSoundMuted(muted);\n // Unmuting a book that started muted is the first time the clip is actually needed: load now.\n if (!muted) void this.#loadSound();\n }\n\n /** Read the reader's remembered mute choice (persist mode), overriding the configured default.\n * Storage can throw in private mode or a sandboxed iframe — fall back to the default silently. */\n #loadSoundMuted(): void {\n try {\n const raw = localStorage.getItem(SOUND_MUTED_STORAGE_KEY);\n if (raw === 'true' || raw === 'false') this.#soundMuted = raw === 'true';\n } catch {\n // No stored choice; keep the configured default.\n }\n }\n\n #saveSoundMuted(muted: boolean): void {\n try {\n localStorage.setItem(SOUND_MUTED_STORAGE_KEY, String(muted));\n } catch {\n // Persisting is best-effort; the in-memory state still holds this session.\n }\n }\n\n /** Hand the browser the gestures we don't use, so a flipbook filling the viewport doesn't trap\n * the page. At rest we only claim horizontal turns, so vertical panning (page scroll) stays the\n * browser's — `pan-y`. Zoomed in, one finger pans the image in both axes, so we take it all. */\n #applyTouchAction(): void {\n this.#container.style.touchAction = this.#scale > 1 ? 'none' : 'pan-y';\n }\n\n #clampPan(tx: number, ty: number, scale: number, w: number, h: number): [number, number] {\n // Bound the pan by where the pages actually are, not by the container. A lone page (a cover,\n // or the back of a book-mode spread) is painted in the middle half, and letterboxing insets\n // the book on any container whose aspect differs; clamping to the container would let the\n // reader drag the page off to one side and stare at the space beside it.\n //\n // `screen` is where the renderer says the pages are under the *current* view, so the offset\n // between it and the translate that produced it converts a wanted translate into a page\n // position. The renderers place a lone page differently (one shifts outside the view scale,\n // one inside), which is why this asks rather than deriving it.\n // Land on whole device pixels. A fractional offset makes every screen pixel a different\n // bilinear blend of the same texels, so as the page slides the strokes of each glyph thicken\n // and thin: the text appears to shimmer and change weight rather than simply move.\n const dpr = typeof devicePixelRatio === 'number' && devicePixelRatio > 0 ? devicePixelRatio : 1;\n // `|| 0` normalises -0, which Object.is and strict deep-equality treat as distinct from 0.\n const snap = (v: number): number => Math.round(v * dpr) / dpr || 0;\n\n // Asked for `scale` rather than read from the current view, because setZoom clamps before it\n // commits and would otherwise be bounded by the scale it is leaving.\n // Falls back to the container, which is what the pages fill on a renderer that does not\n // report a box: keeps a bare Renderer implementation clamped rather than free to pan away.\n const box = this.#renderer?.measure().screenAt?.(scale) ?? {\n x: 0,\n y: 0,\n width: w * scale,\n height: h * scale,\n };\n const [minX, maxX] = axisPanRange(box.x, box.width, w);\n const [minY, maxY] = axisPanRange(box.y, box.height, h);\n return [snap(clamp(tx, minX, maxX)), snap(clamp(ty, minY, maxY))];\n }\n\n on<K extends keyof ZineEventMap>(\n event: K,\n listener: (payload: ZineEventMap[K]) => void,\n ): () => void {\n return this.#emitter.on(event, listener);\n }\n\n destroy(): void {\n this.#destroyed = true;\n if (this.#raf !== null) cancelAnimationFrame(this.#raf);\n this.#activeAnim = null;\n this.#queuedFlip = null;\n this.#clearPendingClickFlip();\n this.#clearIdleNudge();\n this.#cancelPeek();\n this.#cancelNudge();\n this.#clearZoomHint();\n this.#hideCaption();\n this.#sound?.destroy();\n this.#sound = null;\n this.#resizeObserver?.disconnect();\n this.#deepLink?.stop();\n this.#deepLink = null;\n this.#unsubscribeDeepLink?.();\n if (this.#deepLinkEnabled) releaseHash(); // a later book may own it now\n this.#controlsCleanup?.();\n this.#contextMenuCleanup?.();\n this.#dismissLoader();\n this.#a11yCleanup?.();\n this.#unbindWheel?.();\n this.#unbindDblClick?.();\n this.#unbindContextMenu?.();\n this.#unbindCursor?.();\n this.#unbindInput?.();\n if (this.#tileTimer !== null) clearTimeout(this.#tileTimer);\n this.#tileGeneration++; // strand any raster still resolving\n this.#renderer?.destroy();\n this.#renderer = null;\n this.#source.destroy();\n this.#emitter.clear();\n }\n\n #startFlip(targetIndex: number): void {\n if (targetIndex < 0 || targetIndex >= this.#spreads.length || targetIndex === this.#current) {\n return;\n }\n this.#cancelPeek(); // a real turn is starting; drop any peek folding the same leaf\n this.#cancelNudge(); // and any boundary rubber-band still springing\n // `flip` is legal only from idle; otherwise a flip/drag is already in flight.\n if (this.#machine.send('flip') === null) return;\n\n const direction: FlipDirection = targetIndex > this.#current ? 'forward' : 'backward';\n const toPage = this.#leadPage(targetIndex);\n // State is already locked (send('flip') above), so re-entrant flips are rejected\n // even though staging the destination content below is async.\n this.#emitter.emit('flipStart', { from: this.#currentPage, to: toPage });\n this.#playFlipSound(); // an animated turn (click/swipe/arrow/API) is committed once it starts\n // Claimed here, synchronously, so this flip owns the run before #runFlip's first await.\n // Bumping inside #runFlip instead let a superseded flip's continuation claim the newest\n // generation and cancel the flip that replaced it.\n void this.#runFlip(targetIndex, direction, toPage, ++this.#flipGeneration);\n }\n\n async #runFlip(\n targetIndex: number,\n direction: FlipDirection,\n toPage: number,\n generation: number,\n ): Promise<void> {\n // Staging the destination is async, so this can resume after a later flip has already\n // superseded it (interrupting mid-turn does exactly that). Without this check the stale\n // continuation would begin animating its own, now-abandoned target and commit it, undoing\n // the turn the reader actually asked for.\n const toSpread = this.#spreads[targetIndex];\n const toContent: SpreadContent = toSpread\n ? await this.#resolveContent(toSpread)\n : { left: null, right: null };\n if (generation !== this.#flipGeneration) return;\n this.#renderer?.beginFlip(this.#currentContent, toContent, direction, {\n fill: this.#singlePage,\n fit: this.#fit,\n curl: this.#effectiveCurl(),\n anchor: { y: this.#anchorY },\n });\n this.#animateProgress(\n 0,\n 1,\n direction,\n () => this.#commit(targetIndex, toPage, toContent),\n () => this.#announcePage(toPage),\n );\n }\n\n /** Animate the fold from `fromT` to `toT`, easing per-frame, then run `onDone`. */\n #animateProgress(\n fromT: number,\n toT: number,\n direction: FlipDirection,\n onDone: () => void,\n onLead?: () => void,\n ): void {\n const duration = this.#effectiveDuration() * Math.abs(toT - fromT);\n if (duration <= 0) {\n // Reduced motion (or zero-duration): swap without the curl sweep. onDone announces the\n // page itself, so there is no separate lead to fire.\n this.#renderer?.setFlipProgress(toT, direction);\n onDone();\n return;\n }\n // Remembered so a mid-turn flip request can #finishActiveAnim() to land it at once.\n // Object identity also tags this run: if #finishActiveAnim (or a new flip) replaces it,\n // a stale frame that still fires bails instead of re-committing or rescheduling itself.\n const anim = { toT, direction, onDone };\n this.#activeAnim = anim;\n const start = performance.now();\n let ledPage = false;\n const step = (now: number): void => {\n if (this.#activeAnim !== anim) return; // superseded/interrupted → this frame is void\n // Easing lives here; flipProgressToPose stays linear so seeks are deterministic.\n // Same ease for lone and spread so single-page turns feel like the same flipbook, not a\n // stretched dissolve. Curl geometry holds the landed pose through the decelerating tail.\n const raw = Math.min(1, (now - start) / duration);\n const eased = easeInOutQuad(raw);\n // The ease decelerates hard into the end, so once the pose crosses the lead threshold the\n // fold already reads as landed. Announce the page number there — once — while the fold plays\n // on to its full duration: the number leads the near-still tail without shortening the flip.\n if (onLead && !ledPage) {\n const lead = this.#singlePage ? PAGE_LEAD_LONE : PAGE_LEAD;\n if (eased >= lead) {\n ledPage = true;\n onLead();\n }\n }\n // `raw < 1` (not `raw >= 1`) so a finished — or NaN, under a rAF stub that omits the\n // timestamp — clock still lands rather than looping forever.\n if (raw < 1) {\n this.#renderer?.setFlipProgress(fromT + (toT - fromT) * eased, direction);\n this.#raf = requestAnimationFrame(step);\n } else {\n this.#renderer?.setFlipProgress(toT, direction);\n this.#raf = null;\n this.#activeAnim = null;\n onDone();\n }\n };\n this.#raf = requestAnimationFrame(step);\n }\n\n /** The page-facing half of a landing: current page, a11y text, and `pageChanged`. Fired from the\n * animation lead a little before the fold finishes so the number updates on time, then guarded\n * so the end-of-flip #commit does not repeat it. A jump with no animation just calls it inline. */\n #announcePage(toPage: number): void {\n if (this.#currentPage === toPage) return; // already led this turn (or nothing moved)\n this.#currentPage = toPage;\n this.#announce();\n this.#emitter.emit('pageChanged', { page: toPage });\n }\n\n #commit(targetIndex: number, toPage: number, toContent: SpreadContent): void {\n const spread = this.#spreads[targetIndex];\n this.#current = targetIndex;\n this.#currentContent = toContent;\n // renderSpread paints the landed spread and clears the turning leaf.\n if (spread) this.#paintSpread(spread, toContent);\n this.#prefetchWindow();\n this.#machine.send('settle');\n // Stamp the landing so a dead-edge double-click that arrives on its heels reads as the tail of\n // a flipping streak rather than a zoom (see #zoomAt).\n this.#lastFlipAt = performance.now();\n // No-op if the animation already led the number; otherwise (reduced motion, a canceled lead,\n // or an interrupt landing before the threshold) this is where it lands.\n this.#announcePage(toPage);\n this.#markLearned('turn'); // a page has turned — the peek/idle-nudge have taught their lesson\n this.#updateCursor(); // the new spread may have reached an end, killing a flip zone under the pointer\n this.#emitter.emit('flipEnd', { page: toPage });\n this.#drainQueuedFlip();\n }\n\n #leadPage(spreadIndex: number): number {\n const spread = this.#spreads[spreadIndex];\n if (!spread) return this.#currentPage;\n const pages = [spread.left, spread.right].filter((p): p is number => p !== null);\n return pages.length > 0 ? Math.min(...pages) : this.#currentPage;\n }\n\n #onDragStart(clientX: number, clientY: number): void {\n if (this.#pinching || !this.#renderer) return;\n this.#cancelNudge(); // a new press reclaims the view from a boundary rubber-band still springing\n this.#clearPendingClickFlip(); // a new press cancels a click-flip still waiting out its window\n // Zoomed in → a drag pans; at scale 1 → a corner drag flips (§9 mode switch).\n if (this.#scale > 1) {\n if (this.#machine.send('panStart') === null) return;\n this.#pan = { baseTx: this.#tx, baseTy: this.#ty };\n return;\n }\n const b = this.#contentRect();\n const point = this.#toBookPoint(clientX, clientY);\n // Recorded before the busy check below: a tap landing mid-flip is still a real tap, and\n // on release it decides which way to turn. Leaving the previous press in place made that\n // release repeat the last flip's direction instead of honouring the side just tapped.\n this.#press = point; // remembered for tap classification / click-to-flip zone\n // Grabbable along the whole outer band of each side edge, not just the corners: a drag\n // from the vertical middle should peel and follow the finger too, the way a corner does.\n // The fold then anchors at the grabbed height, so mid-edge grabs curl from the middle.\n const edgeBand = Math.min(b.width, b.height) * CORNER_FRACTION;\n const insidePage = point.x >= 0 && point.x <= b.width && point.y >= 0 && point.y <= b.height;\n const nearSideEdge = point.x <= edgeBand || point.x >= b.width - edgeBand;\n const wantsGrab = insidePage && nearSideEdge;\n // A fold may already be mid-flight when this press lands.\n if (this.#machine.state !== 'idle') {\n // Only an edge grab that can land the running turn takes it over. A live finger drag has\n // no animation to cut short (#activeAnim null), and a non-grab press (centre tap, or an\n // edge *tap* handled on release) must leave the animation to play out — so both bail here.\n // A flip's async content-staging also has #activeAnim null, so that window bails too.\n if (!this.#activeAnim || !wantsGrab) {\n this.#pendingGrab = null;\n return;\n }\n // Commit the running turn now so this grab peels straight on from where the book lands,\n // exactly the way repeated taps interrupt-and-advance. #current updates before we target.\n this.#finishActiveAnim();\n }\n if (!wantsGrab) {\n return; // not near a side edge — no drag; a tap here may still click-to-flip on release\n }\n this.#anchorY = clamp(point.y / b.height, 0, 1); // fold anchors at the grabbed point\n // Arm a potential drag; it only becomes a real flip once the pointer moves\n // (so an edge *tap* never fires a spurious flipStart). The right edge\n // turns the page forward in LTR; RTL mirrors it.\n const rightSide = point.x > b.width / 2;\n const forward = this.#direction === 'rtl' ? !rightSide : rightSide;\n const targetIndex = this.#current + (forward ? 1 : -1);\n if (targetIndex < 0 || targetIndex >= this.#spreads.length) return;\n this.#pendingGrab = {\n direction: forward ? 'forward' : 'backward',\n targetIndex,\n toPage: this.#leadPage(targetIndex),\n width: b.width,\n };\n }\n\n /** Turn an armed corner press into a live drag flip once the pointer has moved. */\n #promoteGrab(): void {\n const grab = this.#pendingGrab;\n if (!grab) return;\n this.#pendingGrab = null;\n if (this.#machine.send('grab') === null) return;\n this.#drag = {\n direction: grab.direction,\n targetIndex: grab.targetIndex,\n toPage: grab.toPage,\n toContent: { left: null, right: null },\n width: grab.width,\n t: 0,\n };\n this.#emitter.emit('flipStart', { from: this.#currentPage, to: grab.toPage });\n void this.#stageDrag(grab.targetIndex, grab.direction);\n }\n\n async #stageDrag(targetIndex: number, direction: FlipDirection): Promise<void> {\n const toSpread = this.#spreads[targetIndex];\n const toContent: SpreadContent = toSpread\n ? await this.#resolveContent(toSpread)\n : { left: null, right: null };\n // The drag may have ended (or retargeted) while content was resolving.\n if (this.#drag?.targetIndex !== targetIndex) return;\n this.#drag.toContent = toContent;\n this.#renderer?.beginFlip(this.#currentContent, toContent, direction, {\n fill: this.#singlePage,\n fit: this.#fit,\n curl: this.#effectiveCurl(),\n anchor: { y: this.#anchorY },\n });\n this.#renderer?.setFlipProgress(this.#drag.t, direction);\n }\n\n #onDragMove(dx: number, dy: number): void {\n if (this.#pinching) return;\n if (this.#pan) {\n if (!this.#renderer) return;\n const m = this.#renderer.measure();\n const [tx, ty] = this.#clampPan(\n this.#pan.baseTx + dx,\n this.#pan.baseTy + dy,\n this.#scale,\n m.containerWidth,\n m.containerHeight,\n );\n this.#tx = tx;\n this.#ty = ty;\n this.#renderer.setViewTransform(this.#scale, tx, ty);\n this.#refreshZoomTiles();\n this.#markLearned('pan'); // the reader is panning — the caption has served its purpose\n this.#hideCaption();\n return;\n }\n // An armed corner press becomes a real flip once it moves past the threshold.\n if (this.#pendingGrab && Math.hypot(dx, dy) > DRAG_THRESHOLD) {\n this.#promoteGrab();\n }\n const drag = this.#drag;\n if (!drag) return;\n const signed = drag.direction === 'forward' ? -dx : dx;\n drag.t = clamp(signed / drag.width, 0, 1);\n this.#renderer?.setFlipProgress(drag.t, drag.direction);\n }\n\n #onDragEnd(gesture: GestureEnd): void {\n if (this.#pinching) return;\n if (this.#pan) {\n this.#pan = null;\n this.#machine.send('panEnd');\n this.#updateCursor(); // pan over → re-derive the resting hint (no move fires on release)\n return;\n }\n const drag = this.#drag;\n if (!drag) {\n // Never became a grab (the press was not on a side edge, so no live peel armed). A fast\n // horizontal flick from anywhere still turns the page; otherwise it was a tap.\n this.#pendingGrab = null;\n if (!this.#swipeFlip(gesture)) this.#maybeClickFlip(gesture);\n return;\n }\n this.#drag = null;\n this.#machine.send('release');\n\n // A flick at release throws the page, and its direction is the release velocity (vx), not the\n // net displacement: you can peel a page well past halfway and then flick it back toward the\n // edge to let go — the mobile counterpart of dragging back to cancel on desktop. An onward\n // throw commits even from short of halfway; a back-throw cancels even from past it. Position\n // (drag.t) decides only when the release is not a horizontal flick.\n const horizFlick = gesture.swipe && Math.abs(gesture.vx) > Math.abs(gesture.vy);\n const onward = drag.direction === 'forward' ? gesture.vx < 0 : gesture.vx > 0;\n const flungOnward = horizFlick && onward;\n const flungBack = horizFlick && !onward;\n if (flungOnward || (drag.t >= 0.5 && !flungBack)) {\n this.#playFlipSound(); // the drag committed to a turn (flung on, or released past halfway)\n this.#animateProgress(\n drag.t,\n 1,\n drag.direction,\n () => this.#commit(drag.targetIndex, drag.toPage, drag.toContent),\n () => this.#announcePage(drag.toPage),\n );\n } else {\n this.#animateProgress(drag.t, 0, drag.direction, () => this.#cancelFlip());\n }\n }\n\n /** A fast horizontal flick that started away from the side edges (so no live peel was armed):\n * turn the page in the flick's direction. Returns whether it consumed the gesture, so the caller\n * skips click-to-flip. Zoomed in the same gesture pans, so this only applies at scale 1. */\n #swipeFlip(gesture: GestureEnd): boolean {\n if (this.#scale > 1) return false;\n // A flick, and predominantly horizontal — a vertical flick (page scroll intent) is not a turn.\n if (!gesture.swipe || Math.abs(gesture.dx) <= Math.abs(gesture.dy)) return false;\n // In LTR a leftward flick (dx < 0) turns forward; RTL and the reading direction mirror it.\n const forward = this.#direction === 'rtl' ? gesture.dx > 0 : gesture.dx < 0;\n const direction: FlipDirection = forward ? 'forward' : 'backward';\n if (!this.#canFlip(direction)) {\n this.#playEdgeNudge(direction); // swiped past the end: rubber-band toward the edge\n return false;\n }\n const step = forward ? 1 : -1;\n this.#requestFlip(() => this.#startFlip(this.#current + step));\n return true;\n }\n\n #maybeClickFlip(gesture: GestureEnd): void {\n if (this.#clickToFlip === 'off' || this.#scale > 1 || !this.#press) return;\n // A tap barely moves; anything more was a drag we already ignored.\n if (Math.abs(gesture.dx) > DRAG_THRESHOLD || Math.abs(gesture.dy) > DRAG_THRESHOLD) return;\n const direction = this.#clickFlipDirection(this.#press);\n if (!direction) return;\n if (!this.#canFlip(direction)) {\n this.#playEdgeNudge(direction); // nowhere to turn: rubber-band toward the edge instead\n return;\n }\n // Anchor the fold at the tapped height (cone/leaf/flick curl from where you tap).\n this.#anchorY = clamp(this.#press.y / this.#contentRect().height, 0, 1);\n // Read `#current` lazily: interrupting commits the running turn first, so this steps on\n // from wherever the book actually landed — one spread back from there is the neighbour\n // the reader is looking at, not the spread they tapped on two turns ago.\n const step = direction === 'forward' ? 1 : -1;\n const flip = (): void => this.#startFlip(this.#current + step);\n this.#clearPendingClickFlip();\n if (this.#clickFlipDelayValue <= 0) {\n // No double-click competing here → flip right away (or queue if mid-turn).\n this.#requestFlip(flip);\n return;\n }\n // Wait out the window; a double-click (zoom) cancels this.\n this.#pendingClickTimer = setTimeout(() => {\n this.#pendingClickTimer = null;\n this.#requestFlip(flip);\n }, this.#clickFlipDelayValue);\n }\n\n /** Where the current spread is actually drawn (lone pages centered), as the renderer reports\n * it — the region taps/drags map into. Falls back to book, then the full container. */\n #contentRect(): { x: number; y: number; width: number; height: number } {\n const m = this.#renderer!.measure();\n return m.content ?? m.book ?? { x: 0, y: 0, width: m.containerWidth, height: m.containerHeight };\n }\n\n /** A viewport point in book-local px: relative to the drawn page, not the container. The two\n * differ whenever the book is letterboxed, so every hit test against a zone or corner has to\n * come through here — measuring from the container edge puts the zones off by the bar width. */\n #toBookPoint(clientX: number, clientY: number): { x: number; y: number } {\n const rect = this.#container.getBoundingClientRect();\n const b = this.#contentRect();\n return { x: clientX - rect.left - b.x, y: clientY - rect.top - b.y };\n }\n\n /** Which way a tap at `point` (book-local, via {@link #toBookPoint}) turns the page, or null\n * for a dead zone. */\n #clickFlipDirection(point: { x: number; y: number }): FlipDirection | null {\n if (!this.#renderer) return null;\n const bookWidth = this.#contentRect().width;\n let side: 'left' | 'right' | null;\n if (this.#clickToFlip === 'half') {\n side = point.x > bookWidth / 2 ? 'right' : 'left';\n } else if (point.x <= this.#clickZoneSize) {\n side = 'left';\n } else if (point.x >= bookWidth - this.#clickZoneSize) {\n side = 'right';\n } else {\n side = null; // center dead zone in 'edge' mode\n }\n if (!side) return null;\n const forwardSide = this.#direction === 'rtl' ? 'left' : 'right';\n return side === forwardSide ? 'forward' : 'backward';\n }\n\n /** Whether a flip in `direction` has somewhere to land: false at the first spread going back\n * and the last spread going forward. A flip zone that fails this is dead, so a double-click\n * there should zoom rather than defer to a turn that can never happen. */\n #canFlip(direction: FlipDirection): boolean {\n const target = this.#current + (direction === 'forward' ? 1 : -1);\n return target >= 0 && target < this.#spreads.length;\n }\n\n /** Watch the pointer at rest and hint what a press there would do through the cursor (mouse only;\n * touch never fires these). A separate listener from the gesture recognizer so it can be removed\n * on its own; both read the same `pointermove` stream. */\n #bindCursorHints(): void {\n const move = (e: Event): void => {\n const p = e as PointerEvent;\n this.#pointerClient = { x: p.clientX, y: p.clientY };\n this.#updateCursor();\n };\n const leave = (): void => {\n this.#pointerClient = null;\n this.#updateCursor();\n };\n this.#container.addEventListener('pointermove', move);\n this.#container.addEventListener('pointerleave', leave);\n this.#unbindCursor = (): void => {\n this.#container.removeEventListener('pointermove', move);\n this.#container.removeEventListener('pointerleave', leave);\n };\n }\n\n /** Re-derive the cursor from the last pointer position. Called both on movement and after a page\n * turn, zoom, or resize changes what a press at the same spot would do without the mouse moving. */\n #updateCursor(): void {\n if (!this.#cursorHints || !this.#renderer) return;\n let cursor = '';\n if (this.#drag || this.#pan) {\n cursor = 'grabbing'; // a peel or pan is in flight — the hand has hold of the page\n } else if (this.#pointerClient) {\n cursor = this.#cursorFor(this.#pointerClient.x, this.#pointerClient.y);\n }\n this.#container.style.cursor = cursor;\n }\n\n /**\n * The cursor for a resting pointer at client (x, y): a hint for what a press would do, read from\n * the same zone classifiers the gestures use. Returns '' (the default arrow) off the drawn page\n * or where a press does nothing.\n *\n * Zoomed in, the whole page pans, so one steady 'grab' covers it — no flicker as the reader moves,\n * and it never lies about a double-click that would zoom back out (grab, not a stale zoom-in). At\n * rest there are two hints: 'pointer' over a live click-to-flip zone, and 'zoom-in' everywhere else\n * a double-click would zoom (including the edge peel band, so a sweep across the page stays calm).\n * A drag peels regardless; it just is not called out with its own resting cursor.\n */\n #cursorFor(clientX: number, clientY: number): string {\n if (!this.#renderer) return '';\n const b = this.#contentRect();\n const p = this.#toBookPoint(clientX, clientY);\n if (p.x < 0 || p.y < 0 || p.x > b.width || p.y > b.height) return ''; // a letterbox bar\n // Zoomed in the page pans everywhere: a drag is the standing gesture, so hint it with grab.\n if (this.#scale > 1) return 'grab';\n // At rest a click turns the page in the flip zones...\n const flipDir = this.#clickFlipDirection(p);\n if (flipDir && this.#canFlip(flipDir)) return 'pointer';\n // ...and elsewhere a double-click zooms in (from rest the next configured level is always up).\n if (this.#zoomEnabled && this.#doubleClickLevels !== null) return 'zoom-in';\n return '';\n }\n\n // --- Discoverability hints (corner peek, idle nudge, first-zoom caption) ---\n\n /** Read the learned flags from localStorage (persist mode). Storage can throw in private mode or a\n * sandboxed iframe, and a corrupt value is not worth crashing over — either way, learn nothing. */\n #loadLearned(): void {\n try {\n const raw = localStorage.getItem(HINTS_STORAGE_KEY);\n if (!raw) return;\n const saved = JSON.parse(raw) as Partial<Learned>;\n this.#learned = {\n turn: saved.turn === true,\n zoom: saved.zoom === true,\n pan: saved.pan === true,\n };\n } catch {\n // No stored history; the reader is simply taught fresh.\n }\n }\n\n /** Record that the reader has performed a gesture, so the hint teaching it stays quiet from now on\n * (and across visits under persist). Idempotent; tears down whatever that signal silences. */\n #markLearned(signal: 'turn' | 'zoom' | 'pan'): void {\n if (this.#learned[signal]) return;\n this.#learned[signal] = true;\n if (signal === 'turn') this.#clearIdleNudge(); // no more peeks once a page has been turned\n if (signal === 'zoom') this.#clearZoomHint(); // reader zoomed; a pending zoom caption is moot\n if (this.#hintsPersist) {\n try {\n localStorage.setItem(HINTS_STORAGE_KEY, JSON.stringify(this.#learned));\n } catch {\n // Persisting is best-effort; the in-memory flag still silences the hint this session.\n }\n }\n }\n\n /** Arm the one-shot idle nudge: if the reader turns no page within IDLE_NUDGE_MS, replay the peek\n * once. Any real turn clears the timer first (see #markLearned('turn')). */\n #armIdleNudge(): void {\n if (!this.#hintsEnabled || this.#reducedMotion || this.#learned.turn) return;\n this.#clearIdleNudge();\n this.#idleTimer = setTimeout(() => {\n this.#idleTimer = null;\n this.#playPeek();\n }, IDLE_NUDGE_MS);\n }\n\n #clearIdleNudge(): void {\n if (this.#idleTimer !== null) {\n clearTimeout(this.#idleTimer);\n this.#idleTimer = null;\n }\n }\n\n /**\n * The corner peek: lift the leading page's outer edge to PEEK_PROGRESS and settle it back, once,\n * without committing a turn. A wordless \"this page turns\" cue that reaches touch and mouse alike.\n *\n * Its own rAF loop, not #animateProgress — that one is wired to #commit and the flip generation.\n * Tagged by #peekAnim so a real flip (or destroy) cancels a peek in flight; the leaf is repainted\n * flat at the end so nothing is left half-folded.\n */\n async #playPeek(): Promise<void> {\n if (\n !this.#hintsEnabled ||\n this.#reducedMotion ||\n this.#learned.turn ||\n !this.#renderer ||\n typeof requestAnimationFrame !== 'function' || // no way to animate; skip the cosmetic cue\n this.#machine.state !== 'idle' // a real flip/drag/pan owns the leaf; don't fight it\n ) {\n return;\n }\n const targetIndex = this.#current + 1;\n const toSpread = this.#spreads[targetIndex];\n if (!toSpread) return; // nothing ahead to peek toward (single spread, or already at the end)\n const toContent = await this.#resolveContent(toSpread);\n // Guard the async gap: a turn, a zoom, a teardown, or a second peek may have intervened.\n if (this.#destroyed || this.#learned.turn || this.#machine.state !== 'idle' || this.#peekAnim) {\n return;\n }\n // Anchor the fold at the bottom-outer corner, the natural place a thumb lifts a page.\n this.#anchorY = 1;\n this.#renderer.beginFlip(this.#currentContent, toContent, 'forward', {\n fill: this.#singlePage,\n fit: this.#fit,\n curl: this.#effectiveCurl(),\n anchor: { y: this.#anchorY },\n });\n const token = {};\n this.#peekAnim = token;\n const start = performance.now();\n const total = PEEK_LEG_MS * 2;\n // Read the clock from performance.now(), not the rAF timestamp arg: it is what drives the two\n // legs, and it keeps the loop working under a rAF stub that omits the timestamp.\n const step = (): void => {\n if (this.#peekAnim !== token) return; // superseded/cancelled → this frame is void\n const raw = Math.min(1, (performance.now() - start) / total);\n // Out to PEEK_PROGRESS by the midpoint, back to 0 by the end; a soft sine so it eases at both\n // the lift and the settle rather than snapping.\n const t = PEEK_PROGRESS * Math.sin(raw * Math.PI);\n this.#renderer?.setFlipProgress(Math.max(0, t), 'forward');\n if (raw < 1) {\n this.#raf = requestAnimationFrame(step);\n } else {\n this.#raf = null;\n this.#peekAnim = null;\n this.#repaintRestingSpread(); // clear the turning leaf, back to a flat spread\n }\n };\n this.#raf = requestAnimationFrame(step);\n }\n\n /** Stop a peek mid-play (a real gesture is taking over) and leave the spread flat. */\n #cancelPeek(): void {\n if (!this.#peekAnim) return;\n this.#peekAnim = null;\n if (this.#raf !== null) {\n cancelAnimationFrame(this.#raf);\n this.#raf = null;\n }\n this.#repaintRestingSpread();\n }\n\n /** Repaint the current spread flat (no turning leaf), used to land a finished/cancelled peek. */\n #repaintRestingSpread(): void {\n const spread = this.#spreads[this.#current];\n if (spread && this.#machine.state === 'idle') this.#paintSpread(spread, this.#currentContent);\n }\n\n /**\n * The boundary rubber-band: on a tap/swipe that would turn the page but has nowhere to land (the\n * last spread going forward, the first going back), slide the whole spread a little toward that\n * edge and spring it back. A wordless \"this is the end\" that reads on touch and mouse alike.\n *\n * A view-transform animation (setViewTransform), not a fold — there is no next page to curl toward\n * at a boundary. Its own rAF loop, tagged by #nudgeAnim so a real gesture cancels it mid-spring.\n */\n #playEdgeNudge(direction: FlipDirection): void {\n if (\n !this.#renderer ||\n typeof requestAnimationFrame !== 'function' || // no way to animate; skip the cosmetic cue\n this.#reducedMotion ||\n this.#machine.state !== 'idle' || // a flip/drag owns the view; don't fight it\n this.#scale > 1 || // zoomed: the transform is the reader's pan, not ours to move\n this.#nudgeAnim // one nudge at a time\n ) {\n return;\n }\n // Slide toward the edge the reader tried to reach: forward turns off the right in LTR, so the\n // spread travels left (negative x); RTL and backward mirror it.\n const forward = direction === 'forward';\n const toRightEdge = this.#direction === 'rtl' ? !forward : forward;\n const sign = toRightEdge ? -1 : 1;\n const token = {};\n this.#nudgeAnim = token;\n const start = performance.now();\n const step = (): void => {\n if (this.#nudgeAnim !== token) return; // superseded/cancelled → this frame is void\n const raw = Math.min(1, (performance.now() - start) / EDGE_NUDGE_MS);\n // Out to EDGE_NUDGE_PX at the midpoint, back to 0 by the end — a soft sine springs at both ends.\n const offset = EDGE_NUDGE_PX * Math.sin(raw * Math.PI);\n this.#renderer?.setViewTransform(1, sign * offset, 0);\n if (raw < 1) {\n this.#raf = requestAnimationFrame(step);\n } else {\n this.#raf = null;\n this.#nudgeAnim = null;\n this.#renderer?.setViewTransform(1, 0, 0); // land exactly on center\n }\n };\n this.#raf = requestAnimationFrame(step);\n }\n\n /** Stop a boundary nudge mid-spring (a real gesture is taking over) and reset the view to center. */\n #cancelNudge(): void {\n if (!this.#nudgeAnim) return;\n this.#nudgeAnim = null;\n if (this.#raf !== null) {\n cancelAnimationFrame(this.#raf);\n this.#raf = null;\n }\n this.#renderer?.setViewTransform(1, 0, 0);\n }\n\n /** Show the one-time \"drag to move\" caption over the book on the first zoom. Self-contained inline\n * styles: the controls stylesheet is absent when `controls: false`, and this hint must still work.\n * A dark toast pill with light text reads on any page background and either theme. */\n #showPanCaption(): void {\n if (this.#learned.pan) return;\n this.#showCaption(this.#strings.panHint); // panning has no visual analog, so this one hint uses words\n }\n\n /** Show a transient caption pill over the book, replacing any caption already up. Self-contained\n * inline styles: the controls stylesheet is absent when `controls: false`, and a hint must still\n * work. Guarded by #hintsEnabled so `hints: false` shows nothing. */\n #showCaption(text: string): void {\n if (!this.#hintsEnabled) return;\n const doc = this.#container.ownerDocument;\n if (!doc || typeof this.#container.appendChild !== 'function') return;\n this.#hideCaption(); // one pill at a time; drop any caption (and its timer) already showing\n const el = doc.createElement('div');\n el.className = 'zine-hint-caption';\n el.setAttribute('aria-hidden', 'true'); // decorative; screen readers get the gesture via a11y text\n el.textContent = text;\n el.style.cssText =\n 'position:absolute;left:50%;bottom:16px;transform:translateX(-50%);z-index:3;' +\n 'pointer-events:none;padding:6px 12px;border-radius:999px;font:500 13px/1.2 system-ui,sans-serif;' +\n 'color:#fff;background:rgba(24,24,27,0.82);box-shadow:0 1px 4px rgba(0,0,0,0.35);' +\n 'white-space:nowrap;opacity:0;';\n const canAnimate = !this.#reducedMotion && typeof requestAnimationFrame === 'function';\n if (canAnimate) el.style.transition = 'opacity 200ms ease';\n this.#container.appendChild(el);\n this.#captionEl = el;\n if (canAnimate) {\n // Next frame so the initial opacity:0 is committed before the transition to 1.\n requestAnimationFrame(() => {\n if (this.#captionEl === el) el.style.opacity = '1';\n });\n } else {\n el.style.opacity = '1'; // no fade available — just show it\n }\n this.#captionTimer = setTimeout(() => this.#hideCaption(), CAPTION_MS);\n }\n\n /** Remove the caption (on the gesture it taught, its timeout, or teardown). */\n #hideCaption(): void {\n if (this.#captionTimer !== null) {\n clearTimeout(this.#captionTimer);\n this.#captionTimer = null;\n }\n this.#captionEl?.remove();\n this.#captionEl = null;\n }\n\n /** After a lone click lands where only a double-click would do anything (a dead zone at rest),\n * wait out the pairing window; if no second click pairs, the reader tried to act and nothing\n * happened — hint how to zoom. Mouse-only (touch has no double-click), once a session, and never\n * once zoom is learned. A real double-click clears this (it zooms, which teaches zoom directly). */\n #maybeArmZoomHint(clientX: number, clientY: number): void {\n if (!this.#hintsEnabled || this.#learned.zoom || this.#zoomHintShown) return;\n if (this.#lastPointerType !== 'mouse') return;\n // Only where a single click did nothing but a double-click would zoom (dead zone, scale 1, zoom\n // on). #cursorFor already folds in letterbox, flip-zone, and scale arbitration.\n if (this.#cursorFor(clientX, clientY) !== 'zoom-in') return;\n // Not on the tail of a rapid flipping streak — those dead-zone clicks are turning, not zooming.\n if (this.#machine.state === 'animating' || performance.now() - this.#lastFlipAt <= FLIP_STREAK_MS) {\n return;\n }\n this.#clearZoomHint();\n this.#zoomHintTimer = setTimeout(() => {\n this.#zoomHintTimer = null;\n if (this.#learned.zoom || this.#zoomHintShown) return; // a zoom landed while we waited\n this.#zoomHintShown = true;\n this.#showCaption(this.#zoomHintText());\n }, DOUBLE_CLICK_MS);\n }\n\n /** The zoom caption's text, naming whichever zoom gestures are actually enabled. Double-click is\n * always one (this fires only where a double-click would zoom); Ctrl/⌘-scroll is added when wheel\n * zoom is on, with the platform's modifier. */\n #zoomHintText(): string {\n const nav = this.#container.ownerDocument?.defaultView?.navigator;\n const mac = /Mac|iPhone|iPad/.test(nav?.platform ?? '');\n return this.#strings.zoomHint({\n doubleClick: this.#doubleClickLevels !== null,\n wheel: this.#wheelZoom,\n mac,\n });\n }\n\n #clearZoomHint(): void {\n if (this.#zoomHintTimer !== null) {\n clearTimeout(this.#zoomHintTimer);\n this.#zoomHintTimer = null;\n }\n }\n\n #clearPendingClickFlip(): void {\n if (this.#pendingClickTimer !== null) {\n clearTimeout(this.#pendingClickTimer);\n this.#pendingClickTimer = null;\n }\n }\n\n #cancelFlip(): void {\n const spread = this.#spreads[this.#current];\n if (spread) this.#paintSpread(spread, this.#currentContent);\n this.#machine.send('settle');\n this.#updateCursor(); // the snapped-back spread may have changed the hint under a resting pointer\n this.#emitter.emit('flipEnd', { page: this.#currentPage });\n this.#drainQueuedFlip();\n }\n\n #onPinchStart(): void {\n // A second finger abandons any single-pointer gesture in flight.\n this.#pendingGrab = null;\n this.#clearPendingClickFlip();\n if (this.#drag) {\n this.#drag = null;\n this.#machine.send('release');\n this.#machine.send('settle');\n const spread = this.#spreads[this.#current];\n if (spread) this.#paintSpread(spread, this.#currentContent);\n } else if (this.#pan) {\n this.#pan = null;\n this.#machine.send('panEnd');\n }\n this.#pinching = true;\n this.#pinchBaseScale = this.#scale;\n }\n\n #onPinchMove(centerX: number, centerY: number, scale: number): void {\n if (!this.#pinching) return;\n const rect = this.#container.getBoundingClientRect();\n this.setZoom(this.#pinchBaseScale * scale, { x: centerX - rect.left, y: centerY - rect.top });\n }\n\n #onPinchEnd(): void {\n this.#pinching = false;\n }\n\n /** Swallow the right-click menu over the book, when the consumer asked for that. */\n #bindContextMenu(): void {\n if (!this.#disableContextMenu) return;\n const onContextMenu = (event: Event): void => event.preventDefault();\n this.#container.addEventListener('contextmenu', onContextMenu);\n this.#unbindContextMenu = () =>\n this.#container.removeEventListener('contextmenu', onContextMenu);\n }\n\n #bindWheelZoom(): void {\n const onWheel = (event: WheelEvent): void => {\n // Ctrl/⌘ + wheel only — this is also what a desktop trackpad pinch emits.\n if (!this.#wheelZoom || !this.#zoomEnabled || !(event.ctrlKey || event.metaKey)) return;\n // Override the browser's native page zoom.\n event.preventDefault();\n const rect = this.#container.getBoundingClientRect();\n const factor = Math.exp(-event.deltaY * WHEEL_ZOOM_SENSITIVITY);\n this.setZoom(this.#scale * factor, {\n x: event.clientX - rect.left,\n y: event.clientY - rect.top,\n });\n };\n this.#container.addEventListener('wheel', onWheel as EventListener, { passive: false });\n this.#unbindWheel = () =>\n this.#container.removeEventListener('wheel', onWheel as EventListener);\n }\n\n #bindDoubleClickZoom(): void {\n // The library pairs clicks itself rather than trusting the browser's `dblclick`. The native\n // event drops out on rapid streaks — four fast clicks in one spot fire only a single\n // `dblclick`, because the browser resets its click counter mid-streak (often when the first\n // zoom shifts the target under the pointer). Detecting pairs from raw `click` events makes\n // every second click of a streak zoom (or cycle), so continuous clicks keep toggling.\n // Click carries no pointerType; capture it from the preceding pointerdown so the zoom hint can\n // stay mouse-only (touch has no double-click).\n const onDown = (event: PointerEvent): void => {\n this.#lastPointerType = event.pointerType;\n };\n const onClick = (event: MouseEvent): void => {\n const levels = this.#doubleClickLevels;\n if (!this.#zoomEnabled || levels === null || !this.#renderer) return;\n const t = performance.now();\n const prev = this.#lastClick;\n const paired =\n prev !== null &&\n t - prev.t <= DOUBLE_CLICK_MS &&\n Math.hypot(event.clientX - prev.x, event.clientY - prev.y) <= DOUBLE_CLICK_MOVE;\n if (!paired) {\n // First click of a potential pair: remember it and wait for a second.\n this.#lastClick = { t, x: event.clientX, y: event.clientY };\n // A lone click in a dead zone is a reader who tried something and got nothing: hint how to\n // zoom, unless the second click of a pair arrives first (a real zoom teaches it directly).\n this.#maybeArmZoomHint(event.clientX, event.clientY);\n return;\n }\n // Second click within the window → a double-click. Consume it so the next click starts a\n // fresh pair rather than chaining a third click into another zoom.\n this.#lastClick = null;\n this.#clearZoomHint(); // this pair is a real double-click; drop the pending lone-click hint\n this.#zoomAt(event.clientX, event.clientY, levels);\n };\n this.#container.addEventListener('pointerdown', onDown as EventListener);\n this.#container.addEventListener('click', onClick as EventListener);\n this.#unbindDblClick = () => {\n this.#container.removeEventListener('pointerdown', onDown as EventListener);\n this.#container.removeEventListener('click', onClick as EventListener);\n };\n }\n\n /** Cycle to the next configured zoom level, keeping the clicked point fixed. Honors the\n * flip-zone arbitration at scale 1 so an edge double-click still turns the page there, and\n * suppresses zoom on the tail of a rapid flipping streak (see below). */\n #zoomAt(clientX: number, clientY: number, levels: number[]): void {\n // At scale 1 a double-click inside a live flip zone belongs to click-to-flip, not zoom,\n // unless we're honoring double-click zoom there.\n if (this.#scale <= 1 && this.#clickToFlip !== 'off' && !this.#honorDoubleClickInFlipZone) {\n const local = this.#toBookPoint(clientX, clientY);\n const dir = this.#clickFlipDirection(local);\n // Yield to a zone that can actually turn: click-to-flip owns it, not zoom.\n if (dir !== null && this.#canFlip(dir)) return;\n // Otherwise — a dead edge at the book's ends, or the centre dead zone. That normally zooms,\n // but if the reader is mid-streak (a flip still folding, or one that just landed) the pair is\n // the tail of rapid flipping, not a zoom: swallow it. This is the cover/lone-page glitch —\n // the flip zone sits mid-screen there, so the first click turns the page and the second lands\n // in the centre of the spread it flipped to. A deliberate zoom comes once the reader pauses,\n // past the streak window.\n if (this.#machine.state === 'animating' || performance.now() - this.#lastFlipAt <= FLIP_STREAK_MS) {\n return;\n }\n }\n // A double-click means the single-click flip we may have queued was really a zoom.\n this.#clearPendingClickFlip();\n // Next configured level above the current scale, else wrap to the first.\n const next = levels.find((l) => l > this.#scale + 1e-6) ?? levels[0] ?? this.#scale;\n const rect = this.#container.getBoundingClientRect();\n this.setZoom(next, { x: clientX - rect.left, y: clientY - rect.top });\n }\n\n async #init(rendererOption: RendererOption): Promise<void> {\n // Load the renderer chunk while opening an async source (e.g. a PDF) in parallel.\n const rendererPromise = selectRenderer(rendererOption);\n // Report the download to the progress event and the built-in loader; register before open()\n // so the very first bytes are seen.\n this.#source.onProgress?.((p) => this.#onLoadProgress(p));\n if (typeof this.#source.open === 'function') {\n void this.#mountLoader(); // an async source has a wait worth indicating; sync ones do not\n try {\n await this.#source.open();\n this.#buildSpreadModel(); // page count known now; validates + builds spreads\n } catch (error) {\n this.#dismissLoader(); // tear the overlay down before `ready` rejects\n throw error;\n }\n this.#setLoaderPhase('preparing'); // bytes are in; the wait is now rasterizing\n }\n this.#source.prefetch([this.#currentPage]);\n\n const selected = await rendererPromise;\n const renderer = await this.#mountWithFallback(selected);\n this.#renderer = renderer;\n\n // Re-render the current spread when a source upgrades one of its pages in place.\n this.#source.onPageUpdate?.((index) => this.#onPageUpdate(index));\n\n // Now that we can measure, apply single-page mode if the container is narrow.\n this.#applySinglePage(renderer.measure().containerWidth);\n\n const pointer = new PointerRecognizer({\n onStart: (g) => this.#onDragStart(g.x, g.y),\n onMove: (g) => this.#onDragMove(g.dx, g.dy),\n onEnd: (g) => this.#onDragEnd(g),\n });\n const pinch = new PinchRecognizer({\n onPinchStart: () => this.#onPinchStart(),\n onPinchMove: (g) => this.#onPinchMove(g.centerX, g.centerY, g.scale),\n onPinchEnd: () => this.#onPinchEnd(),\n });\n this.#unbindInput = bindGestures(this.#container, { pointer, pinch });\n // We now own horizontal gestures; hand vertical panning (page scroll) back to the browser.\n this.#applyTouchAction();\n this.#bindWheelZoom();\n this.#bindDoubleClickZoom();\n this.#bindContextMenu();\n this.#bindCursorHints();\n this.#setupReducedMotion();\n this.#a11yCleanup = this.#setupA11y();\n\n await this.#renderCurrent();\n this.#dismissLoader(); // the first spread is painted; drop the overlay before the toolbar mounts\n this.#observeResize();\n this.#prefetchWindow();\n this.#announce();\n this.#bindDeepLink();\n await this.#mountControls();\n void this.#loadSound();\n this.#emitter.emit('ready');\n // The book is live: peek the leading corner once, then watch for an idle reader.\n void this.#playPeek();\n this.#armIdleNudge();\n }\n\n /**\n * Lazily import and build the flip-sound controller, once, when sound is enabled and audible — so\n * a silent book (`sound: false`, or `{ muted: true }` never unmuted) never pulls in the audio code\n * or the bundled clip. Called on ready for an audible book and again when the reader unmutes.\n * Best-effort: a failed import is ignored.\n */\n async #loadSound(): Promise<void> {\n if (!this.#soundEnabled || this.#soundMuted || this.#soundLoadStarted || this.#destroyed) return;\n this.#soundLoadStarted = true;\n try {\n const { FlipSound } = await import('./sound/flipSound');\n if (this.#destroyed) return; // torn down while the chunk loaded\n this.#sound = new FlipSound(this.#soundOptions);\n this.#sound.setMuted(this.#soundMuted); // honor a mute toggled while the chunk arrived\n } catch {\n // No audio code available: the book just stays silent.\n }\n }\n\n /** Play the flip sound, if enabled and ready. Called only where a turn is actually committed. */\n #playFlipSound(): void {\n this.#sound?.play();\n }\n\n /** Forward a download-progress tick to the public event and the built-in loader. */\n #onLoadProgress(progress: LoadProgress): void {\n this.#lastProgress = progress;\n this.#emitter.emit('progress', progress);\n this.#loader?.update(progress);\n }\n\n /**\n * Fetch the loader chunk and show the overlay, unless disabled or the book is already up.\n *\n * Lazy, so `loading: false` (or a source that never reports progress) never downloads it.\n * Failure is swallowed: a loader that cannot load should cost the reader a spinner, not the book.\n */\n async #mountLoader(): Promise<void> {\n if (this.#loadingOption === false || this.#destroyed) return;\n const container = this.#container;\n if (!container.ownerDocument || typeof container.appendChild !== 'function') return;\n try {\n const { mountLoading } = await import('./loading/loading');\n // The spread may have painted while the chunk was in flight; do not pop an overlay over it.\n if (this.#loaderPhase === 'done' || this.#destroyed) return;\n this.#loader = mountLoading(container, this.#strings);\n if (this.#lastProgress) this.#loader.update(this.#lastProgress);\n if (this.#loaderPhase === 'preparing') this.#loader.preparing();\n } catch {\n // No overlay; the book itself is unaffected.\n }\n }\n\n #setLoaderPhase(phase: 'preparing'): void {\n if (this.#loaderPhase === 'done') return;\n this.#loaderPhase = phase;\n this.#loader?.preparing();\n }\n\n #dismissLoader(): void {\n this.#loaderPhase = 'done';\n this.#loader?.destroy();\n this.#loader = null;\n }\n\n /** Mount `renderer`; if it can't initialize (e.g. no WebGL2 context) fall back to CSS. */\n async #mountWithFallback(renderer: Renderer): Promise<Renderer> {\n try {\n await renderer.mount(this.#container);\n renderer.onFatal?.(() => {\n void this.#fallbackToCss();\n });\n return renderer;\n } catch {\n renderer.destroy();\n const css = await selectRenderer('css');\n await css.mount(this.#container);\n this.#fellBack = true;\n this.#emitter.emit('rendererFallback', { from: 'webgl2', to: 'css' });\n return css;\n }\n }\n\n /** Runtime fallback: a live renderer signaled an unrecoverable failure (§8.4). */\n async #fallbackToCss(): Promise<void> {\n if (this.#fellBack || this.#destroyed) return;\n this.#fellBack = true;\n this.#renderer?.destroy();\n this.#renderer = null;\n const css = await selectRenderer('css');\n if (this.#destroyed) {\n css.destroy();\n return;\n }\n await css.mount(this.#container);\n this.#renderer = css;\n this.#applySinglePage(css.measure().containerWidth);\n await this.#renderCurrent();\n this.#emitter.emit('rendererFallback', { from: 'webgl2', to: 'css' });\n }\n\n #setupReducedMotion(): void {\n if (typeof matchMedia !== 'function') return;\n const query = matchMedia('(prefers-reduced-motion: reduce)');\n this.#reducedMotion = query.matches;\n query.addEventListener?.('change', (e) => {\n this.#reducedMotion = e.matches;\n });\n }\n\n /**\n * Build the toolbar and the right-click menu, whichever are wanted, if the container is real DOM.\n *\n * The chunk is fetched lazily so a book with neither feature never downloads it. Both share the\n * one chunk, so a book with `controls: false` but `contextMenu` on still gets the right-click menu\n * without a toolbar. Failure is swallowed on purpose: controls that cannot load should cost the\n * reader the controls, not the whole flipbook — `#init` has no error handling of its own, so a\n * throw here would reject `ready`.\n */\n async #mountControls(): Promise<void> {\n const wantsToolbar = this.#controlsOption !== false;\n const wantsContextMenu = this.#contextMenuOption !== false;\n if ((!wantsToolbar && !wantsContextMenu) || this.#destroyed) return;\n const container = this.#container;\n if (!container.ownerDocument || typeof container.appendChild !== 'function') return;\n try {\n const { mountControls, mountContextMenu } = await import('./controls/controls');\n if (this.#destroyed) return; // destroyed while the chunk was in flight\n if (this.#controlsOption !== false) {\n const options = this.#controlsOption === true ? {} : this.#controlsOption;\n this.#controlsCleanup = mountControls(this, container, options, this.#hiddenControls);\n }\n if (this.#contextMenuOption !== false) {\n const options = this.#contextMenuOption === true ? {} : this.#contextMenuOption;\n this.#contextMenuCleanup = mountContextMenu(this, container, options, this.#hiddenControls);\n }\n } catch {\n // No controls; the book itself is unaffected.\n }\n }\n\n /** Mirror the page in the URL, and follow the URL when it changes. */\n #bindDeepLink(): void {\n if (!this.#deepLinkEnabled || typeof globalThis.location === 'undefined') return;\n const win = globalThis as unknown as Window;\n this.#deepLink = bindDeepLink(win, (page) => {\n // Only act on a hash the reader changed — following a link, or pressing Back. A hash we\n // wrote ourselves describes where the book already is.\n if (page !== this.#currentPage) this.flipTo(page);\n });\n this.#deepLink.push(this.#currentPage);\n this.#unsubscribeDeepLink = this.#emitter.on('pageChanged', ({ page }) => {\n this.#deepLink?.push(page);\n });\n }\n\n /** The URL that opens this book at `page` (defaults to the current one). */\n pageLink(page: number = this.#currentPage): string {\n const href = globalThis.location?.href ?? '';\n if (!href) return '';\n const url = new URL(href);\n url.hash = hashWithPage(url.hash, clamp(page, 0, Math.max(0, this.#source.pageCount - 1)));\n return url.toString();\n }\n\n /** Wire keyboard nav + an aria-live page announcer. Skips non-DOM containers. */\n #setupA11y(): (() => void) | null {\n const container = this.#container;\n const doc = container.ownerDocument;\n if (!doc || typeof container.setAttribute !== 'function') return null;\n\n if (!container.hasAttribute('tabindex')) container.tabIndex = 0;\n container.setAttribute('aria-roledescription', this.#strings.roledescription);\n const onKey = (e: KeyboardEvent): void => this.#onKeyDown(e);\n container.addEventListener('keydown', onKey);\n\n const live = doc.createElement('div');\n live.setAttribute('aria-live', 'polite');\n live.setAttribute('aria-atomic', 'true');\n live.style.cssText =\n 'position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0;';\n container.appendChild(live);\n this.#liveRegion = live;\n\n return () => {\n container.removeEventListener('keydown', onKey);\n live.remove();\n this.#liveRegion = null;\n };\n }\n\n #onKeyDown(event: KeyboardEvent): void {\n const forwardKey = this.#direction === 'rtl' ? 'ArrowLeft' : 'ArrowRight';\n switch (event.key) {\n case 'ArrowRight':\n case 'ArrowLeft':\n if (event.key === forwardKey) this.flipNext();\n else this.flipPrev();\n event.preventDefault();\n break;\n case 'Home':\n this.flipTo(0);\n event.preventDefault();\n break;\n case 'End':\n this.flipTo(this.#source.pageCount - 1);\n event.preventDefault();\n break;\n default:\n break;\n }\n }\n\n #announce(): void {\n if (this.#liveRegion) {\n this.#liveRegion.textContent = this.#strings.pageAnnounce(\n this.#currentPage + 1,\n this.#source.pageCount,\n );\n }\n }\n\n #effectiveDuration(): number {\n // Reduced motion still turns the page, just briefly and flat (see #effectiveCurl): a page\n // that teleports gives the reader no sense of which way the book moved.\n if (this.#reducedMotion) return REDUCED_MOTION_DURATION;\n return this.#flipDuration * (this.#singlePage ? LONE_PAGE_FLIP_SCALE : 1);\n }\n\n /** The curl to turn with: reduced motion flattens the sheet, whatever the book asked for. */\n #effectiveCurl(): CurlSpec {\n return this.#reducedMotion ? 'simple' : this.#curl;\n }\n\n /** Re-measure and re-render; call after the container resizes. */\n update(): void {\n if (!this.#renderer || this.#machine.state !== 'idle') return;\n this.#applySinglePage(this.#renderer.measure().containerWidth);\n void this.#renderCurrent();\n this.#updateCursor(); // the drawn box (and its zones) may have moved under a resting pointer\n }\n\n /** The layout to actually use: the responsive narrow fallback forces 'single'. */\n #effectiveMode(): SpreadMode {\n return this.#narrow || this.#spreadMode === 'single' ? 'single' : this.#spreadMode;\n }\n\n #applySinglePage(containerWidth: number): void {\n const narrow =\n this.#responsiveSpread && shouldSinglePage(containerWidth, this.#singlePageThreshold);\n if (narrow === this.#narrow) return;\n this.#narrow = narrow;\n this.#rebuildSpreads();\n }\n\n /** Regroup the pages after something changes the layout, keeping the reader on the page they\n * were looking at even though its spread index may have moved. */\n #rebuildSpreads(): void {\n const mode = this.#effectiveMode();\n const changed = mode !== this.#effectiveModeShown;\n this.#effectiveModeShown = mode;\n this.#singlePage = mode === 'single';\n this.#spreads = buildSpreads(this.#source.pageCount, { direction: this.#direction, mode });\n this.#current = this.#spreadIndexForPage(this.#currentPage);\n // The book's proportions change with the layout, so consumers sizing around it need to know.\n if (changed) this.#emitter.emit('spreadChanged', { mode, singlePage: this.#singlePage });\n }\n\n async #renderCurrent(): Promise<void> {\n const spread = this.#spreads[this.#current];\n if (!spread || !this.#renderer) return;\n this.#currentContent = await this.#resolveContent(spread);\n this.#paintSpread(spread, this.#currentContent);\n }\n\n #paintSpread(spread: Spread, content: SpreadContent): void {\n this.#renderer?.renderSpread(spread, content, { fill: this.#singlePage, fit: this.#fit });\n this.#applyContainerAspect();\n // These pages were resolved at fit-to-screen; if the reader is zoomed, ask for them sharper.\n this.#zoomedAt = 1;\n this.#refreshZoomTiles();\n }\n\n /** Match the container's aspect-ratio to the book so it fits with no letterbox bars.\n * Non-destructive: `aspect-ratio` only drives whichever dimension the consumer leaves\n * auto (fit-width when they set a width), and is ignored if both are fixed. */\n #applyContainerAspect(): void {\n const m = this.#renderer?.measure();\n // Prefer the document-stable aspect (from the first page) so a book with off-size pages keeps\n // one container shape as you navigate; fall back to the painted book box before it is known.\n let ratio = m?.containerAspect;\n if (ratio === undefined) {\n const b = m?.book;\n if (!b || b.width <= 0 || b.height <= 0) return;\n ratio = b.width / b.height;\n }\n if (ratio <= 0) return;\n // Tolerance, not exact equality: writing `aspect-ratio` resizes the container, whose ResizeObserver\n // re-measures a book box off by a sub-pixel and would rewrite a marginally different ratio, an\n // endless resize/measure/write feedback loop (cheap for images, but re-rasterizes a PDF each turn,\n // freezing the tab). Ignoring changes below ~0.5% lets it settle after the first write.\n if (this.#lastAspect !== null && Math.abs(ratio - this.#lastAspect) < 0.005) return;\n this.#lastAspect = ratio;\n this.#container.style.aspectRatio = ratio.toFixed(4);\n }\n\n /**\n * A source upgraded a page's content (e.g. a progressive PDF swapping in the crisp render).\n *\n * Repainting mid-flip would disturb the animation, so an upgrade that lands during one is\n * noted and applied on settle instead of dropped. Dropping it left the page stuck at low\n * resolution for good: the upgrade fires once, and jumping straight to a page — from a search\n * hit, the page field, the outline — is exactly when it tends to arrive.\n */\n #onPageUpdate(index: number): void {\n if (this.#machine.state !== 'idle') {\n this.#pendingUpgrades.add(index);\n return;\n }\n const spread = this.#spreads[this.#current];\n if (!spread || !(spread.left === index || spread.right === index)) return;\n // While the reader is zoomed, the upgrade touched the fit raster underneath the sharp tile, not\n // what they see. A fit #renderCurrent would flash the page soft before the tiles rebuilt; refresh\n // the tiles instead (a no-op when they are already at the viewed scale).\n if (this.#scale > 1) {\n this.#refreshZoomTiles();\n return;\n }\n void this.#renderCurrent();\n }\n\n /**\n * Ask the source for a crisp render of whatever is visible, and lay it over the magnified page.\n *\n * Zooming is only a view transform, so without this the reader is magnifying the pixels of a\n * raster made to fit the screen. A vector source can do better; one backed by a fixed-resolution\n * original cannot, and says so by handing back a whole page, which is dropped.\n *\n * Debounced, and hidden while the view moves. A tile is crisp for one view only, so during a\n * pan it is stale by definition; showing it anyway puts two versions of the same text on screen\n * at once. Hiding leaves the reader with the uniformly magnified page an image book already\n * shows, which is smooth because there is nothing to disagree with it. The tile returns once\n * the view settles.\n */\n /**\n * Re-rasterize the visible pages at the magnification being viewed, and hand them to the\n * renderer as ordinary page content.\n *\n * Zoom is a view transform, so the renderer magnifies a raster made to fit the screen and text\n * goes soft. Giving it a sharper raster for the same page fixes that at the root: panning stays\n * a pure view transform over crisp pixels, which is why an image book pans smoothly and why an\n * overlay painted only between gestures never could.\n *\n * Debounced, since rasterizing a PDF page is far too slow to do per frame.\n */\n #refreshZoomTiles(): void {\n if (this.#tileTimer !== null) clearTimeout(this.#tileTimer);\n const wanted = this.#upgradeScale();\n if (wanted === this.#zoomedAt) return; // already showing this resolution\n if (wanted === 1) {\n this.#tileGeneration++; // abandon anything in flight: it is no longer wanted\n this.#zoomedAt = 1;\n void this.#renderCurrent(); // back to the fit-to-screen raster\n return;\n }\n this.#tileTimer = setTimeout(() => {\n this.#tileTimer = null;\n void this.#renderZoomTiles();\n }, ZOOM_TILE_DELAY);\n }\n\n /**\n * How many device pixels the visible page is painted across, over how many its fit-resolution\n * raster has (the neediest page of the spread). > 1 means the raster is stretched to cover the\n * display and text is soft; < 1 means the raster already has pixels to spare (a high-res image on\n * a low-dpr screen), which must be allowed so a crispness floor doesn't wastefully re-request it.\n *\n * A single-page spread gives one page the width two shared, so a fit raster sized for the double\n * layout is stretched over ~2x the space; this is the magnification the upgrade pass must undo.\n */\n #fitFactor(): number {\n const m = this.#renderer?.measure();\n const box = m?.content ?? m?.book;\n if (!box || box.width <= 0) return 1;\n const c = this.#currentContent;\n const painted = (c.left ? 1 : 0) + (c.right ? 1 : 0);\n if (painted === 0) return 1;\n // Both halves of a spread are equal width, so the per-page painted width is the box over the\n // number of pages in it (1 for a lone page, whose `content` box is already the centered half).\n const paintedPageCss = box.width / painted;\n const dpr = typeof devicePixelRatio === 'number' && devicePixelRatio > 0 ? devicePixelRatio : 1;\n let f = 0;\n for (const p of [c.left, c.right]) {\n if (!p || p.width <= 0) continue;\n // #renderZoomTiles swaps an upgraded (larger) raster into #currentContent and records its\n // multiplier in #zoomedAt; dividing it back out recovers the invariant fit-resolution width,\n // so a re-measure of an already-sharp page reports the same factor instead of flip-flopping\n // between fit and sharp.\n const intrinsic = p.width / this.#zoomedAt;\n f = Math.max(f, (paintedPageCss * dpr) / intrinsic);\n }\n return f === 0 ? 1 : f;\n }\n\n /**\n * The resolution multiplier to rasterize the visible pages at.\n *\n * Three demands compound onto the fit raster. `#fitFactor` covers a page painted larger than its\n * raster (device px). Zoom magnifies it further. And a text-crispness floor lifts the raster to\n * ~2x CSS px even when it already covers the display 1:1, because a 1:1 raster reads soft; the\n * floor is scaled by 1/dpr so a retina screen (already ~2x) is left alone and pays no memory.\n * Capped because a page costs the square of this in memory: at 4x a letter page is over 100 MB,\n * which would dwarf the source's whole cache.\n */\n #upgradeScale(): number {\n const dpr = typeof devicePixelRatio === 'number' && devicePixelRatio > 0 ? devicePixelRatio : 1;\n const crispnessFloor = Math.max(1, TEXT_SUPERSAMPLE / dpr);\n const zoom = this.#scale <= 1 ? 1 : this.#scale;\n const raw = this.#fitFactor() * Math.max(zoom, crispnessFloor);\n if (raw <= 1 + 1e-3) return 1;\n return Math.min(Math.ceil(raw * 2) / 2, MAX_PAGE_UPGRADE);\n }\n\n async #renderZoomTiles(): Promise<void> {\n const spread = this.#spreads[this.#current];\n if (!this.#renderer || !spread) return;\n const scale = this.#upgradeScale();\n if (scale === 1 || scale === this.#zoomedAt) return;\n // An upgrade repaints through renderSpread, which clears the renderer's turning leaf. If a flip\n // is animating (rapid edge clicks queue a settle-time upgrade whose debounce fires mid-next-turn)\n // painting now would wipe the curl and the leaf would snap flat. Skip: the flip's own settle\n // repaints and re-requests this once the book is idle, the defer rule #onPageUpdate follows too.\n // Checked here, before the slow rasterize, so a turn in progress costs nothing.\n if (this.#machine.state !== 'idle') return;\n\n const generation = ++this.#tileGeneration;\n const whole = { x: 0, y: 0, width: 1, height: 1 };\n const pages = await Promise.all(\n [spread.left, spread.right].map(async (index) => {\n if (index === null) return null;\n try {\n return await this.#source.get(index, {\n scale,\n region: whole,\n maxSize: this.#renderer?.maxTextureSize,\n });\n } catch (error) {\n // An upgrade is an enhancement: the readable, softer page stays on screen.\n this.#emitter.emit('sourceError', { index, error });\n return null;\n }\n }),\n );\n // Rasterizing is slow and the reader may have moved on, zoomed elsewhere, or begun a flip while\n // it resolved (the slow-source race the entry check cannot catch). Any of those and this raster\n // is stale; drop it and let the settle re-request.\n if (\n generation !== this.#tileGeneration ||\n this.#upgradeScale() !== scale ||\n this.#machine.state !== 'idle'\n ) {\n return;\n }\n // A side whose upgrade threw comes back null; keep the readable fit raster already on screen\n // rather than blanking that half (the upgrade is an enhancement, per the get() catch above).\n const left = pages[0] ?? this.#currentContent.left;\n const right = pages[1] ?? this.#currentContent.right;\n // Nothing sharper came back: a source with a fixed-resolution original hands over the raster\n // already on screen, and repainting it would be pure churn.\n if (left === this.#currentContent.left && right === this.#currentContent.right) return;\n\n this.#zoomedAt = scale;\n this.#currentContent = { left, right };\n this.#renderer.renderSpread(spread, this.#currentContent, { fill: this.#singlePage, fit: this.#fit });\n }\n\n /** Apply any upgrade that arrived mid-flip, now that the book has settled. */\n #flushPendingUpgrades(): void {\n if (this.#pendingUpgrades.size === 0) return;\n const spread = this.#spreads[this.#current];\n const onScreen =\n spread !== undefined &&\n [spread.left, spread.right].some((p) => p !== null && this.#pendingUpgrades.has(p));\n this.#pendingUpgrades.clear();\n if (!onScreen) return;\n // Same rule as #onPageUpdate: while the reader is zoomed, a fit #renderCurrent would flash the\n // page soft before the tiles rebuilt. Refresh the tiles instead. Gated on the live zoom scale,\n // not #zoomedAt: the #commit that leads here just repainted at fit and reset #zoomedAt to 1.\n if (this.#scale > 1) this.#refreshZoomTiles();\n else void this.#renderCurrent();\n }\n\n #observeResize(): void {\n if (typeof ResizeObserver === 'undefined') return;\n this.#resizeObserver = new ResizeObserver(() => this.#scheduleUpdate());\n this.#resizeObserver.observe(this.#container);\n }\n\n #scheduleUpdate(): void {\n if (this.#updateScheduled) return;\n this.#updateScheduled = true;\n requestAnimationFrame(() => {\n this.#updateScheduled = false;\n this.update();\n });\n }\n\n async #resolveContent(spread: Spread): Promise<SpreadContent> {\n const [left, right] = await Promise.all([this.#getPage(spread.left), this.#getPage(spread.right)]);\n return { left, right };\n }\n\n /** Decode one page; on failure emit `sourceError` and degrade to a blank (null) page. */\n async #getPage(index: number | null): Promise<PageContent | null> {\n if (index === null) return null;\n try {\n return await this.#source.get(index);\n } catch (error) {\n this.#emitter.emit('sourceError', { index, error });\n return null;\n }\n }\n\n #prefetchWindow(): void {\n const { toMount } = this.#virtualizer.update(this.#current, this.#spreads.length);\n const pages: number[] = [];\n for (const index of toMount) {\n const spread = this.#spreads[index];\n if (spread?.left != null) pages.push(spread.left);\n if (spread?.right != null) pages.push(spread.right);\n }\n this.#source.prefetch(pages);\n }\n\n #spreadIndexForPage(page: number): number {\n const index = this.#spreads.findIndex((s) => s.left === page || s.right === page);\n return index === -1 ? 0 : index;\n }\n\n /** Validate the (now-known) page count + startPage and build the spread model. */\n #buildSpreadModel(): void {\n const pageCount = this.#source.pageCount;\n if (!Number.isInteger(pageCount) || pageCount < 1) {\n throw new Error(`Zine: source has ${pageCount} pages; a Source must have at least 1 page.`);\n }\n const startPage = this.#startPageOption;\n if (\n startPage !== undefined &&\n (!Number.isInteger(startPage) || startPage < 0 || startPage >= pageCount)\n ) {\n throw new Error(\n `Zine: startPage ${JSON.stringify(startPage)} is out of range for a ${pageCount}-page book (valid 0..${pageCount - 1}).`,\n );\n }\n const mode = this.#effectiveMode();\n this.#singlePage = mode === 'single';\n this.#spreads = buildSpreads(pageCount, { direction: this.#direction, mode });\n // A page in the URL wins over `startPage`: the reader followed a link to it, which is a\n // later and more specific intent than the page the author configured.\n const linked = this.#deepLinkEnabled ? pageFromHash(globalThis.location?.hash ?? '') : null;\n this.#currentPage = clamp(linked ?? startPage ?? 0, 0, pageCount - 1);\n this.#current = this.#spreadIndexForPage(this.#currentPage);\n }\n}\n\nfunction clamp(value: number, min: number, max: number): number {\n return value < min ? min : value > max ? max : value;\n}\n\n/**\n * The translate range that keeps content within the viewport along one axis.\n *\n * `pos`/`size` are where the pages sit with no pan applied, so the view puts them at `pos + t`.\n * When they are larger than the viewport the range spans from their far edge to their near one,\n * so the reader can reach every part. When smaller there is nothing to explore, so the range\n * collapses to the single value that centres them: a lone page at 2x fills the width exactly, and\n * any freedom there would only let it drift off to one side.\n */\nfunction axisPanRange(pos: number, size: number, viewport: number): [min: number, max: number] {\n if (size <= viewport) {\n const centered = (viewport - size) / 2 - pos;\n return [centered, centered];\n }\n return [viewport - (pos + size), -pos];\n}\n\n/** Quadratic ease-in-out. Deliberately gentler than a cubic: a cubic leaves the page nearly\n * motionless for the first quarter of the turn and then spikes through the middle at roughly\n * double the peak speed, which reads as a lurch rather than a sheet of paper being turned. */\nfunction easeInOutQuad(t: number): number {\n return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;\n}\n\nfunction typeName(v: unknown): string {\n if (v === null) return 'null';\n if (Array.isArray(v)) return 'array';\n return typeof v;\n}\n\n/** Validate constructor inputs up front, throwing agent-actionable errors. */\nfunction validateOptions(container: unknown, options: unknown): void {\n if (\n typeof container !== 'object' ||\n container === null ||\n typeof (container as { appendChild?: unknown }).appendChild !== 'function' ||\n typeof (container as { addEventListener?: unknown }).addEventListener !== 'function'\n ) {\n throw new Error(`Zine: container must be a DOM element; received ${typeName(container)}.`);\n }\n if (typeof options !== 'object' || options === null) {\n throw new Error('Zine: an options object with a `source` is required.');\n }\n const o = options as Record<string, unknown>;\n\n const source = o.source as { get?: unknown; pageCount?: unknown } | undefined;\n if (\n typeof source !== 'object' ||\n source === null ||\n typeof source.get !== 'function' ||\n typeof source.pageCount !== 'number'\n ) {\n throw new Error('Zine: `source` is required and must be a Source, e.g. new ImageSource(urls).');\n }\n // Note: pageCount >= 1 and startPage-in-range are validated in #buildSpreadModel —\n // synchronously for a sync source, or after open() for an async one (e.g. PdfSource).\n\n const startPage = o.startPage;\n if (startPage !== undefined && (typeof startPage !== 'number' || !Number.isInteger(startPage) || startPage < 0)) {\n throw new Error(`Zine: startPage must be a non-negative integer; got ${JSON.stringify(startPage)}.`);\n }\n\n const direction = o.direction;\n if (direction !== undefined && direction !== 'ltr' && direction !== 'rtl') {\n throw new Error(`Zine: direction must be 'ltr' or 'rtl'; got ${JSON.stringify(direction)}.`);\n }\n\n const clickToFlip = o.clickToFlip;\n if (clickToFlip !== undefined && !['edge', 'half', 'off'].includes(clickToFlip as string)) {\n throw new Error(`Zine: clickToFlip must be 'edge', 'half', or 'off'; got ${JSON.stringify(clickToFlip)}.`);\n }\n\n const fit = o.fit;\n if (fit !== undefined && fit !== 'contain' && fit !== 'fill') {\n throw new Error(`Zine: fit must be 'contain' or 'fill'; got ${JSON.stringify(fit)}.`);\n }\n\n const strings = o.strings;\n if (strings !== undefined && (typeof strings !== 'object' || strings === null)) {\n throw new Error(`Zine: strings must be an object of text overrides; got ${typeName(strings)}.`);\n }\n\n const curl = o.curl;\n if (curl !== undefined) {\n if (typeof curl === 'object' && curl !== null) {\n // An imported or custom model. Check the one member the renderer calls every frame, so a\n // wrong shape fails here rather than as an undefined call mid-flip.\n if (typeof (curl as { deform?: unknown }).deform !== 'function') {\n throw new Error('Zine: a curl model must have a deform() function.');\n }\n } else if (IMPORTABLE_CURLS.includes(curl as string)) {\n // A real curl, just not bundled: say how to get it rather than calling it invalid.\n throw new Error(\n `Zine: the '${String(curl)}' curl is not bundled. Import it and pass the model: ` +\n `import { ${String(curl)} } from '@zinejs/core/curls' → curl: ${String(curl)}`,\n );\n } else if (!CURL_TYPES.includes(curl as CurlType)) {\n throw new Error(\n `Zine: curl must be ${CURL_TYPES.join(' or ')}, or a model imported from ` +\n `'@zinejs/core/curls' (${IMPORTABLE_CURLS.join(', ')}); got ${JSON.stringify(curl)}.`,\n );\n }\n }\n\n for (const flag of ['deepLink', 'disableContextMenu', 'responsiveSpread', 'loading'] as const) {\n if (o[flag] !== undefined && typeof o[flag] !== 'boolean') {\n throw new Error(`Zine: ${flag} must be a boolean; got ${typeName(o[flag])}.`);\n }\n }\n\n const controls = o.controls;\n if (controls !== undefined && typeof controls !== 'boolean' && (typeof controls !== 'object' || controls === null)) {\n throw new Error(\n `Zine: controls must be a boolean or an options object; got ${typeName(controls)}.`,\n );\n }\n const position = (controls as { position?: unknown } | undefined)?.position;\n if (position !== undefined && !['top', 'bottom', 'left', 'right'].includes(position as string)) {\n throw new Error(\n `Zine: controls.position must be 'top', 'bottom', 'left', or 'right'; got ${JSON.stringify(position)}.`,\n );\n }\n const colorScheme = (controls as { colorScheme?: unknown } | undefined)?.colorScheme;\n if (colorScheme !== undefined && !['light', 'dark', 'auto'].includes(colorScheme as string)) {\n throw new Error(\n `Zine: controls.colorScheme must be 'light', 'dark', or 'auto'; got ${JSON.stringify(colorScheme)}.`,\n );\n }\n const arrows = (controls as { arrows?: unknown } | undefined)?.arrows;\n if (arrows !== undefined && typeof arrows !== 'boolean' && !['desktop', 'mobile'].includes(arrows as string)) {\n throw new Error(\n `Zine: controls.arrows must be a boolean, 'desktop', or 'mobile'; got ${JSON.stringify(arrows)}.`,\n );\n }\n\n const contextMenu = o.contextMenu;\n if (contextMenu !== undefined && typeof contextMenu !== 'boolean' && (typeof contextMenu !== 'object' || contextMenu === null)) {\n throw new Error(\n `Zine: contextMenu must be a boolean or an options object; got ${typeName(contextMenu)}.`,\n );\n }\n const menuColorScheme = (contextMenu as { colorScheme?: unknown } | undefined)?.colorScheme;\n if (menuColorScheme !== undefined && !['light', 'dark', 'auto'].includes(menuColorScheme as string)) {\n throw new Error(\n `Zine: contextMenu.colorScheme must be 'light', 'dark', or 'auto'; got ${JSON.stringify(menuColorScheme)}.`,\n );\n }\n if (o.disableContextMenu === true && contextMenu !== undefined && contextMenu !== false) {\n throw new Error(\n \"Zine: disableContextMenu and contextMenu are mutually exclusive — one removes the browser's \" +\n 'right-click menu, the other replaces it. Enable only one.',\n );\n }\n\n const hideControls = o.hideControls;\n if (\n hideControls !== undefined &&\n (!Array.isArray(hideControls) || hideControls.some((id) => typeof id !== 'string'))\n ) {\n throw new Error(\n `Zine: hideControls must be an array of control-id strings; got ${typeName(hideControls)}.`,\n );\n }\n\n const sound = o.sound;\n if (sound !== undefined && typeof sound !== 'boolean' && (typeof sound !== 'object' || sound === null)) {\n throw new Error(`Zine: sound must be a boolean or an options object; got ${typeName(sound)}.`);\n }\n if (typeof sound === 'object' && sound !== null) {\n const s = sound as { url?: unknown; volume?: unknown; muted?: unknown; persist?: unknown };\n if (s.url !== undefined && typeof s.url !== 'string') {\n throw new Error(`Zine: sound.url must be a string URL; got ${typeName(s.url)}.`);\n }\n if (s.volume !== undefined && (typeof s.volume !== 'number' || s.volume < 0 || s.volume > 1)) {\n throw new Error(`Zine: sound.volume must be a number between 0 and 1; got ${JSON.stringify(s.volume)}.`);\n }\n if (s.muted !== undefined && typeof s.muted !== 'boolean') {\n throw new Error(`Zine: sound.muted must be a boolean; got ${typeName(s.muted)}.`);\n }\n if (s.persist !== undefined && typeof s.persist !== 'boolean') {\n throw new Error(`Zine: sound.persist must be a boolean; got ${typeName(s.persist)}.`);\n }\n }\n\n const spreadMode = o.spreadMode;\n if (spreadMode !== undefined && !['double', 'single', 'cover', 'book'].includes(spreadMode as string)) {\n throw new Error(\n `Zine: spreadMode must be 'double', 'single', 'cover', or 'book'; got ${JSON.stringify(spreadMode)}.`,\n );\n }\n\n for (const key of ['frontCover', 'backCover'] as const) {\n const value = o[key];\n if (value !== undefined && typeof value !== 'string') {\n throw new Error(`Zine: ${key} must be an image URL string; got ${JSON.stringify(value)}.`);\n }\n }\n const pages = o.pages;\n if (pages !== undefined && (typeof pages !== 'object' || pages === null || Array.isArray(pages))) {\n throw new Error('Zine: pages must be an object mapping page indices to image URLs.');\n }\n\n assertMin(o.width, 'width', 1);\n assertMin(o.height, 'height', 1);\n assertMin(o.flipDuration, 'flipDuration', 0);\n assertMin(o.clickZoneSize, 'clickZoneSize', 0);\n assertMin(o.clickFlipDelay, 'clickFlipDelay', 0);\n assertMin(o.singlePageThreshold, 'singlePageThreshold', 0);\n validateZoomOption(o.zoom);\n validateRendererOption(o.renderer);\n}\n\nfunction assertMin(value: unknown, name: string, min: number): void {\n if (value === undefined) return;\n if (typeof value !== 'number' || !Number.isFinite(value) || value < min) {\n throw new Error(`Zine: ${name} must be a number >= ${min}; got ${JSON.stringify(value)}.`);\n }\n}\n\nfunction validateZoomOption(zoom: unknown): void {\n if (zoom === undefined) return;\n if (typeof zoom !== 'object' || zoom === null || Array.isArray(zoom)) {\n throw new Error('Zine: zoom must be an object, e.g. { max: 4 }.');\n }\n const z = zoom as Record<string, unknown>;\n if (z.enabled !== undefined && typeof z.enabled !== 'boolean') {\n throw new Error(`Zine: zoom.enabled must be a boolean; got ${JSON.stringify(z.enabled)}.`);\n }\n if (z.wheel !== undefined && typeof z.wheel !== 'boolean') {\n throw new Error(`Zine: zoom.wheel must be a boolean; got ${JSON.stringify(z.wheel)}.`);\n }\n if (z.doubleClickInFlipZone !== undefined && typeof z.doubleClickInFlipZone !== 'boolean') {\n throw new Error(\n `Zine: zoom.doubleClickInFlipZone must be a boolean; got ${JSON.stringify(z.doubleClickInFlipZone)}.`,\n );\n }\n if (z.doubleClick !== undefined && z.doubleClick !== false) {\n const levels = z.doubleClick;\n if (\n !Array.isArray(levels) ||\n levels.some((n) => typeof n !== 'number' || !Number.isFinite(n) || n < 1)\n ) {\n throw new Error(\n 'Zine: zoom.doubleClick must be false or an array of zoom levels >= 1, e.g. [1, 2, 4].',\n );\n }\n }\n assertMin(z.max, 'zoom.max', 1);\n}\n\nfunction validateRendererOption(renderer: unknown): void {\n if (renderer === undefined) return;\n if (typeof renderer === 'string') {\n if (renderer !== 'auto' && renderer !== 'css' && renderer !== 'webgl2') {\n throw new Error(\n `Zine: renderer '${renderer}' is not recognized; use 'auto', 'css', 'webgl2', an array of those, or a custom Renderer.`,\n );\n }\n return;\n }\n if (Array.isArray(renderer)) {\n for (const kind of renderer) {\n if (kind !== 'css' && kind !== 'webgl2') {\n throw new Error(\n `Zine: renderer order array may only contain 'css' or 'webgl2'; got ${JSON.stringify(kind)}.`,\n );\n }\n }\n return;\n }\n if (\n typeof renderer === 'object' &&\n renderer !== null &&\n typeof (renderer as { mount?: unknown }).mount === 'function'\n ) {\n return;\n }\n throw new Error(\n \"Zine: renderer must be 'auto', 'css', 'webgl2', an array of those, or a custom Renderer instance.\",\n );\n}\n","import type { PageContent } from '../renderer/types';\nimport type { Source } from './types';\n\nexport type ImageFit = 'contain' | 'cover';\n\nexport interface ImageSourceOptions {\n /** How many adjacent pages to prefetch around a requested page. Default 1. */\n preload?: number;\n /** How each image fits its page; a hint consumed by the render layer. Default 'contain'. */\n fit?: ImageFit;\n}\n\n/**\n * Default content source for image books: decodes a list of image URLs to\n * ImageBitmaps, memoizes them, and prefetches neighbors on access.\n */\nexport class ImageSource implements Source {\n readonly pageCount: number;\n readonly fit: ImageFit;\n #urls: string[];\n #preload: number;\n #cache = new Map<number, Promise<ImageBitmap>>();\n\n constructor(urls: string[], options: ImageSourceOptions = {}) {\n this.#urls = urls;\n this.pageCount = urls.length;\n this.#preload = options.preload ?? 1;\n this.fit = options.fit ?? 'contain';\n }\n\n get(index: number): Promise<PageContent> {\n const decoded = this.#decode(index);\n for (let d = 1; d <= this.#preload; d++) {\n this.prefetch([index - d, index + d]);\n }\n return decoded;\n }\n\n prefetch(indices: number[]): void {\n for (const i of indices) {\n if (i >= 0 && i < this.pageCount) {\n void this.#decode(i).catch(() => {}); // swallow: prefetch failures surface on real get()\n }\n }\n }\n\n destroy(): void {\n for (const pending of this.#cache.values()) {\n void pending.then((bitmap) => bitmap.close()).catch(() => {});\n }\n this.#cache.clear();\n }\n\n #decode(index: number): Promise<ImageBitmap> {\n const cached = this.#cache.get(index);\n if (cached) return cached;\n\n const url = this.#urls[index];\n if (url === undefined) {\n return Promise.reject(\n new RangeError(`ImageSource: page ${index} is out of range (0..${this.pageCount - 1}).`),\n );\n }\n\n const promise = (async () => {\n const res = await fetch(url);\n if (!res.ok) {\n throw new Error(`ImageSource: failed to fetch page ${index} (${url}) — HTTP ${res.status}.`);\n }\n return createImageBitmap(await res.blob());\n })().catch((err: unknown) => {\n this.#cache.delete(index); // drop the failed entry so a later get() can retry\n throw err;\n });\n\n this.#cache.set(index, promise);\n return promise;\n }\n}\n","export { Zine } from './zine';\nexport type { ZineOptions, ZoomOptions, SearchHit } from './zine';\nexport { defaultStrings } from './strings';\nexport type { ZineStrings } from './strings';\n\n// Just the registry mechanism — the toolbar, its icons and the built-in definitions all live in\n// the lazily-loaded chunk, so registering a custom control costs nothing up front.\nexport { defineControl, getControl, DEFAULT_ITEMS } from './controls/registry';\nexport type {\n ControlContext,\n ControlDef,\n ControlItem,\n ControlsColorScheme,\n ControlsOptions,\n ControlsPosition,\n} from './controls/types';\n\nexport { ImageSource } from './source/imageSource';\nexport type { ImageSourceOptions, ImageFit } from './source/imageSource';\nexport type { Source, DownloadInfo, LoadProgress, OutlineItem, PageRequest } from './source/types';\n\nexport type { RendererOption, RendererKind } from './renderer/select';\nexport type { Renderer, SpreadContent, PageContent, FlipDirection, LayoutMetrics, RenderOptions } from './renderer/types';\n\nexport type { ZineEventMap } from './engine/emitter';\nexport type { Direction, Spread } from './engine/spread';\nexport { CURL_TYPES, IMPORTABLE_CURLS } from './geometry/curls/types';\nexport type { CurlType, CurlSpec, CurlModel, CurlDeform, CurlAnchor } from './geometry/curls/types';\nexport type { PageMesh } from './geometry/curls/mesh';\n"],"mappings":"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"}
|