@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.
@@ -0,0 +1,36 @@
1
+ //#region src/controls/registry.ts
2
+ var e = /* @__PURE__ */ new Map();
3
+ function t(t) {
4
+ return e.set(t.id, t), t;
5
+ }
6
+ function n(t) {
7
+ return e.get(t);
8
+ }
9
+ function r(t) {
10
+ if (typeof t == "string") {
11
+ let n = e.get(t);
12
+ if (!n) throw Error(`controls: unknown control '${t}'. Register it with defineControl() or pass a definition object.`);
13
+ return n;
14
+ }
15
+ let n = e.get(t.id);
16
+ return n ? {
17
+ ...n,
18
+ ...t
19
+ } : t;
20
+ }
21
+ var i = [
22
+ "prev",
23
+ "pageInput",
24
+ "next",
25
+ "|",
26
+ "zoomOut",
27
+ "zoomIn",
28
+ "search",
29
+ "share",
30
+ "menu",
31
+ "fullscreen"
32
+ ];
33
+ //#endregion
34
+ export { r as i, t as n, n as r, i as t };
35
+
36
+ //# sourceMappingURL=registry-5nuYFMd4.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry-5nuYFMd4.js","names":[],"sources":["../src/controls/registry.ts"],"sourcesContent":["import type { ControlDef, ControlItem } from './types';\n\n/**\n * The control registry.\n *\n * Deliberately just the mechanism — no icons, no built-in definitions — because `defineControl`\n * has to be callable before the toolbar chunk loads, so this module lands in the base bundle.\n * The built-ins live in `./builtins`, which the lazy chunk pulls in along with the icon set.\n */\nconst registry = new Map<string, ControlDef>();\n\n/**\n * Add a control, or replace one by reusing its id. Returns the definition so it can be dropped\n * straight into a layout.\n *\n * ```js\n * defineControl({ id: 'print', title: 'Print', icon: '<path d=\"…\"/>', action: () => print() });\n * new Zine(el, { source, controls: { items: ['prev', 'next', 'print'] } });\n * ```\n */\nexport function defineControl(def: ControlDef): ControlDef {\n registry.set(def.id, def);\n return def;\n}\n\n/** Look up a registered control. */\nexport function getControl(id: string): ControlDef | undefined {\n return registry.get(id);\n}\n\n/**\n * Turn a layout entry into a concrete definition. A bare string names a registered control; an\n * object with only some fields is layered over the registered one, so `{ id: 'next', title: 'Go\n * on' }` relabels the built-in without reimplementing it.\n */\nexport function resolveControl(item: ControlItem): ControlDef {\n if (typeof item === 'string') {\n const found = registry.get(item);\n if (!found) {\n throw new Error(\n `controls: unknown control '${item}'. Register it with defineControl() or pass a definition object.`,\n );\n }\n return found;\n }\n const base = registry.get(item.id);\n if (!base) return item as ControlDef;\n return { ...base, ...item };\n}\n\n/** The toolbar shown when `items` is not given. */\nexport const DEFAULT_ITEMS: readonly ControlItem[] = [\n 'prev',\n 'pageInput',\n 'next',\n '|',\n 'zoomOut',\n 'zoomIn',\n 'search',\n 'share',\n 'menu',\n 'fullscreen',\n];\n"],"mappings":";AASA,IAAM,oBAAW,IAAI,IAAwB;AAW7C,SAAgB,EAAc,GAA6B;CAEzD,OADA,EAAS,IAAI,EAAI,IAAI,CAAG,GACjB;AACT;AAGA,SAAgB,EAAW,GAAoC;CAC7D,OAAO,EAAS,IAAI,CAAE;AACxB;AAOA,SAAgB,EAAe,GAA+B;CAC5D,IAAI,OAAO,KAAS,UAAU;EAC5B,IAAM,IAAQ,EAAS,IAAI,CAAI;EAC/B,IAAI,CAAC,GACH,MAAU,MACR,8BAA8B,EAAK,iEACrC;EAEF,OAAO;CACT;CACA,IAAM,IAAO,EAAS,IAAI,EAAK,EAAE;CAEjC,OADK,IACE;EAAE,GAAG;EAAM,GAAG;CAAK,IADR;AAEpB;AAGA,IAAa,IAAwC;CACnD;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF"}
@@ -0,0 +1,283 @@
1
+ import { n as e, t } from "./icons-D87p7JMP.js";
2
+ //#region src/controls/socials.ts
3
+ var n = encodeURIComponent, r = [
4
+ {
5
+ id: "facebook",
6
+ label: "Facebook",
7
+ 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\"/>",
8
+ href: (e) => `https://www.facebook.com/sharer/sharer.php?u=${n(e)}`
9
+ },
10
+ {
11
+ id: "x",
12
+ label: "X (Twitter)",
13
+ 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\"/>",
14
+ href: (e, t) => `https://twitter.com/intent/tweet?url=${n(e)}&text=${n(t)}`
15
+ },
16
+ {
17
+ id: "linkedin",
18
+ label: "LinkedIn",
19
+ 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\"/>",
20
+ href: (e) => `https://www.linkedin.com/sharing/share-offsite/?url=${n(e)}`
21
+ },
22
+ {
23
+ id: "whatsapp",
24
+ label: "WhatsApp",
25
+ 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\"/>",
26
+ href: (e, t) => `https://api.whatsapp.com/send?text=${n(`${t} ${e}`)}`
27
+ },
28
+ {
29
+ id: "pinterest",
30
+ label: "Pinterest",
31
+ 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\"/>",
32
+ href: (e, t) => `https://pinterest.com/pin/create/button/?url=${n(e)}&description=${n(t)}`
33
+ },
34
+ {
35
+ id: "email",
36
+ label: "Email",
37
+ 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\"/>",
38
+ href: (e, t) => `mailto:?subject=${n(t)}&body=${n(e)}`
39
+ }
40
+ ], i = [
41
+ 16,
42
+ 28,
43
+ 44,
44
+ 64,
45
+ 86,
46
+ 108,
47
+ 124,
48
+ 154,
49
+ 182,
50
+ 216
51
+ ], a = [
52
+ 10,
53
+ 16,
54
+ 26,
55
+ 18,
56
+ 24,
57
+ 16,
58
+ 18,
59
+ 22,
60
+ 22,
61
+ 26
62
+ ], o = [
63
+ 1,
64
+ 1,
65
+ 1,
66
+ 2,
67
+ 2,
68
+ 4,
69
+ 4,
70
+ 4,
71
+ 5,
72
+ 5
73
+ ], s = [
74
+ [],
75
+ [6, 18],
76
+ [6, 22],
77
+ [6, 26],
78
+ [6, 30],
79
+ [6, 34],
80
+ [
81
+ 6,
82
+ 22,
83
+ 38
84
+ ],
85
+ [
86
+ 6,
87
+ 24,
88
+ 42
89
+ ],
90
+ [
91
+ 6,
92
+ 26,
93
+ 46
94
+ ],
95
+ [
96
+ 6,
97
+ 28,
98
+ 50
99
+ ]
100
+ ], c = /* @__PURE__ */ new Uint8Array(512), l = /* @__PURE__ */ new Uint8Array(256);
101
+ for (let e = 0, t = 1; e < 255; e++) c[e] = t, l[t] = e, t <<= 1, t & 256 && (t ^= 285);
102
+ for (let e = 255; e < 512; e++) c[e] = c[e - 255];
103
+ var u = (e, t) => e === 0 || t === 0 ? 0 : c[(l[e] + l[t]) % 255];
104
+ function d(e) {
105
+ let t = new Uint8Array([1]);
106
+ for (let n = 0; n < e; n++) {
107
+ let e = new Uint8Array(t.length + 1);
108
+ for (let r = 0; r < t.length; r++) e[r] = e[r] ^ t[r], e[r + 1] = e[r + 1] ^ u(t[r], c[n]);
109
+ t = e;
110
+ }
111
+ return t;
112
+ }
113
+ function f(e, t) {
114
+ let n = d(t), r = new Uint8Array(t);
115
+ for (let i of e) {
116
+ let e = i ^ r[0];
117
+ if (r.copyWithin(0, 1), r[t - 1] = 0, e !== 0) for (let i = 0; i < t; i++) r[i] = r[i] ^ u(n[i + 1], e);
118
+ }
119
+ return r;
120
+ }
121
+ function p(e) {
122
+ let t = 0 | e, n = t;
123
+ for (let e = 0; e < 10; e++) n = n << 1 ^ (n >>> 9) * 1335;
124
+ return (t << 10 | n) ^ 21522;
125
+ }
126
+ var m = (e, t, n, r, i = !0) => {
127
+ e.modules[n * e.size + t] = +!!r, i && (e.reserved[n * e.size + t] = 1);
128
+ };
129
+ function h(e, t, n) {
130
+ for (let r = -1; r <= 7; r++) for (let i = -1; i <= 7; i++) {
131
+ let a = t + i, o = n + r;
132
+ if (a < 0 || o < 0 || a >= e.size || o >= e.size) continue;
133
+ let s = Math.max(Math.abs(i - 3), Math.abs(r - 3));
134
+ m(e, a, o, s !== 2 && s <= 3);
135
+ }
136
+ }
137
+ function g(e) {
138
+ let t = new TextEncoder().encode(e), n = i.findIndex((e) => t.length + 2 <= e) + 1;
139
+ if (n === 0) throw Error(`qr: ${t.length} bytes is too long for this encoder (max 214).`);
140
+ let r = i[n - 1], c = [], l = (e, t) => {
141
+ for (let n = t - 1; n >= 0; n--) c.push(e >>> n & 1);
142
+ };
143
+ l(4, 4), l(t.length, n < 10 ? 8 : 16);
144
+ for (let e of t) l(e, 8);
145
+ for (l(0, Math.min(4, r * 8 - c.length)); c.length % 8;) c.push(0);
146
+ let u = new Uint8Array(r);
147
+ for (let e = 0; e < c.length; e += 8) {
148
+ let t = 0;
149
+ for (let n = 0; n < 8; n++) t = t << 1 | c[e + n];
150
+ u[e / 8] = t;
151
+ }
152
+ for (let e = c.length / 8, t = 0; e < r; e++, t++) u[e] = t % 2 == 0 ? 236 : 17;
153
+ let d = o[n - 1], g = a[n - 1], _ = Math.floor(r / d), v = r % d, y = [], b = [];
154
+ for (let e = 0, t = 0; e < d; e++) {
155
+ let n = _ + +(e >= d - v), r = u.subarray(t, t + n);
156
+ t += n, y.push(r), b.push(f(r, g));
157
+ }
158
+ let x = [];
159
+ for (let e = 0; e < _ + 1; e++) for (let t of y) e < t.length && x.push(t[e]);
160
+ for (let e = 0; e < g; e++) for (let t of b) x.push(t[e]);
161
+ let S = n * 4 + 17, C = {
162
+ size: S,
163
+ modules: new Uint8Array(S * S),
164
+ reserved: new Uint8Array(S * S)
165
+ };
166
+ h(C, 0, 0), h(C, S - 7, 0), h(C, 0, S - 7);
167
+ for (let e = 8; e < S - 8; e++) {
168
+ let t = e % 2 == 0;
169
+ m(C, e, 6, t), m(C, 6, e, t);
170
+ }
171
+ for (let e of s[n - 1]) for (let t of s[n - 1]) if (!(e < 9 && t < 9 || e < 9 && t > S - 10 || e > S - 10 && t < 9)) for (let n = -2; n <= 2; n++) for (let r = -2; r <= 2; r++) m(C, e + r, t + n, Math.max(Math.abs(r), Math.abs(n)) !== 1);
172
+ m(C, 8, S - 8, !0);
173
+ for (let e = 0; e < 9; e++) e !== 6 && (m(C, e, 8, !1), m(C, 8, e, !1));
174
+ for (let e = 0; e < 8; e++) m(C, S - 1 - e, 8, !1), m(C, 8, S - 1 - e, !1);
175
+ m(C, 8, S - 8, !0);
176
+ let w = 0, T = x.length * 8;
177
+ for (let e = S - 1; e >= 1; e -= 2) {
178
+ e === 6 && (e = 5);
179
+ for (let t = 0; t < S; t++) for (let n = 0; n < 2; n++) {
180
+ let r = e - n, i = e + 1 & 2 ? t : S - 1 - t;
181
+ if (C.reserved[i * S + r]) continue;
182
+ let a = !1;
183
+ w < T && (a = (x[w >>> 3] >>> 7 - (w & 7) & 1) == 1, w++), (r + i) % 2 == 0 && (a = !a), m(C, r, i, a, !1);
184
+ }
185
+ }
186
+ let E = p(0);
187
+ for (let e = 0; e < 15; e++) {
188
+ let t = (E >>> e & 1) == 1, n = e < 6 ? e : e < 8 ? e + 1 : 8, r = e < 8 ? 8 : e < 9 ? 7 : 14 - e;
189
+ m(C, n, 8, t), m(C, 8, r, t), e < 8 ? m(C, S - 1 - e, 8, t) : m(C, 8, S - 15 + e, t);
190
+ }
191
+ return m(C, 8, S - 8, !0), {
192
+ size: S,
193
+ modules: C.modules
194
+ };
195
+ }
196
+ function _(e, t) {
197
+ let { size: n, modules: r } = g(e), i = "";
198
+ for (let e = 0; e < n; e++) for (let t = 0; t < n; t++) r[e * n + t] && (i += `M${t} ${e}h1v1h-1z`);
199
+ let a = n + 4;
200
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${t}" height="${t}" viewBox="0 0 ${a} ${a}" shape-rendering="crispEdges"><rect width="${a}" height="${a}" fill="#fff"/><g transform="translate(2 2)" fill="#000">${`<path d="${i}"/>`}</g></svg>`;
201
+ }
202
+ //#endregion
203
+ //#region src/controls/share.ts
204
+ var v = 132, y = 1600, b = class {
205
+ #e;
206
+ #t;
207
+ #n;
208
+ #r;
209
+ #i = null;
210
+ #a;
211
+ constructor(n, r) {
212
+ let i = r.ownerDocument;
213
+ this.#e = i, this.#r = i.activeElement;
214
+ let a = n.pageLink(), o = i.title || "Flipbook";
215
+ this.#t = i.createElement("div"), this.#t.className = "zine-share-backdrop", this.#t.addEventListener("pointerdown", (e) => {
216
+ e.target === this.#t && this.close();
217
+ }), this.#n = i.createElement("div"), this.#n.className = "zine-share", this.#n.setAttribute("role", "dialog"), this.#n.setAttribute("aria-modal", "true"), this.#n.setAttribute("aria-label", "Share"), this.#t.appendChild(this.#n);
218
+ let s = i.createElement("div");
219
+ s.className = "zine-share-head";
220
+ let c = i.createElement("strong");
221
+ c.textContent = "Share";
222
+ let l = i.createElement("button");
223
+ l.type = "button", l.className = "zine-share-close", l.setAttribute("aria-label", "Close"), l.appendChild(e(i, t.close)), l.addEventListener("click", () => this.close()), s.append(c, l), this.#n.append(s, this.#s(i, a), this.#l(i, a, o));
224
+ let u = this.#o(i, a);
225
+ u && this.#n.insertBefore(u, this.#n.children[1]), r.ownerDocument.body.appendChild(this.#t), this.#a = (e) => {
226
+ e.key === "Escape" && this.close(), e.key === "Tab" && this.#u(e);
227
+ }, this.#t.addEventListener("keydown", this.#a), l.focus();
228
+ }
229
+ #o(e, t) {
230
+ if (!t) return null;
231
+ try {
232
+ let n = e.createElement("div");
233
+ return n.className = "zine-share-qr", n.innerHTML = _(t, v), n.setAttribute("aria-label", "QR code for this page"), n.setAttribute("role", "img"), n;
234
+ } catch {
235
+ return null;
236
+ }
237
+ }
238
+ #s(e, t) {
239
+ let n = e.createElement("div");
240
+ n.className = "zine-share-link";
241
+ let r = e.createElement("input");
242
+ r.type = "text", r.readOnly = !0, r.value = t, r.setAttribute("aria-label", "Link to this page"), r.addEventListener("focus", () => r.select());
243
+ let i = e.createElement("button");
244
+ return i.type = "button", i.className = "zine-share-copy", i.textContent = "Copy", i.addEventListener("click", () => {
245
+ this.#c(t, i);
246
+ }), n.append(r, i), n;
247
+ }
248
+ async #c(e, t) {
249
+ try {
250
+ await navigator.clipboard?.writeText(e), t.textContent = "Copied";
251
+ } catch {
252
+ t.textContent = "Press Ctrl+C";
253
+ }
254
+ this.#i && clearTimeout(this.#i), this.#i = setTimeout(() => {
255
+ t.textContent = "Copy";
256
+ }, y);
257
+ }
258
+ #l(t, n, i) {
259
+ let a = t.createElement("div");
260
+ a.className = "zine-share-socials";
261
+ for (let o of r) {
262
+ let r = t.createElement("a");
263
+ r.className = "zine-share-social", r.href = o.href(n, i), r.title = o.label, r.setAttribute("aria-label", `Share on ${o.label}`), o.id !== "email" && (r.target = "_blank", r.rel = "noopener noreferrer"), r.appendChild(e(t, o.icon)), a.appendChild(r);
264
+ }
265
+ return a;
266
+ }
267
+ #u(e) {
268
+ let t = [...this.#n.querySelectorAll("button, a[href], input")].filter((e) => !e.hasAttribute("disabled"));
269
+ if (t.length === 0) return;
270
+ let n = t[0], r = t.at(-1), i = this.#e.activeElement;
271
+ e.shiftKey && i === n ? (r.focus(), e.preventDefault()) : !e.shiftKey && i === r && (n.focus(), e.preventDefault());
272
+ }
273
+ close() {
274
+ this.destroy();
275
+ }
276
+ destroy() {
277
+ this.#i && clearTimeout(this.#i), this.#i = null, this.#t.removeEventListener("keydown", this.#a), this.#t.remove(), this.#r?.focus?.();
278
+ }
279
+ };
280
+ //#endregion
281
+ export { b as ShareDialog };
282
+
283
+ //# sourceMappingURL=share-CyrZiFqf.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"share-CyrZiFqf.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 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) {\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 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":";;AAgBA,IAAM,IAAI,oBAEG,IAA6B;CACxC;EACE,IAAI;EACJ,OAAO;EACP,MAAM;EACN,OAAO,MAAQ,gDAAgD,EAAE,CAAG;CACtE;CACA;EACE,IAAI;EACJ,OAAO;EACP,MAAM;EACN,OAAO,GAAK,MAAU,wCAAwC,EAAE,CAAG,EAAE,QAAQ,EAAE,CAAK;CACtF;CACA;EACE,IAAI;EACJ,OAAO;EACP,MAAM;EACN,OAAO,MAAQ,uDAAuD,EAAE,CAAG;CAC7E;CACA;EACE,IAAI;EACJ,OAAO;EACP,MAAM;EACN,OAAO,GAAK,MAAU,sCAAsC,EAAE,GAAG,EAAM,GAAG,GAAK;CACjF;CACA;EACE,IAAI;EACJ,OAAO;EACP,MAAM;EACN,OAAO,GAAK,MAAU,gDAAgD,EAAE,CAAG,EAAE,eAAe,EAAE,CAAK;CACrG;CACA;EACE,IAAI;EACJ,OAAO;EACP,MAAM;EACN,OAAO,GAAK,MAAU,mBAAmB,EAAE,CAAK,EAAE,QAAQ,EAAE,CAAG;CACjE;AACF,GC5CM,IAAiB;CAAC;CAAI;CAAI;CAAI;CAAI;CAAI;CAAK;CAAK;CAAK;CAAK;AAAG,GAE7D,IAAe;CAAC;CAAI;CAAI;CAAI;CAAI;CAAI;CAAI;CAAI;CAAI;CAAI;AAAE,GAEtD,IAAS;CAAC;CAAG;CAAG;CAAG;CAAG;CAAG;CAAG;CAAG;CAAG;CAAG;AAAC,GAEtC,IAAY;CAChB,CAAC;CACD,CAAC,GAAG,EAAE;CACN,CAAC,GAAG,EAAE;CACN,CAAC,GAAG,EAAE;CACN,CAAC,GAAG,EAAE;CACN,CAAC,GAAG,EAAE;CACN;EAAC;EAAG;EAAI;CAAE;CACV;EAAC;EAAG;EAAI;CAAE;CACV;EAAC;EAAG;EAAI;CAAE;CACV;EAAC;EAAG;EAAI;CAAE;AACZ,GAGM,oBAAM,IAAI,WAAW,GAAG,GACxB,oBAAM,IAAI,WAAW,GAAG;AAC9B,KAAK,IAAI,IAAI,GAAG,IAAI,GAAG,IAAI,KAAK,KAI9B,AAHA,EAAI,KAAK,GACT,EAAI,KAAK,GACT,MAAM,GACF,IAAI,QAAO,KAAK;AAEtB,KAAK,IAAI,IAAI,KAAK,IAAI,KAAK,KAAK,EAAI,KAAK,EAAI,IAAI;AAEjD,IAAM,KAAO,GAAW,MACtB,MAAM,KAAK,MAAM,IAAI,IAAI,GAAK,EAAI,KAAM,EAAI,MAAO;AAGrD,SAAS,EAAU,GAA4B;CAC7C,IAAI,IAAO,IAAI,WAAW,CAAC,CAAC,CAAC;CAC7B,KAAK,IAAI,IAAI,GAAG,IAAI,GAAQ,KAAK;EAC/B,IAAM,IAAO,IAAI,WAAW,EAAK,SAAS,CAAC;EAC3C,KAAK,IAAI,IAAI,GAAG,IAAI,EAAK,QAAQ,KAI/B,AADA,EAAK,KAAK,EAAK,KAAM,EAAK,IAC1B,EAAK,IAAI,KAAK,EAAK,IAAI,KAAM,EAAI,EAAK,IAAK,EAAI,EAAG;EAEpD,IAAO;CACT;CACA,OAAO;AACT;AAGA,SAAS,EAAQ,GAAkB,GAA2B;CAC5D,IAAM,IAAM,EAAU,CAAK,GACrB,IAAM,IAAI,WAAW,CAAK;CAChC,KAAK,IAAM,KAAQ,GAAM;EACvB,IAAM,IAAS,IAAO,EAAI;EAG1B,IAFA,EAAI,WAAW,GAAG,CAAC,GACnB,EAAI,IAAQ,KAAK,GACb,MAAW,GACb,KAAK,IAAI,IAAI,GAAG,IAAI,GAAO,KAAK,EAAI,KAAK,EAAI,KAAM,EAAI,EAAI,IAAI,IAAK,CAAM;CAE9E;CACA,OAAO;AACT;AAGA,SAAS,EAAW,GAAsB;CACxC,IAAM,IAAQ,IAAa,GACvB,IAAM;CACV,KAAK,IAAI,IAAI,GAAG,IAAI,IAAI,KAAK,IAAO,KAAO,KAAO,MAAQ,KAAK;CAC/D,QAAS,KAAQ,KAAM,KAAO;AAChC;AAKA,IAAM,KAAO,GAAS,GAAW,GAAW,GAAe,IAAU,OAAe;CAElF,AADA,EAAE,QAAQ,IAAI,EAAE,OAAO,KAAK,MACxB,MAAS,EAAE,SAAS,IAAI,EAAE,OAAO,KAAK;AAC5C;AAEA,SAAS,EAAY,GAAS,GAAY,GAAkB;CAC1D,KAAK,IAAI,IAAK,IAAI,KAAM,GAAG,KACzB,KAAK,IAAI,IAAK,IAAI,KAAM,GAAG,KAAM;EAC/B,IAAM,IAAI,IAAK,GACT,IAAI,IAAK;EACf,IAAI,IAAI,KAAK,IAAI,KAAK,KAAK,EAAE,QAAQ,KAAK,EAAE,MAAM;EAClD,IAAM,IAAI,KAAK,IAAI,KAAK,IAAI,IAAK,CAAC,GAAG,KAAK,IAAI,IAAK,CAAC,CAAC;EACrD,EAAI,GAAG,GAAG,GAAG,MAAM,KAAK,KAAK,CAAC;CAChC;AAEJ;AAGA,SAAgB,EAAS,GAAqD;CAC5E,IAAM,IAAQ,IAAI,YAAY,CAAC,CAAC,OAAO,CAAI,GACrC,IAAU,EAAe,WAAW,MAAQ,EAAM,SAAS,KAAK,CAAG,IAAI;CAC7E,IAAI,MAAY,GACd,MAAU,MAAM,OAAO,EAAM,OAAO,+CAA+C;CAIrF,IAAM,IAAW,EAAe,IAAU,IACpC,IAAiB,CAAC,GAClB,KAAQ,GAAe,MAAwB;EACnD,KAAK,IAAI,IAAI,IAAQ,GAAG,KAAK,GAAG,KAAK,EAAK,KAAM,MAAU,IAAK,CAAC;CAClE;CAEA,AADA,EAAK,GAAQ,CAAC,GACd,EAAK,EAAM,QAAQ,IAAU,KAAK,IAAI,EAAE;CACxC,KAAK,IAAM,KAAQ,GAAO,EAAK,GAAM,CAAC;CAEtC,KADA,EAAK,GAAG,KAAK,IAAI,GAAG,IAAW,IAAI,EAAK,MAAM,CAAC,GACxC,EAAK,SAAS,IAAG,EAAK,KAAK,CAAC;CACnC,IAAM,IAAO,IAAI,WAAW,CAAQ;CACpC,KAAK,IAAI,IAAI,GAAG,IAAI,EAAK,QAAQ,KAAK,GAAG;EACvC,IAAI,IAAO;EACX,KAAK,IAAI,IAAI,GAAG,IAAI,GAAG,KAAK,IAAQ,KAAQ,IAAK,EAAK,IAAI;EAC1D,EAAK,IAAI,KAAK;CAChB;CACA,KAAK,IAAI,IAAI,EAAK,SAAS,GAAG,IAAM,GAAG,IAAI,GAAU,KAAK,KACxD,EAAK,KAAK,IAAM,KAAM,IAAI,MAAO;CAInC,IAAM,IAAa,EAAO,IAAU,IAC9B,IAAU,EAAa,IAAU,IACjC,IAAW,KAAK,MAAM,IAAW,CAAU,GAC3C,IAAY,IAAW,GACvB,IAA2B,CAAC,GAC5B,IAAyB,CAAC;CAChC,KAAK,IAAI,IAAI,GAAG,IAAS,GAAG,IAAI,GAAY,KAAK;EAC/C,IAAM,IAAM,IAAY,OAAK,IAAa,IACpC,IAAQ,EAAK,SAAS,GAAQ,IAAS,CAAG;EAGhD,AAFA,KAAU,GACV,EAAW,KAAK,CAAK,GACrB,EAAS,KAAK,EAAQ,GAAO,CAAO,CAAC;CACvC;CACA,IAAM,IAAsB,CAAC;CAC7B,KAAK,IAAI,IAAI,GAAG,IAAI,IAAW,GAAG,KAChC,KAAK,IAAM,KAAS,GAAY,AAAI,IAAI,EAAM,UAAQ,EAAU,KAAK,EAAM,EAAG;CAEhF,KAAK,IAAI,IAAI,GAAG,IAAI,GAAS,KAC3B,KAAK,IAAM,KAAS,GAAU,EAAU,KAAK,EAAM,EAAG;CAIxD,IAAM,IAAO,IAAU,IAAI,IACrB,IAAU;EACd;EACA,SAAS,IAAI,WAAW,IAAO,CAAI;EACnC,UAAU,IAAI,WAAW,IAAO,CAAI;CACtC;CAGA,AAFA,EAAY,GAAG,GAAG,CAAC,GACnB,EAAY,GAAG,IAAO,GAAG,CAAC,GAC1B,EAAY,GAAG,GAAG,IAAO,CAAC;CAC1B,KAAK,IAAI,IAAI,GAAG,IAAI,IAAO,GAAG,KAAK;EACjC,IAAM,IAAO,IAAI,KAAM;EAEvB,AADA,EAAI,GAAG,GAAG,GAAG,CAAI,GACjB,EAAI,GAAG,GAAG,GAAG,CAAI;CACnB;CACA,KAAK,IAAM,KAAM,EAAU,IAAU,IACnC,KAAK,IAAM,KAAM,EAAU,IAAU,IAE9B,UAAK,KAAK,IAAK,KAAO,IAAK,KAAK,IAAK,IAAO,MAAQ,IAAK,IAAO,MAAM,IAAK,IAChF,KAAK,IAAI,IAAK,IAAI,KAAM,GAAG,KACzB,KAAK,IAAI,IAAK,IAAI,KAAM,GAAG,KACzB,EAAI,GAAG,IAAK,GAAI,IAAK,GAAI,KAAK,IAAI,KAAK,IAAI,CAAE,GAAG,KAAK,IAAI,CAAE,CAAC,MAAM,CAAC;CAK3E,EAAI,GAAG,GAAG,IAAO,GAAG,EAAI;CAExB,KAAK,IAAI,IAAI,GAAG,IAAI,GAAG,KACrB,AAAI,MAAM,MAAG,EAAI,GAAG,GAAG,GAAG,EAAK,GAAG,EAAI,GAAG,GAAG,GAAG,EAAK;CAEtD,KAAK,IAAI,IAAI,GAAG,IAAI,GAAG,KAAK,EAAI,GAAG,IAAO,IAAI,GAAG,GAAG,EAAK,GAAG,EAAI,GAAG,GAAG,IAAO,IAAI,GAAG,EAAK;CACzF,EAAI,GAAG,GAAG,IAAO,GAAG,EAAI;CAGxB,IAAI,IAAM,GACJ,IAAQ,EAAU,SAAS;CACjC,KAAK,IAAI,IAAQ,IAAO,GAAG,KAAS,GAAG,KAAS,GAAG;EACjD,AAAI,MAAU,MAAG,IAAQ;EACzB,KAAK,IAAI,IAAO,GAAG,IAAO,GAAM,KAC9B,KAAK,IAAI,IAAM,GAAG,IAAM,GAAG,KAAO;GAChC,IAAM,IAAI,IAAQ,GAEZ,IADW,IAAQ,IAAK,IACO,IAAlB,IAAO,IAAI;GAC9B,IAAI,EAAE,SAAS,IAAI,IAAO,IAAI;GAC9B,IAAI,IAAO;GAMX,AALI,IAAM,MACR,KAAS,EAAU,MAAQ,OAAS,KAAK,IAAM,KAAO,MAAO,GAC7D,OAEG,IAAI,KAAK,KAAM,MAAG,IAAO,CAAC,IAC/B,EAAI,GAAG,GAAG,GAAG,GAAM,EAAK;EAC1B;CAEJ;CAGA,IAAM,IAAS,EAAW,CAAC;CAC3B,KAAK,IAAI,IAAI,GAAG,IAAI,IAAI,KAAK;EAC3B,IAAM,KAAS,MAAW,IAAK,MAAO,GAEhC,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,GAChC,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,KAAK;EAIvC,AAHA,EAAI,GAAG,GAAG,GAAG,CAAI,GACjB,EAAI,GAAG,GAAG,GAAG,CAAI,GAEb,IAAI,IAAG,EAAI,GAAG,IAAO,IAAI,GAAG,GAAG,CAAI,IAClC,EAAI,GAAG,GAAG,IAAO,KAAK,GAAG,CAAI;CACpC;CAGA,OAFA,EAAI,GAAG,GAAG,IAAO,GAAG,EAAI,GAEjB;EAAE;EAAM,SAAS,EAAE;CAAQ;AACpC;AAGA,SAAgB,EAAM,GAAc,GAAoB;CACtD,IAAM,EAAE,SAAM,eAAY,EAAS,CAAI,GACnC,IAAO;CACX,KAAK,IAAI,IAAI,GAAG,IAAI,GAAM,KACxB,KAAK,IAAI,IAAI,GAAG,IAAI,GAAM,KACxB,AAAI,EAAQ,IAAI,IAAO,OAAI,KAAQ,IAAI,EAAE,GAAG,EAAE;CAGlD,IACM,IAAM,IAAO;CACnB,OAAO,kDAAkD,EAAG,YAAY,EAAG,iBAAiB,EAAI,GAAG,EAAI,8CAA8C,EAAI,YAAY,EAAI,2DAAyE,YAAY,EAAK,KAAK;AAC1Q;;;AC1OA,IAAM,IAAU,KAEV,IAAY,MAQL,IAAb,MAAyB;CACvB;CACA;CACA;CACA;CACA,KAA+C;CAC/C;CAEA,YAAY,GAAY,GAAwB;EAC9C,IAAM,IAAM,EAAU;EAEtB,AADA,KAAKA,KAAO,GACZ,KAAKC,KAAa,EAAI;EAEtB,IAAM,IAAM,EAAK,SAAS,GACpB,IAAQ,EAAI,SAAS;EAa3B,AAXA,KAAKC,KAAY,EAAI,cAAc,KAAK,GACxC,KAAKA,GAAU,YAAY,uBAC3B,KAAKA,GAAU,iBAAiB,gBAAgB,MAAM;GACpD,AAAI,EAAE,WAAW,KAAKA,MAAW,KAAK,MAAM;EAC9C,CAAC,GAED,KAAKC,KAAU,EAAI,cAAc,KAAK,GACtC,KAAKA,GAAQ,YAAY,cACzB,KAAKA,GAAQ,aAAa,QAAQ,QAAQ,GAC1C,KAAKA,GAAQ,aAAa,cAAc,MAAM,GAC9C,KAAKA,GAAQ,aAAa,cAAc,OAAO,GAC/C,KAAKD,GAAU,YAAY,KAAKC,EAAO;EAEvC,IAAM,IAAO,EAAI,cAAc,KAAK;EACpC,EAAK,YAAY;EACjB,IAAM,IAAU,EAAI,cAAc,QAAQ;EAC1C,EAAQ,cAAc;EACtB,IAAM,IAAQ,EAAI,cAAc,QAAQ;EAQxC,AAPA,EAAM,OAAO,UACb,EAAM,YAAY,oBAClB,EAAM,aAAa,cAAc,OAAO,GACxC,EAAM,YAAY,EAAW,GAAK,EAAM,KAAK,CAAC,GAC9C,EAAM,iBAAiB,eAAe,KAAK,MAAM,CAAC,GAClD,EAAK,OAAO,GAAS,CAAK,GAE1B,KAAKA,GAAQ,OAAO,GAAM,KAAKC,GAAS,GAAK,CAAG,GAAG,KAAKC,GAAW,GAAK,GAAK,CAAK,CAAC;EACnF,IAAM,IAAK,KAAKC,GAAI,GAAK,CAAG;EAU5B,AATI,KAAI,KAAKH,GAAQ,aAAa,GAAI,KAAKA,GAAQ,SAAS,EAAG,GAE/D,EAAU,cAAc,KAAK,YAAY,KAAKD,EAAS,GAEvD,KAAKK,MAAU,MAAY;GAEzB,AADI,EAAE,QAAQ,YAAU,KAAK,MAAM,GAC/B,EAAE,QAAQ,SAAO,KAAKC,GAAW,CAAC;EACxC,GACA,KAAKN,GAAU,iBAAiB,WAAW,KAAKK,EAAM,GACtD,EAAM,MAAM;CACd;CAGA,GAAI,GAAe,GAAiC;EAClD,IAAI,CAAC,GAAK,OAAO;EACjB,IAAI;GACF,IAAM,IAAO,EAAI,cAAc,KAAK;GAKpC,OAJA,EAAK,YAAY,iBACjB,EAAK,YAAY,EAAM,GAAK,CAAO,GACnC,EAAK,aAAa,cAAc,uBAAuB,GACvD,EAAK,aAAa,QAAQ,KAAK,GACxB;EACT,QAAQ;GACN,OAAO;EACT;CACF;CAEA,GAAS,GAAe,GAA0B;EAChD,IAAM,IAAM,EAAI,cAAc,KAAK;EACnC,EAAI,YAAY;EAChB,IAAM,IAAQ,EAAI,cAAc,OAAO;EAKvC,AAJA,EAAM,OAAO,QACb,EAAM,WAAW,IACjB,EAAM,QAAQ,GACd,EAAM,aAAa,cAAc,mBAAmB,GACpD,EAAM,iBAAiB,eAAe,EAAM,OAAO,CAAC;EAEpD,IAAM,IAAO,EAAI,cAAc,QAAQ;EAQvC,OAPA,EAAK,OAAO,UACZ,EAAK,YAAY,mBACjB,EAAK,cAAc,QACnB,EAAK,iBAAiB,eAAe;GACnC,KAAUE,GAAM,GAAK,CAAI;EAC3B,CAAC,GACD,EAAI,OAAO,GAAO,CAAI,GACf;CACT;CAEA,MAAMA,GAAM,GAAa,GAA0C;EACjE,IAAI;GAEF,AADA,MAAM,UAAU,WAAW,UAAU,CAAG,GACxC,EAAO,cAAc;EACvB,QAAQ;GACN,EAAO,cAAc;EACvB;EAEA,AADI,KAAKC,MAAQ,aAAa,KAAKA,EAAM,GACzC,KAAKA,KAAS,iBAAiB;GAC7B,EAAO,cAAc;EACvB,GAAG,CAAS;CACd;CAEA,GAAW,GAAe,GAAa,GAA4B;EACjE,IAAM,IAAM,EAAI,cAAc,KAAK;EACnC,EAAI,YAAY;EAChB,KAAK,IAAM,KAAU,GAAS;GAC5B,IAAM,IAAO,EAAI,cAAc,GAAG;GAUlC,AATA,EAAK,YAAY,qBACjB,EAAK,OAAO,EAAO,KAAK,GAAK,CAAK,GAClC,EAAK,QAAQ,EAAO,OACpB,EAAK,aAAa,cAAc,YAAY,EAAO,OAAO,GACtD,EAAO,OAAO,YAChB,EAAK,SAAS,UACd,EAAK,MAAM,wBAEb,EAAK,YAAY,EAAW,GAAK,EAAO,IAAI,CAAC,GAC7C,EAAI,YAAY,CAAI;EACtB;EACA,OAAO;CACT;CAGA,GAAW,GAA4B;EACrC,IAAM,IAAY,CAChB,GAAG,KAAKP,GAAQ,iBAA8B,wBAAwB,CACxE,CAAC,CAAC,QAAQ,MAAO,CAAC,EAAG,aAAa,UAAU,CAAC;EAC7C,IAAI,EAAU,WAAW,GAAG;EAC5B,IAAM,IAAQ,EAAU,IAClB,IAAO,EAAU,GAAG,EAAE,GACtB,IAAS,KAAKH,GAAK;EACzB,AAAI,EAAM,YAAY,MAAW,KAC/B,EAAK,MAAM,GACX,EAAM,eAAe,KACZ,CAAC,EAAM,YAAY,MAAW,MACvC,EAAM,MAAM,GACZ,EAAM,eAAe;CAEzB;CAEA,QAAc;EACZ,KAAK,QAAQ;CACf;CAEA,UAAgB;EAMd,AALI,KAAKU,MAAQ,aAAa,KAAKA,EAAM,GACzC,KAAKA,KAAS,MACd,KAAKR,GAAU,oBAAoB,WAAW,KAAKK,EAAM,GACzD,KAAKL,GAAU,OAAO,GAEtB,KAAMD,IAAmC,QAAQ;CACnD;AACF"}
@@ -12,8 +12,15 @@ export interface Source {
12
12
  * synchronously-known page count (e.g. ImageSource) can omit it.
13
13
  */
14
14
  open?(): Promise<void>;
15
- /** Decode/resolve a single page to a raster. */
16
- get(index: number): Promise<PageContent>;
15
+ /**
16
+ * Decode/resolve a single page to a raster.
17
+ *
18
+ * `opts` is a hint, not a contract: it is passed only while the reader is zoomed in, and a
19
+ * source is free to ignore it and return its normal full-page raster. Sources backed by a
20
+ * fixed-resolution original (images) have nothing sharper to offer; vector sources (PDF) can
21
+ * re-rasterize the requested region to recover the detail that magnifying pixels loses.
22
+ */
23
+ get(index: number, opts?: PageRequest): Promise<PageContent>;
17
24
  /** Hint that these pages will be needed soon; out-of-range indices are ignored. */
18
25
  prefetch(indices: number[]): void;
19
26
  /** Release any decoded resources. */
@@ -24,4 +31,56 @@ export interface Source {
24
31
  * re-render that page if it's on screen.
25
32
  */
26
33
  onPageUpdate?(handler: (index: number) => void): void;
34
+ /**
35
+ * Optional: the page's plain text, for search. Sources backed by images can't have any and
36
+ * simply omit this — `Zine.canSearch()` reports whether a book is searchable at all.
37
+ */
38
+ getText?(index: number): Promise<string>;
39
+ /**
40
+ * Optional: the original document, for a download control. Return null when there is nothing
41
+ * to hand over — a source built from a pre-opened document has no bytes of its own, and an
42
+ * image book is not a single file at all.
43
+ */
44
+ getDownload?(): Promise<DownloadInfo | null>;
45
+ /**
46
+ * Optional: the document's table of contents, for an outline panel. Return an empty array when
47
+ * the document has none — PDFs frequently do not.
48
+ */
49
+ getOutline?(): Promise<OutlineItem[]>;
50
+ }
51
+ /**
52
+ * A request for part of a page at a particular magnification, sent when the reader zooms in.
53
+ *
54
+ * A source that honours it returns a raster of `region` alone, rendered at `scale` times the
55
+ * resolution it would normally use. Returning the whole page instead is always valid: the engine
56
+ * tells the two apart by comparing the raster's aspect against the region's.
57
+ */
58
+ export interface PageRequest {
59
+ /** How much sharper than the fit-to-screen raster to render, matching the reader's zoom. */
60
+ scale: number;
61
+ /** The visible part of the page, as fractions of its width and height in [0,1]. */
62
+ region: {
63
+ x: number;
64
+ y: number;
65
+ width: number;
66
+ height: number;
67
+ };
68
+ }
69
+ /** One entry in a document's table of contents. */
70
+ export interface OutlineItem {
71
+ /** The heading text. */
72
+ title: string;
73
+ /** Zero-based page it points at, or null when the destination cannot be resolved. */
74
+ page: number | null;
75
+ /** Nested entries; empty for a leaf. */
76
+ children: OutlineItem[];
77
+ }
78
+ /** A downloadable original document. */
79
+ export interface DownloadInfo {
80
+ /** Where to fetch it, or a blob/object URL the caller can link to directly. */
81
+ url: string;
82
+ /** Suggested file name, used for the download attribute. */
83
+ filename: string;
84
+ /** True when `url` was created with `URL.createObjectURL` and must be revoked after use. */
85
+ revoke?: boolean;
27
86
  }
package/dist/zine.d.ts CHANGED
@@ -1,9 +1,17 @@
1
- import { type Direction, type SpreadMode } from './engine/spread';
1
+ import { type Direction, type Spread, type SpreadMode } from './engine/spread';
2
2
  import { type ZineEventMap } from './engine/emitter';
3
3
  import { type CurlType } from './geometry/curls/types';
4
4
  import { type RendererOption } from './renderer/select';
5
- import type { FlipDirection } from './renderer/types';
6
- import type { Source } from './source/types';
5
+ import type { FlipDirection, PageContent } from './renderer/types';
6
+ import type { DownloadInfo, OutlineItem, Source } from './source/types';
7
+ import type { ControlsOptions } from './controls/types';
8
+ /** One page that matched a {@link Zine.search} query. */
9
+ export interface SearchHit {
10
+ /** Zero-based page index; pass straight to `flipTo`. */
11
+ page: number;
12
+ /** The match with a little text either side, ellipsised where it was cut. */
13
+ excerpt: string;
14
+ }
7
15
  export interface ZoomOptions {
8
16
  /** Whether zooming is allowed at all; default true. */
9
17
  enabled?: boolean;
@@ -58,6 +66,37 @@ export interface ZineOptions {
58
66
  zoom?: ZoomOptions;
59
67
  /** Container widths below this (px) switch to one page per spread; default 640. */
60
68
  singlePageThreshold?: number;
69
+ /**
70
+ * Whether a narrow container may override `spreadMode` and show one page at a time. Default
71
+ * true.
72
+ *
73
+ * Set false to hold the configured mode at every width — a two-page spread stays two pages on
74
+ * a phone, however small. (`singlePageThreshold: 0` has the same effect; this says it out
75
+ * loud, and can be changed at runtime with {@link Zine.setResponsiveSpread}.)
76
+ */
77
+ responsiveSpread?: boolean;
78
+ /**
79
+ * The built-in toolbar. Shown by default; pass `false` to render none, or an object to choose
80
+ * its position and which controls appear.
81
+ */
82
+ controls?: boolean | ControlsOptions;
83
+ /**
84
+ * Keep the current page in the URL hash (`#page=12`), so a link opens where the reader was and
85
+ * back/forward move through the book. Default true.
86
+ *
87
+ * Only a `page` key is read or written — anything else in the hash is left alone — and updates
88
+ * use `replaceState`, so turning pages does not fill the back button. Pass `false` if your app
89
+ * owns the hash.
90
+ */
91
+ deepLink?: boolean;
92
+ /**
93
+ * Suppress the browser's right-click menu over the book. Default false.
94
+ *
95
+ * A mild deterrent for published documents, not protection: the pages are still in the DOM and
96
+ * reachable by anyone who wants them. It also takes away Inspect and "Open image in new tab"
97
+ * for everyone, so it is off unless asked for.
98
+ */
99
+ disableContextMenu?: boolean;
61
100
  }
62
101
  /**
63
102
  * The public flipbook. Composes the engine (spreads, virtualizer, events), a
@@ -76,6 +115,123 @@ export declare class Zine {
76
115
  get ready(): Promise<void>;
77
116
  getPageCount(): number;
78
117
  getPage(): number;
118
+ /** The element this flipbook was mounted into. */
119
+ get container(): HTMLElement;
120
+ /**
121
+ * How pages are currently grouped, one entry per spread — the same model the renderer paints,
122
+ * so it already reflects `spreadMode` and the responsive single-page fallback. Useful for
123
+ * building a page list or thumbnail rail that matches the book.
124
+ */
125
+ getSpreads(): readonly Spread[];
126
+ /** The spread index currently on screen; indexes into {@link getSpreads}. */
127
+ getSpreadIndex(): number;
128
+ /** Reading direction, which mirrors each spread's left/right sides. */
129
+ getDirection(): Direction;
130
+ /** How pages are grouped: the configured mode, not what a narrow container may be forcing. */
131
+ getSpreadMode(): SpreadMode;
132
+ /** Whether the book is showing one page at a time, for whatever reason. */
133
+ isSinglePage(): boolean;
134
+ /**
135
+ * Regroup the pages, keeping the reader on the page they are looking at.
136
+ *
137
+ * A narrow container may still override this with one page at a time; see
138
+ * {@link setResponsiveSpread}.
139
+ */
140
+ setSpreadMode(mode: SpreadMode): void;
141
+ /**
142
+ * Switch between one page and two, and back again.
143
+ *
144
+ * Returning to two pages restores the mode the book was built with, so a `cover` book gets its
145
+ * lone first page back rather than becoming a plain `double`.
146
+ */
147
+ toggleSpreadMode(): void;
148
+ /** Whether a narrow container is allowed to override `spreadMode` with one page at a time. */
149
+ getResponsiveSpread(): boolean;
150
+ /**
151
+ * Whether the book is showing one page at a time *because the container is narrow*, rather
152
+ * than because it was asked to. False when `responsiveSpread` is off.
153
+ *
154
+ * A book already set to `single` is not being overridden, however narrow it is: asking for one
155
+ * page makes the book itself narrower, so reporting that as an override would strand the reader
156
+ * in a layout they could no longer switch out of.
157
+ */
158
+ isResponsiveSingle(): boolean;
159
+ /**
160
+ * Allow or forbid the narrow-container override, and re-lay out at once.
161
+ *
162
+ * Turning it off holds the configured `spreadMode` at every width; turning it back on
163
+ * re-measures, so a book on a narrow screen collapses to single again straight away.
164
+ */
165
+ setResponsiveSpread(enabled: boolean): void;
166
+ /**
167
+ * Resolve one page's raster, for drawing a thumbnail or exporting. Returns null if the page
168
+ * cannot be decoded — the failure is reported through `sourceError` rather than thrown, as it
169
+ * is for the pages the renderer paints.
170
+ */
171
+ getPageImage(index: number): Promise<PageContent | null>;
172
+ /** Whether there is a spread after the current one (false on the last). */
173
+ canFlipNext(): boolean;
174
+ /** Whether there is a spread before the current one (false on the first). */
175
+ canFlipPrev(): boolean;
176
+ /** The ceiling `setZoom` clamps to. */
177
+ getMaxZoom(): number;
178
+ /** Whether this book's source can produce text — false for image books. */
179
+ canSearch(): boolean;
180
+ /** Whether the original document can be downloaded — true for a PDF from a URL or bytes. */
181
+ canDownload(): boolean;
182
+ /**
183
+ * Whether the book is a document rather than a set of loose images — true for a PDF, false for
184
+ * an `ImageSource`. Document-shaped features (the page rail, the outline) key off this.
185
+ *
186
+ * Inferred from the source implementing document-only capabilities, so a custom source opts in
187
+ * simply by implementing them.
188
+ */
189
+ isDocument(): boolean;
190
+ /** Whether this book can be printed — true when the source has an original document. */
191
+ canPrint(): boolean;
192
+ /**
193
+ * Print the document.
194
+ *
195
+ * The original file is handed to the browser in an offscreen frame rather than printing the
196
+ * host page: printing the page would capture the toolbar and whatever single spread happens to
197
+ * be on screen, while the browser paginates a PDF properly on its own. Resolves false when
198
+ * there is no original to print — an image book, or a caller-owned pdf.js document.
199
+ */
200
+ print(): Promise<boolean>;
201
+ /** Whether the source can supply a table of contents. True for PDFs; the document may still
202
+ * turn out to have no outline, in which case {@link getOutline} resolves empty. */
203
+ canOutline(): boolean;
204
+ /**
205
+ * The document's table of contents, or an empty list when it has none.
206
+ *
207
+ * Memoized: an outline does not change for the life of the document, and resolving one means
208
+ * following every entry's destination to a page. The toolbar asks for it up front to decide
209
+ * whether to offer the outline panel at all, and the panel itself then gets it for free.
210
+ */
211
+ getOutline(): Promise<OutlineItem[]>;
212
+ /**
213
+ * Save the original document. Resolves to false when the source has nothing to hand over
214
+ * (an image book, or a PDF opened from a caller-owned pdf.js document).
215
+ */
216
+ /**
217
+ * Where the original document lives, if the source has one — a PDF's URL or a blob of its
218
+ * bytes. Null for an image book, or a PDF opened from a caller-owned pdf.js document.
219
+ *
220
+ * Saving and printing both start here: the original is better than anything reassembled from
221
+ * page rasters, and the browser already knows how to paginate it.
222
+ */
223
+ getSourceFile(): Promise<DownloadInfo | null>;
224
+ download(): Promise<boolean>;
225
+ /**
226
+ * Find `query` in the book's text, case-insensitively.
227
+ *
228
+ * Pages are read on demand and cached by the source, so the first search over a long document
229
+ * costs one text extraction per page and later ones are cheap. Returns at most one hit per
230
+ * page, in page order. Always empty when {@link canSearch} is false.
231
+ */
232
+ search(query: string, options?: {
233
+ limit?: number;
234
+ }): Promise<SearchHit[]>;
79
235
  flipNext(): void;
80
236
  flipPrev(): void;
81
237
  flipTo(page: number): void;
@@ -88,6 +244,8 @@ export declare class Zine {
88
244
  resetZoom(): void;
89
245
  on<K extends keyof ZineEventMap>(event: K, listener: (payload: ZineEventMap[K]) => void): () => void;
90
246
  destroy(): void;
247
+ /** The URL that opens this book at `page` (defaults to the current one). */
248
+ pageLink(page?: number): string;
91
249
  /** Re-measure and re-render; call after the container resizes. */
92
250
  update(): void;
93
251
  }