@zinejs/core 0.4.1 → 0.6.0
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/README.md +11 -0
- package/dist/controls/arrows.d.ts +17 -0
- package/dist/controls/builtins.d.ts +8 -0
- package/dist/controls/controls.d.ts +14 -0
- package/dist/controls/icons.d.ts +51 -0
- package/dist/controls/outline.d.ts +14 -0
- package/dist/controls/qr.d.ts +16 -0
- package/dist/controls/registry.d.ts +21 -0
- package/dist/controls/search.d.ts +13 -0
- package/dist/controls/share.d.ts +13 -0
- package/dist/controls/sidebar.d.ts +24 -0
- package/dist/controls/socials.d.ts +16 -0
- package/dist/controls/styles.d.ts +3 -0
- package/dist/controls/thumbnails.d.ts +17 -0
- package/dist/controls/toolbar.d.ts +45 -0
- package/dist/controls/types.d.ts +81 -0
- package/dist/controls-BrPo8pKL.js +649 -0
- package/dist/controls-BrPo8pKL.js.map +1 -0
- package/dist/engine/deeplink.d.ts +18 -0
- package/dist/engine/emitter.d.ts +6 -0
- package/dist/engine/zoomTile.d.ts +65 -0
- package/dist/icons-D87p7JMP.js +29 -0
- package/dist/icons-D87p7JMP.js.map +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +765 -348
- package/dist/index.js.map +1 -1
- package/dist/index.umd.js +447 -5
- package/dist/index.umd.js.map +1 -1
- package/dist/registry-5nuYFMd4.js +36 -0
- package/dist/registry-5nuYFMd4.js.map +1 -0
- package/dist/share-CyrZiFqf.js +283 -0
- package/dist/share-CyrZiFqf.js.map +1 -0
- package/dist/source/types.d.ts +61 -2
- package/dist/zine.d.ts +161 -3
- package/options.schema.json +53 -0
- package/package.json +13 -1
package/README.md
CHANGED
|
@@ -59,9 +59,20 @@ new Zine(container, {
|
|
|
59
59
|
clickToFlip: 'edge', // 'edge' | 'half' | 'off'
|
|
60
60
|
zoom: { enabled: true, max: 4, wheel: true, doubleClick: [1, 2, 4] },
|
|
61
61
|
renderer: 'auto', // 'auto' | 'css' | 'webgl2' | a custom Renderer
|
|
62
|
+
controls: true, // built-in toolbar; false for none, or { position, items, … }
|
|
62
63
|
});
|
|
63
64
|
```
|
|
64
65
|
|
|
66
|
+
## Controls
|
|
67
|
+
|
|
68
|
+
A toolbar (paging, page number, zoom, search, share, a `⋮` menu with download, and fullscreen)
|
|
69
|
+
is rendered below the book by default. Turn it off with `controls: false`, move it with
|
|
70
|
+
`controls: { position: 'top' }`, float it over the book with `controls: { docked: false }`, or
|
|
71
|
+
choose the buttons with `controls: { items: [...] }`. Register your own with `defineControl`.
|
|
72
|
+
|
|
73
|
+
The toolbar and its icons load as a separate chunk, so a book with `controls: false` never
|
|
74
|
+
downloads them. See the [full docs](https://github.com/awecode/zinejs#controls).
|
|
75
|
+
|
|
65
76
|
See the [root README](../../README.md#curl-models) for what each curl model does.
|
|
66
77
|
## Methods & events
|
|
67
78
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Zine } from '../zine';
|
|
2
|
+
/**
|
|
3
|
+
* Big page-turn arrows flanking the book.
|
|
4
|
+
*
|
|
5
|
+
* They sit *beside* the book rather than over it, for the same reason the docked toolbar does:
|
|
6
|
+
* the container's measured box is what the renderer sizes pages from and what pointer
|
|
7
|
+
* hit-testing maps into, so anything inside it would shrink the book and skew every tap. The
|
|
8
|
+
* book (with whatever the toolbar already wrapped around it) goes in a flex row between them.
|
|
9
|
+
*
|
|
10
|
+
* An arrow disables itself at the end of the book rather than disappearing, so the layout does
|
|
11
|
+
* not jump as the reader reaches the covers.
|
|
12
|
+
*/
|
|
13
|
+
export declare class Arrows {
|
|
14
|
+
#private;
|
|
15
|
+
constructor(zine: Zine, container: HTMLElement);
|
|
16
|
+
destroy(): void;
|
|
17
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Register the stock controls. Called once as the toolbar chunk loads, so the icon set and these
|
|
3
|
+
* definitions stay out of the base bundle.
|
|
4
|
+
*
|
|
5
|
+
* `pageInput` and `search` are not here: both need the toolbar instance itself (one builds an
|
|
6
|
+
* input, the other opens an anchored panel), so the toolbar registers them.
|
|
7
|
+
*/
|
|
8
|
+
export declare function registerBuiltins(): void;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ControlsOptions } from './types';
|
|
2
|
+
import type { Zine } from '../zine';
|
|
3
|
+
export { defineControl, getControl, DEFAULT_ITEMS } from './registry';
|
|
4
|
+
export { ICONS, createIcon } from './icons';
|
|
5
|
+
export type { ControlContext, ControlDef, ControlItem, ControlsOptions, ControlsPosition, } from './types';
|
|
6
|
+
/**
|
|
7
|
+
* Build the toolbar inside `container` and return its teardown.
|
|
8
|
+
*
|
|
9
|
+
* This module is the lazy chunk `zine.ts` reaches through a dynamic `import()`, so a book built
|
|
10
|
+
* with `controls: false` never downloads the toolbar, its icons or its stylesheet. The file is
|
|
11
|
+
* named `controls.ts` rather than `index.ts` so the emitted chunk is `controls-<hash>.js`, which
|
|
12
|
+
* is what the size-limit budget watches.
|
|
13
|
+
*/
|
|
14
|
+
export declare function mountControls(zine: Zine, container: HTMLElement, options?: ControlsOptions): () => void;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Toolbar glyphs, inlined so the bundle carries no icon-font or sprite dependency.
|
|
3
|
+
*
|
|
4
|
+
* Paths are from Lucide (https://lucide.dev), ISC licensed:
|
|
5
|
+
* Copyright (c) for portions of Lucide are held by Cole Bemis 2013-2022 as part of Feather
|
|
6
|
+
* (MIT). All other copyright (c) for Lucide are held by Lucide Contributors 2022.
|
|
7
|
+
*
|
|
8
|
+
* Each value is the inner markup of a 24x24 `viewBox` drawn with `stroke="currentColor"`,
|
|
9
|
+
* `fill="none"` and round caps/joins — {@link createIcon} supplies those, so the strings stay to
|
|
10
|
+
* the geometry.
|
|
11
|
+
*/
|
|
12
|
+
export declare const ICONS: {
|
|
13
|
+
/** chevron-left */
|
|
14
|
+
prev: string;
|
|
15
|
+
/** chevron-right */
|
|
16
|
+
next: string;
|
|
17
|
+
/** chevrons-left */
|
|
18
|
+
first: string;
|
|
19
|
+
/** chevrons-right */
|
|
20
|
+
last: string;
|
|
21
|
+
/** zoom-in */
|
|
22
|
+
zoomIn: string;
|
|
23
|
+
/** zoom-out */
|
|
24
|
+
zoomOut: string;
|
|
25
|
+
/** search */
|
|
26
|
+
search: string;
|
|
27
|
+
/** share-2 */
|
|
28
|
+
share: string;
|
|
29
|
+
/** ellipsis-vertical */
|
|
30
|
+
menu: string;
|
|
31
|
+
/** maximize */
|
|
32
|
+
fullscreen: string;
|
|
33
|
+
/** minimize */
|
|
34
|
+
exitFullscreen: string;
|
|
35
|
+
/** gallery-vertical-end */
|
|
36
|
+
thumbnails: string;
|
|
37
|
+
/** list-tree */
|
|
38
|
+
outline: string;
|
|
39
|
+
/** book-open: two facing pages */
|
|
40
|
+
twoPages: string;
|
|
41
|
+
/** file: a single page */
|
|
42
|
+
onePage: string;
|
|
43
|
+
/** printer */
|
|
44
|
+
print: string;
|
|
45
|
+
/** download */
|
|
46
|
+
download: string;
|
|
47
|
+
/** x */
|
|
48
|
+
close: string;
|
|
49
|
+
};
|
|
50
|
+
/** Build an `<svg>` for one of the {@link ICONS}, or null if the name is unknown. */
|
|
51
|
+
export declare function createIcon(doc: Document, markup: string): SVGSVGElement;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Zine } from '../zine';
|
|
2
|
+
/**
|
|
3
|
+
* The document's table of contents beside the book.
|
|
4
|
+
*
|
|
5
|
+
* One row per heading, nested entries indented under their parent. Clicking a row turns to the
|
|
6
|
+
* page it points at; a heading whose destination could not be resolved is still listed, just not
|
|
7
|
+
* clickable. Unlike the thumbnail rail this is a single column — headings are text, and a second
|
|
8
|
+
* column would only make them narrower.
|
|
9
|
+
*/
|
|
10
|
+
export declare class Outline {
|
|
11
|
+
#private;
|
|
12
|
+
constructor(zine: Zine, container: HTMLElement);
|
|
13
|
+
destroy(): void;
|
|
14
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A small QR encoder, enough to turn a URL into a scannable grid.
|
|
3
|
+
*
|
|
4
|
+
* Written rather than pulled in so the package keeps its "no dependencies" promise, and so no
|
|
5
|
+
* reader's document URL is handed to a third-party image service to be logged. Deliberately
|
|
6
|
+
* narrow: byte mode, error-correction level M, versions 1-10 (up to 271 characters at level M),
|
|
7
|
+
* which covers any sane page URL. Longer input throws rather than silently producing a code that
|
|
8
|
+
* will not scan.
|
|
9
|
+
*/
|
|
10
|
+
/** Encode `text` as a QR matrix: a square of 0/1 by row. */
|
|
11
|
+
export declare function encodeQr(text: string): {
|
|
12
|
+
size: number;
|
|
13
|
+
modules: Uint8Array;
|
|
14
|
+
};
|
|
15
|
+
/** Render a QR matrix as an SVG string, sized to `px`. */
|
|
16
|
+
export declare function qrSvg(text: string, px: number): string;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ControlDef, ControlItem } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Add a control, or replace one by reusing its id. Returns the definition so it can be dropped
|
|
4
|
+
* straight into a layout.
|
|
5
|
+
*
|
|
6
|
+
* ```js
|
|
7
|
+
* defineControl({ id: 'print', title: 'Print', icon: '<path d="…"/>', action: () => print() });
|
|
8
|
+
* new Zine(el, { source, controls: { items: ['prev', 'next', 'print'] } });
|
|
9
|
+
* ```
|
|
10
|
+
*/
|
|
11
|
+
export declare function defineControl(def: ControlDef): ControlDef;
|
|
12
|
+
/** Look up a registered control. */
|
|
13
|
+
export declare function getControl(id: string): ControlDef | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* Turn a layout entry into a concrete definition. A bare string names a registered control; an
|
|
16
|
+
* object with only some fields is layered over the registered one, so `{ id: 'next', title: 'Go
|
|
17
|
+
* on' }` relabels the built-in without reimplementing it.
|
|
18
|
+
*/
|
|
19
|
+
export declare function resolveControl(item: ControlItem): ControlDef;
|
|
20
|
+
/** The toolbar shown when `items` is not given. */
|
|
21
|
+
export declare const DEFAULT_ITEMS: readonly ControlItem[];
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Zine } from '../zine';
|
|
2
|
+
/**
|
|
3
|
+
* Full-text search beside the book: a query field over its results.
|
|
4
|
+
*
|
|
5
|
+
* It lives in the same rail as the thumbnails and outline rather than a popover, because results
|
|
6
|
+
* are an open-ended list — a floating panel over the book would either cover the page being
|
|
7
|
+
* searched or clip the very results it found.
|
|
8
|
+
*/
|
|
9
|
+
export declare class Search {
|
|
10
|
+
#private;
|
|
11
|
+
constructor(zine: Zine, container: HTMLElement);
|
|
12
|
+
destroy(): void;
|
|
13
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Zine } from '../zine';
|
|
2
|
+
/**
|
|
3
|
+
* The share dialog: a QR code, the link, a copy button and the usual social destinations.
|
|
4
|
+
*
|
|
5
|
+
* Loaded on demand — the whole module, its icons and the QR encoder arrive only when a reader
|
|
6
|
+
* presses Share, so a book nobody shares pays nothing for it.
|
|
7
|
+
*/
|
|
8
|
+
export declare class ShareDialog {
|
|
9
|
+
#private;
|
|
10
|
+
constructor(zine: Zine, container: HTMLElement);
|
|
11
|
+
close(): void;
|
|
12
|
+
destroy(): void;
|
|
13
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The rail beside the book that the thumbnail and outline lists both sit in.
|
|
3
|
+
*
|
|
4
|
+
* It exists to solve one problem shared by every such panel: the container's measured box is what
|
|
5
|
+
* the renderer sizes the book from and what pointer hit-testing maps into, so a panel must not
|
|
6
|
+
* live inside it. Instead the book (and its docked toolbar, if any) is wrapped in a flex row with
|
|
7
|
+
* the panel as its sibling, and `destroy` unwinds that again.
|
|
8
|
+
*
|
|
9
|
+
* The rail is absolutely positioned inside a holder that stretches to the book's height, so a
|
|
10
|
+
* long list scrolls internally instead of running past the bottom of the book.
|
|
11
|
+
*/
|
|
12
|
+
export declare class Sidebar {
|
|
13
|
+
#private;
|
|
14
|
+
/** The scrolling list; panels append their rows here. */
|
|
15
|
+
readonly root: HTMLElement;
|
|
16
|
+
constructor(doc: Document, container: HTMLElement, options: {
|
|
17
|
+
className: string;
|
|
18
|
+
label: string;
|
|
19
|
+
width: number;
|
|
20
|
+
});
|
|
21
|
+
/** Resize the rail, e.g. once a thumbnail list knows whether it needs one column or two. */
|
|
22
|
+
setWidth(px: number): void;
|
|
23
|
+
destroy(): void;
|
|
24
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a reader can send a link, and how each service wants it.
|
|
3
|
+
*
|
|
4
|
+
* These are plain share-intent URLs — no SDKs, no third-party scripts, nothing that could track
|
|
5
|
+
* the reader before they choose to share. Each brand mark is drawn from its own official glyph;
|
|
6
|
+
* they are filled shapes rather than the stroked outlines used elsewhere in the toolbar, so they
|
|
7
|
+
* carry `fill="currentColor"` and no stroke.
|
|
8
|
+
*/
|
|
9
|
+
export interface Social {
|
|
10
|
+
id: string;
|
|
11
|
+
label: string;
|
|
12
|
+
/** Inner SVG markup for a 24x24 viewBox, filled rather than stroked. */
|
|
13
|
+
icon: string;
|
|
14
|
+
href(url: string, title: string): string;
|
|
15
|
+
}
|
|
16
|
+
export declare const SOCIALS: readonly Social[];
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare 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 \u2014\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, #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, 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, 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, rgba(255,255,255,0.14)); }\n.zine-controls-btn:focus-visible {\n outline: 2px solid var(--zine-controls-accent, #7dd3fc);\n outline-offset: 1px;\n}\n.zine-controls-btn:disabled { opacity: 0.38; cursor: default; }\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, 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, rgba(255,255,255,0.2));\n border-radius: 4px;\n background: 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, #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, 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/* 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, rgba(255,255,255,0.2));\n border-radius: 6px;\n background: rgba(0, 0, 0, 0.25);\n color: inherit;\n font: inherit;\n box-sizing: border-box;\n}\n.zine-search-hits {\n flex: 1 1 auto;\n overflow-y: auto;\n overscroll-behavior: contain;\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, rgba(255,255,255,0.14)); }\n.zine-search-hit:focus-visible {\n outline: 2px solid var(--zine-controls-accent, #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: 40px;\n height: 40px;\n padding: 0;\n border: 0;\n border-radius: 50%;\n background: var(--zine-controls-bg, rgba(24, 24, 27, 0.82));\n color: var(--zine-controls-fg, #f4f4f5);\n cursor: pointer;\n backdrop-filter: blur(8px);\n -webkit-user-select: none;\n user-select: none;\n}\n.zine-arrow svg { width: 22px; height: 22px; }\n.zine-arrow:hover:not(:disabled) { background: var(--zine-controls-hover, rgba(255,255,255,0.22)); }\n.zine-arrow:focus-visible {\n outline: 2px solid var(--zine-controls-accent, #7dd3fc);\n outline-offset: 2px;\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/* No room to flank on a narrow screen; the toolbar's own prev/next still work. */\n@media (max-width: 640px) {\n .zine-arrow { display: none; }\n}\n@media (prefers-reduced-motion: no-preference) {\n .zine-arrow { transition: background 120ms ease; }\n}\n\n/* Side panels (thumbnails, outline). The rail wraps the book and its docked toolbar so it sits\n beside them without shrinking the container the renderer measures. */\n.zine-panel-wrap { display: flex; align-items: stretch; gap: 8px; }\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.zine-panel {\n position: absolute;\n inset: 0;\n display: flex;\n flex-direction: column;\n gap: 6px;\n overflow-y: auto;\n overscroll-behavior: contain;\n padding: 6px;\n box-sizing: border-box;\n border-radius: 8px;\n background: var(--zine-controls-bg, rgba(24, 24, 27, 0.82));\n color: var(--zine-controls-fg, #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/* 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, rgba(255,255,255,0.18)); }\n.zine-panel-note { padding: 8px; opacity: 0.66; line-height: 1.4; }\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, rgba(255,255,255,0.14)); }\n.zine-thumbs-row:focus-visible {\n outline: 2px solid var(--zine-controls-accent, #7dd3fc);\n outline-offset: -2px;\n}\n.zine-thumbs-row.zine-panel-active .zine-thumbs-cell {\n outline: 1px solid var(--zine-controls-accent, #7dd3fc);\n}\n\n/* Outline: a single column of headings, indented by depth. */\n.zine-outline { gap: 1px; }\n.zine-outline-row {\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 line-height: 1.35;\n cursor: pointer;\n box-sizing: border-box;\n overflow-wrap: anywhere;\n}\n.zine-outline-row:hover:not(:disabled) {\n background: var(--zine-controls-hover, rgba(255,255,255,0.14));\n}\n.zine-outline-row:focus-visible {\n outline: 2px solid var(--zine-controls-accent, #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: 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, #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, #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, 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, rgba(255,255,255,0.2));\n border-radius: 7px;\n background: 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, #7dd3fc);\n color: #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: 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, 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, #7dd3fc);\n outline-offset: 2px;\n}\n\n@media (prefers-reduced-motion: no-preference) {\n .zine-controls-btn { transition: background 120ms ease; }\n}\n";
|
|
2
|
+
/** Add the stylesheet to `doc` if it is not already there. */
|
|
3
|
+
export declare function ensureStyles(doc: Document): void;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Zine } from '../zine';
|
|
2
|
+
/**
|
|
3
|
+
* A rail of page thumbnails beside the book.
|
|
4
|
+
*
|
|
5
|
+
* Rows mirror the book's own spread model, so a `double` book shows two pages per row and a
|
|
6
|
+
* `single` book one — including the responsive fallback that forces single-page on a narrow
|
|
7
|
+
* container. A book that mixes the two (`cover`, `book`) keeps two columns so pairs line up, and
|
|
8
|
+
* centres its lone first/last page between them. Clicking a row turns to that spread.
|
|
9
|
+
*
|
|
10
|
+
* Pages are drawn only as they scroll into view: a long PDF would otherwise decode every page at
|
|
11
|
+
* once the moment the rail opened.
|
|
12
|
+
*/
|
|
13
|
+
export declare class Thumbnails {
|
|
14
|
+
#private;
|
|
15
|
+
constructor(zine: Zine, container: HTMLElement);
|
|
16
|
+
destroy(): void;
|
|
17
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Thumbnails } from './thumbnails';
|
|
2
|
+
import { Outline } from './outline';
|
|
3
|
+
import { Search } from './search';
|
|
4
|
+
/** The side panels, all of which share the one rail beside the book. */
|
|
5
|
+
declare const PANELS: {
|
|
6
|
+
thumbnails: typeof Thumbnails;
|
|
7
|
+
outline: typeof Outline;
|
|
8
|
+
search: typeof Search;
|
|
9
|
+
};
|
|
10
|
+
type PanelKind = keyof typeof PANELS;
|
|
11
|
+
import type { ControlsOptions } from './types';
|
|
12
|
+
import type { Zine } from '../zine';
|
|
13
|
+
export declare class Toolbar {
|
|
14
|
+
#private;
|
|
15
|
+
constructor(zine: Zine, container: HTMLElement, options: ControlsOptions);
|
|
16
|
+
/** Build the layout. Separate from construction so instance-bound widgets (the page field, the
|
|
17
|
+
* search panel) can be registered against this toolbar first. */
|
|
18
|
+
mount(options: ControlsOptions): void;
|
|
19
|
+
destroy(): void;
|
|
20
|
+
/** Registered lazily by {@link registerWidgets} so the widget can reach this instance. */
|
|
21
|
+
makePageInput(): HTMLElement;
|
|
22
|
+
/** Which side panel is showing, if any. */
|
|
23
|
+
openPanel(): PanelKind | null;
|
|
24
|
+
/**
|
|
25
|
+
* Whether this document has a table of contents worth offering.
|
|
26
|
+
*
|
|
27
|
+
* Many PDFs have none, and a control that can only report its own emptiness is a dead end, so
|
|
28
|
+
* the outline button stays hidden until the outline is known to be non-empty. The first call
|
|
29
|
+
* kicks off the lookup and refreshes the toolbar when it lands; `Zine.getOutline` memoizes, so
|
|
30
|
+
* the panel does not pay for it twice.
|
|
31
|
+
*/
|
|
32
|
+
hasOutline(): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Show one of the side panels, or close it if it is already up. They share the rail beside the
|
|
35
|
+
* book, so opening one closes the others. Rebuilt each time, so the thumbnail rail always
|
|
36
|
+
* matches the current spread grouping (which `spreadMode` and the responsive fallback change).
|
|
37
|
+
*/
|
|
38
|
+
togglePanel(kind: PanelKind): void;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Controls that need the toolbar itself (a DOM widget, or a panel to open) are registered per
|
|
42
|
+
* instance, since the registry is module-level and shared.
|
|
43
|
+
*/
|
|
44
|
+
export declare function registerWidgets(toolbar: Toolbar): void;
|
|
45
|
+
export {};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { Zine } from '../zine';
|
|
2
|
+
/** Where the toolbar sits relative to the book. */
|
|
3
|
+
export type ControlsPosition = 'top' | 'bottom' | 'left' | 'right';
|
|
4
|
+
/** What a control is handed when it runs or reports its state. */
|
|
5
|
+
export interface ControlContext {
|
|
6
|
+
/** The flipbook this toolbar drives. */
|
|
7
|
+
readonly zine: Zine;
|
|
8
|
+
/** Dismiss the submenu this control lives in (no-op at the top level). */
|
|
9
|
+
close(): void;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* One button in the toolbar.
|
|
13
|
+
*
|
|
14
|
+
* A control is either an action (`action`), a submenu (`children`), or a bespoke widget
|
|
15
|
+
* (`render`) — the page-number field is the built-in example of the last. The `is*` predicates are
|
|
16
|
+
* re-read whenever the book changes, so a control reflects state without subscribing to anything.
|
|
17
|
+
*/
|
|
18
|
+
export interface ControlDef {
|
|
19
|
+
/** Unique key; also how a layout refers to this control. */
|
|
20
|
+
readonly id: string;
|
|
21
|
+
/**
|
|
22
|
+
* Tooltip and accessible name. A function is re-read whenever the book changes, so a toggle
|
|
23
|
+
* can say what it will do next ("Show thumbnails" / "Hide thumbnails").
|
|
24
|
+
*/
|
|
25
|
+
readonly title: string | ((ctx: ControlContext) => string);
|
|
26
|
+
/**
|
|
27
|
+
* Inner SVG markup, drawn inside a 24x24 viewBox. See `./icons`. A function is re-read
|
|
28
|
+
* whenever the book changes, so a control can mirror its glyph in RTL or swap it for state.
|
|
29
|
+
*/
|
|
30
|
+
readonly icon?: string | ((ctx: ControlContext) => string);
|
|
31
|
+
/** Nested controls. A control with children opens a submenu instead of acting. */
|
|
32
|
+
readonly children?: readonly ControlItem[];
|
|
33
|
+
/** What the button does. Ignored when `children` is present. */
|
|
34
|
+
action?(ctx: ControlContext): void;
|
|
35
|
+
/** Build a custom element instead of a button — for inputs and other non-button widgets. */
|
|
36
|
+
render?(ctx: ControlContext): HTMLElement;
|
|
37
|
+
/**
|
|
38
|
+
* Whether the control applies at all — search on a book with no text, download with no file.
|
|
39
|
+
* A control that fails this is removed outright, since it will not come back.
|
|
40
|
+
*
|
|
41
|
+
* For something that comes and goes as the reader moves, prefer `isDisabled`: a control that
|
|
42
|
+
* vanished and reappeared would shift every button beside it on the bar.
|
|
43
|
+
*/
|
|
44
|
+
isVisible?(ctx: ControlContext): boolean;
|
|
45
|
+
/** Grey out and block the action, e.g. "previous" on the first spread. */
|
|
46
|
+
isDisabled?(ctx: ControlContext): boolean;
|
|
47
|
+
/** Mark as currently on, e.g. fullscreen while engaged. */
|
|
48
|
+
isActive?(ctx: ControlContext): boolean;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* An entry in a layout: a registered control's id, a `'|'` separator, or an inline definition.
|
|
52
|
+
* Inline definitions may also be a partial override of a registered control — `{ id: 'next',
|
|
53
|
+
* title: 'Forward' }` keeps the built-in behaviour and changes only the label.
|
|
54
|
+
*/
|
|
55
|
+
export type ControlItem = string | ControlDef | (Partial<ControlDef> & {
|
|
56
|
+
id: string;
|
|
57
|
+
});
|
|
58
|
+
export interface ControlsOptions {
|
|
59
|
+
/** Which edge of the book the toolbar sits on; default 'bottom'. */
|
|
60
|
+
position?: ControlsPosition;
|
|
61
|
+
/**
|
|
62
|
+
* Sit outside the book (default) or float over it.
|
|
63
|
+
*
|
|
64
|
+
* Docked is the default because a bar over the page hides content. It also keeps the book's
|
|
65
|
+
* own box untouched — the container's measured size drives layout, hit-testing and the
|
|
66
|
+
* aspect-ratio the library writes — so the toolbar goes in as a sibling of the container
|
|
67
|
+
* rather than a child of it, and its clicks never reach the book's gesture handlers at all.
|
|
68
|
+
*
|
|
69
|
+
* Set `false` to overlay the toolbar on the book instead.
|
|
70
|
+
*/
|
|
71
|
+
docked?: boolean;
|
|
72
|
+
/** The toolbar layout. Omit for the default set; pass an array to replace it outright. */
|
|
73
|
+
items?: readonly ControlItem[];
|
|
74
|
+
/**
|
|
75
|
+
* Large page-turn arrows flanking the book, as most flipbooks show. Default true. They sit
|
|
76
|
+
* beside the pages rather than over them, so they never cover content.
|
|
77
|
+
*/
|
|
78
|
+
arrows?: boolean;
|
|
79
|
+
/** Extra class on the toolbar root, for styling hooks. */
|
|
80
|
+
className?: string;
|
|
81
|
+
}
|