@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
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"controls-CcOu_19g.js","names":["#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"],"sources":["../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"],"sourcesContent":["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"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"cssRenderer-Deskm2ND.js","names":["#container","#clip","#viewport","#book","#pageLeft","#makeCanvas","#pageRight","#leaf","#leafFront","#leafBack","#leafShadow","#paint","#fill","#contentShift","#trackAspect","#layoutBook","#applyShift","#fromShiftUnit","#toShiftUnit","#bookW","#pageAspect","#bookBox","#contentBox"],"sources":["../src/geometry/flipProgressToPose.ts","../src/renderer/cssRenderer.ts"],"sourcesContent":["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"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"loading-DF88K9yJ.js","names":[],"sources":["../src/loading/loading.ts"],"sourcesContent":["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"],"mappings":";AAkBA,IAAM,IAAW;AAgEjB,SAAS,EAAa,GAAqB;CACzC,IAAI,EAAI,eAAe,CAAQ,GAAG;CAClC,IAAM,IAAQ,EAAI,cAAc,OAAO;CAGvC,AAFA,EAAM,KAAK,GACX,EAAM,cAAc,g9DACnB,EAAI,QAAQ,EAAI,gBAAA,EAAkB,YAAY,CAAK;AACtD;AAYA,IAAM,KAAS,MAA0B,IAAI,IAAQ,OAAO,KAAA,CAAM,QAAQ,CAAC,EAAE;AAQ7E,SAAgB,EAAa,GAAsC;CACjE,IAAM,IAAM,EAAU;CACtB,EAAa,CAAG;CAEhB,IAAM,IAAU,EAAI,cAAc,KAAK;CAGvC,AAFA,EAAQ,YAAY,gBACpB,EAAQ,aAAa,QAAQ,QAAQ,GACrC,EAAQ,aAAa,aAAa,QAAQ;CAE1C,IAAM,IAAU,EAAI,cAAc,KAAK;CAEvC,AADA,EAAQ,YAAY,wBACpB,EAAQ,aAAa,eAAe,MAAM;CAE1C,IAAM,IAAO,EAAI,cAAc,KAAK;CAEpC,AADA,EAAK,YAAY,qBACjB,EAAK,cAAc;CAEnB,IAAM,IAAM,EAAI,cAAc,KAAK;CAEnC,AADA,EAAI,YAAY,oBAChB,EAAI,MAAM,UAAU;CACpB,IAAM,IAAO,EAAI,cAAc,KAAK;CAKpC,AAJA,EAAK,YAAY,qBACjB,EAAI,YAAY,CAAI,GAEpB,EAAQ,OAAO,GAAS,GAAM,CAAG,GACjC,EAAU,YAAY,CAAO;CAK7B,IAAI,IAAiC,MAC/B,IACJ,OAAO,WAAW,oBAAqB,aAAa,WAAW,iBAAiB,CAAS,IAAI;CAC/F,CAAI,CAAC,KAAY,EAAS,aAAa,YAAY,EAAS,aAAa,QACvE,IAAkB,EAAU,MAAM,UAClC,EAAU,MAAM,WAAW;CAG7B,IAAI,IAAW,IACT,IAAc,iBAAiB;EAEnC,AADA,IAAW,IACX,EAAQ,UAAU,IAAI,oBAAoB;CAC5C,GAAG,GAAe;CAElB,OAAO;EACL,OAAO,GAA8B;GACnC,IAAI,EAAS,QAAQ,GAAG;IACtB,IAAM,IAAM,KAAK,IAAI,KAAK,KAAK,MAAO,EAAS,SAAS,EAAS,QAAS,GAAG,CAAC;IAG9E,AAFA,EAAK,cAAc,yBAAyB,EAAI,IAChD,EAAI,MAAM,UAAU,IACpB,EAAK,MAAM,QAAQ,GAAG,EAAI;GAC5B,OAEE,AADA,EAAK,cAAc,yBAAyB,EAAM,EAAS,MAAM,KACjE,EAAI,MAAM,UAAU;EAExB;EACA,YAAkB;GAEhB,AADA,EAAK,cAAc,oBACnB,EAAI,MAAM,UAAU;EACtB;EACA,UAAgB;GAGd,AAFK,KAAU,aAAa,CAAW,GACvC,EAAQ,OAAO,GACX,MAAoB,SAAM,EAAU,MAAM,WAAW;EAC3D;CACF;AACF"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"share-BrQa6aYm.js","names":["#doc","#lastFocus","#backdrop","#dialog","#linkRow","#socialRow","#qr","#onKey","#trapFocus","#copy","#timer"],"sources":["../src/controls/socials.ts","../src/controls/qr.ts","../src/controls/share.ts"],"sourcesContent":["/**\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","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"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"styles-D_zOGk2q.js","names":[],"sources":["../src/controls/icons.ts","../src/controls/styles.ts"],"sourcesContent":["/**\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","/**\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"],"mappings":";AAcA,IAAa,IAAQ;CAEnB,MAAM;CAEN,MAAM;CAEN,OAAO;CAEP,MAAM;CAEN,QAAQ;CAER,SAAS;CAET,QAAQ;CAER,OACE;CAEF,MAAM;CAEN,YACE;CAEF,gBACE;CAEF,YACE;CAEF,SACE;CAEF,UACE;CAEF,SAAS;CAET,OACE;CAEF,UAAU;CAEV,OAAO;AACT;AAGA,SAAgB,EAAW,GAAe,GAA+B;CACvE,IAAM,IAAM,EAAI,gBAAgB,8BAA8B,KAAK;CAanE,OAZA,EAAI,aAAa,WAAW,WAAW,GACvC,EAAI,aAAa,QAAQ,MAAM,GAC/B,EAAI,aAAa,UAAU,cAAc,GAGzC,EAAI,aAAa,gBAAgB,MAAM,GACvC,EAAI,aAAa,kBAAkB,OAAO,GAC1C,EAAI,aAAa,mBAAmB,OAAO,GAC3C,EAAI,aAAa,eAAe,MAAM,GACtC,EAAI,aAAa,aAAa,OAAO,GAErC,EAAI,YAAY,GACT;AACT;;;AC9DA,IAAM,IAAW,uBAEJ,IAAM;AAyrBnB,SAAgB,EAAa,GAAqB;CAChD,IAAI,EAAI,eAAe,CAAQ,GAAG;CAClC,IAAM,IAAQ,EAAI,cAAc,OAAO;CAGvC,AAFA,EAAM,KAAK,GACX,EAAM,cAAc,IACnB,EAAI,QAAQ,EAAI,gBAAA,EAAkB,YAAY,CAAK;AACtD;AAUA,SAAgB,EAAiB,GAAiB,GAAyC;CACzF,AAAI,MAAW,WAAQ,EAAG,MAAM,cAAc;AAChD"}
|