import { type Direction, type Spread, type SpreadMode } from './engine/spread'; import { type ZineEventMap } from './engine/emitter'; import { type CurlSpec } from './geometry/curls/types'; import { type RendererOption } from './renderer/select'; import type { FlipDirection, PageContent } from './renderer/types'; import type { DownloadInfo, OutlineItem, Source } from './source/types'; import type { ContextMenuOptions, ControlsOptions } from './controls/types'; import { type ZineStrings } from './strings'; /** One page that matched a {@link Zine.search} query. */ export interface SearchHit { /** Zero-based page index; pass straight to `flipTo`. */ page: number; /** The match with a little text either side, ellipsised where it was cut. */ excerpt: string; } export interface ZoomOptions { /** Whether zooming is allowed at all; default true. */ enabled?: boolean; /** Maximum zoom scale; default 4. */ max?: number; /** Zoom on Ctrl/⌘ + wheel (also how trackpad pinch arrives on desktop); default true. */ wheel?: boolean; /** Zoom levels cycled by double-click (wraps to the first); `false` disables. Default [1, 2, 4]. */ doubleClick?: number[] | false; /** * Listen for double-click zoom inside the click-to-flip zones. Default: off in 'edge' mode * (edges flip instantly, no delay), on in 'half' mode. Turning it on in 'edge' mode makes * clicks wait out `clickFlipDelay` so a double-click can preempt them with a zoom. */ doubleClickInFlipZone?: boolean; } export interface ZineOptions { /** Content source (e.g. an ImageSource). Required. */ source: Source; /** Renderer selection; default 'auto'. */ renderer?: RendererOption; /** * Page-curl model for the WebGL2 renderer. Default `'cone'`. * * `'cone'` and `'simple'` are bundled and can be named directly. The others are imported and * passed as models, so a book that never uses them never carries their math: * * ```ts * import { silk } from '@zinejs/core/curls'; * new Zine(el, { source, curl: silk }); * ``` * * Any object of the same shape works, so a custom curl needs no registration. */ curl?: CurlSpec; /** Fixed container width in px; omit to let the container/CSS drive the size. */ width?: number; /** Fixed container height in px; omit to let the container/CSS drive the size. */ height?: number; /** Reading direction; default 'ltr'. */ direction?: Direction; /** How pages group into spreads: 'double' | 'single' | 'cover' | 'book'. Default 'cover'. */ spreadMode?: SpreadMode; /** Image URL prepended as a lone front cover (adds a page). */ frontCover?: string; /** Image URL appended as a lone back cover (adds a page). */ backCover?: string; /** Replace source pages with image URLs, keyed by 0-based source index (negative = from the end). */ pages?: Record; /** Page to open on; default 0. */ startPage?: number; /** Flip animation duration in ms; default 500. */ flipDuration?: number; /** How a tap/click turns pages: near an edge ('edge'), by page half ('half'), or 'off'. Default 'edge'. */ clickToFlip?: 'edge' | 'half' | 'off'; /** How a page whose size differs from the book fits: 'contain' (default) keeps its aspect and * centers it (a small margin shows); 'fill' stretches it to the book's shape. Either way the * book keeps one shape for the whole document, so mixed-size pages never cause a layout shift. */ fit?: 'contain' | 'fill'; /** Hide a print-gutter duplicate: print PDFs sometimes repeat a strip of artwork on both sides * of the spine so nothing is lost in the binding, which shows twice when the spread lies flat. * A number is the overlap as a fraction of page width (e.g. 0.02 = 2%), removed from every * two-page spread, half from each page's spine edge. 'auto' finds the repeat per spread and only * trims spreads that have one. Lone pages are never trimmed. Default 0 (off). */ gutterOverlap?: 'auto' | number; /** Override any reader-facing text (control labels, screen-reader announcements, panel and * loading text) for localization. English by default; pass only the keys you want to change. * Interpolated entries (e.g. the page announcement) are functions. See {@link ZineStrings}. */ strings?: Partial; /** Edge-zone size in px per side, used when clickToFlip is 'edge'. Default 64. */ clickZoneSize?: number; /** * On a mouse, change the cursor over the book to hint what a press would do: `pointer` where a * click turns the page, `zoom-in` where a double-click zooms (including the edge peel band, so a * sweep stays calm), `grab` over the whole page once zoomed (a drag pans), and `grabbing` while a * peel or pan is actually in flight. Default true. Set false when a changing cursor would be noise * in the embedding design. No effect on touch. */ cursorHints?: boolean; /** * Subtle, one-shot discoverability hints shown on the book itself (so they reach touch readers, * unlike {@link cursorHints}): the leading page corner peeks and settles on first open, nudges * once more if the reader sits idle without turning a page, a small "drag to move" caption appears * the first time they zoom in, and (on a mouse) a "double-click or Ctrl/⌘-scroll to zoom" caption * appears if a lone click lands in a dead zone where only a double-click would do anything. Each * hint stops for good the moment the reader performs the gesture it teaches. Honors * `prefers-reduced-motion` (the motion cues are skipped). Default true. Pass an object to keep what * the reader has learned across visits: `{ persist: true }` records it in localStorage so a * returning reader is not re-taught (default is in-memory, per page load). */ hints?: boolean | { enabled?: boolean; persist?: boolean; }; /** * Play a short sound on each page turn. Off by default — web audio is unexpected, so it is * strictly opt-in. `true` uses the bundled clip at the default volume; an object overrides it: * `url` points at your own clip (fetched at runtime), `volume` is 0..1, and `muted: true` offers * the sound but starts it silent so the reader turns it on themselves (via the `mute` control), * and `persist: true` remembers the reader's mute choice across visits in localStorage (otherwise * it resets each load). Playback is best-effort and never throws: a browser that gates audio behind * a gesture, lacks Web Audio, or cannot decode the clip just stays silent. Whenever sound is * enabled — audible or muted — a `mute` control appears in the toolbar and right-click menu. */ sound?: boolean | { url?: string; volume?: number; muted?: boolean; persist?: boolean; }; /** * Delay (ms) a click waits before flipping, so a double-click can preempt it with a zoom. * Omit for auto: 0 when double-click zoom is inactive, 250 when it's active. */ clickFlipDelay?: number; /** Zoom behavior. */ zoom?: ZoomOptions; /** Container widths below this (px) switch to one page per spread; default 640. */ singlePageThreshold?: number; /** * Whether a narrow container may override `spreadMode` and show one page at a time. Default * true. * * Set false to hold the configured mode at every width — a two-page spread stays two pages on * a phone, however small. (`singlePageThreshold: 0` has the same effect; this says it out * loud, and can be changed at runtime with {@link Zine.setResponsiveSpread}.) */ responsiveSpread?: boolean; /** * The built-in toolbar. Shown by default; pass `false` to render none, or an object to choose * its position and which controls appear. */ controls?: boolean | ControlsOptions; /** * The built-in loading indicator, shown while a source that reports download progress (a PDF * from a URL) fetches and prepares its first spread. On by default; pass `false` to render none * and drive your own from the `progress` event and the `ready` promise. Retheme the default with * the `--zine-loading-bg`, `--zine-loading-fg`, `--zine-loading-accent`, and `--zine-loading-track` * CSS variables. */ loading?: boolean; /** * Keep the current page in the URL hash (`#page=12`), so a link opens where the reader was and * back/forward move through the book. Default true. * * Only a `page` key is read or written — anything else in the hash is left alone — and updates * use `replaceState`, so turning pages does not fill the back button. Pass `false` if your app * owns the hash. */ deepLink?: boolean; /** * Suppress the browser's right-click menu over the book. Default false. * * A mild deterrent for published documents, not protection: the pages are still in the DOM and * reachable by anyone who wants them. It also takes away Inspect and "Open image in new tab" * for everyone, so it is off unless asked for. * * Mutually exclusive with {@link contextMenu}: one replaces the native menu, the other only * removes it, so enabling both is a contradiction and throws. */ disableContextMenu?: boolean; /** * A right-click menu of reading controls (zoom, page turns, fullscreen, print, download, share), * opened at the cursor over the book. On by default. Over a canvas the native menu offers only * the generic page actions (Back, Reload, View source), so replacing it with the book's own * controls is the better trade; pass `false` to keep the browser's menu. * * Reuses the toolbar's icons and actions, so the same controls appear whether reached from the * toolbar or a right-click, and it works even with `controls: false` — a bare book can still * offer one. `true` uses the default set; an object with `items` replaces the layout. Mouse * affordance only; touch never raises it. * * Turned off automatically when {@link disableContextMenu} is set (that option removes the native * menu with nothing in its place); setting both explicitly throws. */ contextMenu?: boolean | ContextMenuOptions; /** * Control ids to hide from both surfaces at once: off the toolbar (bar and overflow menu) and out * of the right-click menu. `['print', 'download']` removes those two everywhere, wherever they are * nested, without having to edit `controls.items` and `contextMenu.items` separately. * * A blunt cross-cutting filter: it only removes, and it applies to whatever layout each surface * ends up with (default or custom). To rearrange or add controls, set the per-surface `items` * instead. */ hideControls?: readonly string[]; } /** * The public flipbook. Composes the engine (spreads, virtualizer, events), a * lazily-selected renderer, and a content source. Construction is synchronous * but setup is async — await `ready` (or listen for the `ready` event) before * driving it. */ export declare class Zine { #private; /** Stable seek API: drive the fold to a fixed progress without animating (visual regression). */ readonly debug: { setFlipProgress: (t: number, direction: FlipDirection) => void; }; constructor(container: HTMLElement, options: ZineOptions); /** Resolves once the renderer is mounted and the first spread is painted. */ get ready(): Promise; getPageCount(): number; getPage(): number; /** The element this flipbook was mounted into. */ get container(): HTMLElement; /** * Where the book is actually painted inside the container, in container pixels: the * aspect-fitted, centred region, which is shorter (or narrower) than the container whenever the * two aspect-ratios do not match. A side rail uses it to stand exactly as tall as the page rather * than the whole container. Null before the first paint, when there is nothing to measure yet. */ getPageBox(): { x: number; y: number; width: number; height: number; } | null; /** * How pages are currently grouped, one entry per spread — the same model the renderer paints, * so it already reflects `spreadMode` and the responsive single-page fallback. Useful for * building a page list or thumbnail rail that matches the book. */ getSpreads(): readonly Spread[]; /** The spread index currently on screen; indexes into {@link getSpreads}. */ getSpreadIndex(): number; /** Reading direction, which mirrors each spread's left/right sides. */ getDirection(): Direction; /** How pages are grouped: the configured mode, not what a narrow container may be forcing. */ getSpreadMode(): SpreadMode; /** Whether the book is showing one page at a time, for whatever reason. */ isSinglePage(): boolean; /** * Regroup the pages, keeping the reader on the page they are looking at. * * A narrow container may still override this with one page at a time; see * {@link setResponsiveSpread}. */ setSpreadMode(mode: SpreadMode): void; /** * Switch between one page and two, and back again. * * Returning to two pages restores the mode the book was built with, so a `cover` book gets its * lone first page back rather than becoming a plain `double`. */ toggleSpreadMode(): void; /** Whether a narrow container is allowed to override `spreadMode` with one page at a time. */ getResponsiveSpread(): boolean; /** * Whether the book is showing one page at a time *because the container is narrow*, rather * than because it was asked to. False when `responsiveSpread` is off. * * A book already set to `single` is not being overridden, however narrow it is: asking for one * page makes the book itself narrower, so reporting that as an override would strand the reader * in a layout they could no longer switch out of. */ isResponsiveSingle(): boolean; /** * Allow or forbid the narrow-container override, and re-lay out at once. * * Turning it off holds the configured `spreadMode` at every width; turning it back on * re-measures, so a book on a narrow screen collapses to single again straight away. */ setResponsiveSpread(enabled: boolean): void; /** * Resolve one page's raster, for drawing a thumbnail or exporting. Returns null if the page * cannot be decoded — the failure is reported through `sourceError` rather than thrown, as it * is for the pages the renderer paints. */ getPageImage(index: number): Promise; /** Whether there is a spread after the current one (false on the last). */ canFlipNext(): boolean; /** Whether there is a spread before the current one (false on the first). */ canFlipPrev(): boolean; /** The ceiling `setZoom` clamps to. */ getMaxZoom(): number; /** Whether this book's source can produce text — false for image books. */ canSearch(): boolean; /** Whether the original document can be downloaded — true for a PDF from a URL or bytes. */ canDownload(): boolean; /** * Whether the book is a document rather than a set of loose images — true for a PDF, false for * an `ImageSource`. Document-shaped features (the page rail, the outline) key off this. * * Inferred from the source implementing document-only capabilities, so a custom source opts in * simply by implementing them. */ isDocument(): boolean; /** * Whether this book can be printed: the source must have an original document, and the browser * must render PDFs inline. {@link print} loads the file into an offscreen iframe and drives it * with `window.print()`, which needs a built-in PDF viewer; Android Chrome has none, so the * frame never lays out and the print silently does nothing. `pdfViewerEnabled` reports exactly * that capability, so hide the control when it is explicitly false. It is only hidden on that * explicit false — a browser too old to report the property (undefined) keeps the button. */ canPrint(): boolean; /** * Print the document. * * The original file is handed to the browser in an offscreen frame rather than printing the * host page: printing the page would capture the toolbar and whatever single spread happens to * be on screen, while the browser paginates a PDF properly on its own. Resolves false when * there is no original to print — an image book, or a caller-owned pdf.js document. */ print(): Promise; /** Whether the source can supply a table of contents. True for PDFs; the document may still * turn out to have no outline, in which case {@link getOutline} resolves empty. */ canOutline(): boolean; /** * The document's table of contents, or an empty list when it has none. * * Memoized: an outline does not change for the life of the document, and resolving one means * following every entry's destination to a page. The toolbar asks for it up front to decide * whether to offer the outline panel at all, and the panel itself then gets it for free. */ getOutline(): Promise; /** * Save the original document. Resolves to false when the source has nothing to hand over * (an image book, or a PDF opened from a caller-owned pdf.js document). */ /** * Where the original document lives, if the source has one — a PDF's URL or a blob of its * bytes. Null for an image book, or a PDF opened from a caller-owned pdf.js document. * * Saving and printing both start here: the original is better than anything reassembled from * page rasters, and the browser already knows how to paginate it. */ getSourceFile(): Promise; download(): Promise; /** * Find `query` in the book's text, case-insensitively. * * Pages are read on demand and cached by the source, so the first search over a long document * costs one text extraction per page and later ones are cheap. Returns at most one hit per * page, in page order. Always empty when {@link canSearch} is false. */ search(query: string, options?: { limit?: number; }): Promise; flipNext(): void; flipPrev(): void; flipTo(page: number): void; getZoom(): number; /** Zoom to `scale` (clamped to [1, zoom.max]), keeping `center` (container-local) fixed. */ setZoom(scale: number, center?: { x: number; y: number; }): void; resetZoom(): void; /** The resolved reader-facing text (English defaults merged with the `strings` option). Read by * the controls and panels so a translation reaches every label and announcement. */ get strings(): ZineStrings; /** Whether page-flip sound is enabled for this book (the `sound` option was set). */ isSoundEnabled(): boolean; /** Whether flip sound is currently muted. Always false when sound is not enabled. */ isSoundMuted(): boolean; /** Mute or unmute the flip sound. Remembered even if the sound controller is still loading, and * a no-op when `sound` was never enabled. */ setSoundMuted(muted: boolean): void; on(event: K, listener: (payload: ZineEventMap[K]) => void): () => void; destroy(): void; /** The URL that opens this book at `page` (defaults to the current one). */ pageLink(page?: number): string; /** Re-measure and re-render; call after the container resizes. */ update(): void; }