@zinejs/core 0.8.2 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +73 -0
- package/README.md +27 -49
- package/dist/{bundled-Cp43MIt0.js → bundled-ESapkNsS.js} +34 -34
- package/dist/bundled-ESapkNsS.js.map +1 -0
- package/dist/controls/contextMenu.d.ts +16 -0
- package/dist/controls/controls.d.ts +10 -3
- package/dist/controls/icons.d.ts +6 -2
- package/dist/controls/menu.d.ts +68 -0
- package/dist/controls/search.d.ts +15 -2
- package/dist/controls/styles.d.ts +1 -1
- package/dist/controls/toolbar.d.ts +1 -1
- package/dist/controls/types.d.ts +6 -0
- package/dist/{controls-DgEXR2zi.js → controls-DzVdwPJB.js} +349 -196
- package/dist/controls-DzVdwPJB.js.map +1 -0
- package/dist/curls.js +1 -1
- package/dist/flipSound-BFXSkWBo.js +78 -0
- package/dist/flipSound-BFXSkWBo.js.map +1 -0
- package/dist/geometry/curls/cone.d.ts +4 -3
- package/dist/index.js +671 -440
- package/dist/index.js.map +1 -1
- package/dist/index.umd.js +26 -7
- package/dist/index.umd.js.map +1 -1
- package/dist/loading/loading.d.ts +1 -1
- package/dist/loading-DF88K9yJ.js +45 -0
- package/dist/loading-DF88K9yJ.js.map +1 -0
- package/dist/renderer/gl/texture.d.ts +13 -2
- package/dist/renderer/types.d.ts +6 -0
- package/dist/renderer/webglRenderer.d.ts +1 -0
- package/dist/{share-WXdscwp3.js → share-Cq3RsjPr.js} +2 -2
- package/dist/{share-WXdscwp3.js.map → share-Cq3RsjPr.js.map} +1 -1
- package/dist/sound/clip.d.ts +9 -0
- package/dist/sound/flipSound.d.ts +30 -0
- package/dist/source/types.d.ts +5 -0
- package/dist/styles-C1Qp5pby.js +42 -0
- package/dist/styles-C1Qp5pby.js.map +1 -0
- package/dist/{webglRenderer-nGRruZk2.js → webglRenderer-CmQ_mUrX.js} +113 -101
- package/dist/webglRenderer-CmQ_mUrX.js.map +1 -0
- package/dist/zine.d.ts +75 -1
- package/options.schema.json +66 -0
- package/package.json +13 -11
- package/dist/bundled-Cp43MIt0.js.map +0 -1
- package/dist/controls-DgEXR2zi.js.map +0 -1
- package/dist/loading-BBK6YbSW.js +0 -45
- package/dist/loading-BBK6YbSW.js.map +0 -1
- package/dist/styles-B8fFoYm_.js +0 -40
- package/dist/styles-B8fFoYm_.js.map +0 -1
- package/dist/webglRenderer-nGRruZk2.js.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { LoadProgress } from '../source/types';
|
|
2
|
-
export declare const CSS = "\n.zine-loading {\n position: absolute;\n inset: 0;\n z-index: 3;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 14px;\n padding: 24px;\n box-sizing: border-box;\n text-align: center;\n font: 500 14px/1.4 system-ui, sans-serif;\n color: var(--zine-loading-fg, light-dark(#3f3f46, #d4d4d8));\n background: var(--zine-loading-bg, light-dark(#fafafa, #0c0c11));\n border-radius: inherit;\n opacity: 0;\n transition: opacity 150ms ease;\n}\n.zine-loading-shown { opacity: 1; }\n.zine-loading-spinner {\n width: 32px;\n height: 32px;\n border: 3px solid var(--zine-loading-track, light-dark(rgba(0, 0, 0, 0.1), rgba(255, 255, 255, 0.14)));\n border-top-color: var(--zine-loading-accent, light-dark(#0284c7, #7dd3fc));\n border-radius: 50%;\n animation: zine-loading-spin 0.8s linear infinite;\n}\n@keyframes zine-loading-spin { to { transform: rotate(360deg); } }\n.zine-loading-text { font-variant-numeric: tabular-nums; }\n.zine-loading-bar {\n width: min(240px, 70%);\n height: 6px;\n background: var(--zine-loading-track, light-dark(rgba(0, 0, 0, 0.1), rgba(255, 255, 255, 0.14)));\n border-radius: 3px;\n overflow: hidden;\n}\n.zine-loading-fill {\n width: 0;\n height: 100%;\n background: var(--zine-loading-accent, light-dark(#0284c7, #7dd3fc));\n border-radius: 3px;\n transition: width 150ms ease-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .zine-loading { transition: none; }\n .zine-loading-spinner { animation-duration: 2s; }\n .zine-loading-fill { transition: none; }\n}\n";
|
|
2
|
+
export declare const CSS = "\n.zine-loading {\n position: absolute;\n inset: 0;\n z-index: 3;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 14px;\n /* The overlay shows during download, before the book is measured and the container gets its\n aspect-ratio (and thus its height). A consumer who sizes the container by width alone leaves it\n near-zero-height meanwhile, which would crush the overlay to a strip and clip the spinner.\n A floor keeps it tall enough to lay out; the box snaps to the book's ratio once it paints. */\n min-height: 160px;\n padding: 24px;\n box-sizing: border-box;\n text-align: center;\n font: 500 14px/1.4 system-ui, sans-serif;\n color: var(--zine-loading-fg, light-dark(#3f3f46, #d4d4d8));\n background: var(--zine-loading-bg, light-dark(#fafafa, #0c0c11));\n border-radius: inherit;\n opacity: 0;\n transition: opacity 150ms ease;\n}\n.zine-loading-shown { opacity: 1; }\n.zine-loading-spinner {\n width: 32px;\n height: 32px;\n border: 3px solid var(--zine-loading-track, light-dark(rgba(0, 0, 0, 0.1), rgba(255, 255, 255, 0.14)));\n border-top-color: var(--zine-loading-accent, light-dark(#0284c7, #7dd3fc));\n border-radius: 50%;\n animation: zine-loading-spin 0.8s linear infinite;\n}\n@keyframes zine-loading-spin { to { transform: rotate(360deg); } }\n.zine-loading-text { font-variant-numeric: tabular-nums; }\n.zine-loading-bar {\n width: min(240px, 70%);\n height: 6px;\n background: var(--zine-loading-track, light-dark(rgba(0, 0, 0, 0.1), rgba(255, 255, 255, 0.14)));\n border-radius: 3px;\n overflow: hidden;\n}\n.zine-loading-fill {\n width: 0;\n height: 100%;\n background: var(--zine-loading-accent, light-dark(#0284c7, #7dd3fc));\n border-radius: 3px;\n transition: width 150ms ease-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .zine-loading { transition: none; }\n .zine-loading-spinner { animation-duration: 2s; }\n .zine-loading-fill { transition: none; }\n}\n";
|
|
3
3
|
/** Handle the engine uses to drive and tear down the overlay. */
|
|
4
4
|
export interface LoaderHandle {
|
|
5
5
|
/** Download phase: reflect bytes fetched, showing a percentage bar when a total is known. */
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
//#region src/loading/loading.ts
|
|
2
|
+
var e = "zine-loading-style";
|
|
3
|
+
function t(t) {
|
|
4
|
+
if (t.getElementById(e)) return;
|
|
5
|
+
let n = t.createElement("style");
|
|
6
|
+
n.id = e, n.textContent = "\n.zine-loading {\n position: absolute;\n inset: 0;\n z-index: 3;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 14px;\n /* The overlay shows during download, before the book is measured and the container gets its\n aspect-ratio (and thus its height). A consumer who sizes the container by width alone leaves it\n near-zero-height meanwhile, which would crush the overlay to a strip and clip the spinner.\n A floor keeps it tall enough to lay out; the box snaps to the book's ratio once it paints. */\n min-height: 160px;\n padding: 24px;\n box-sizing: border-box;\n text-align: center;\n font: 500 14px/1.4 system-ui, sans-serif;\n color: var(--zine-loading-fg, light-dark(#3f3f46, #d4d4d8));\n background: var(--zine-loading-bg, light-dark(#fafafa, #0c0c11));\n border-radius: inherit;\n opacity: 0;\n transition: opacity 150ms ease;\n}\n.zine-loading-shown { opacity: 1; }\n.zine-loading-spinner {\n width: 32px;\n height: 32px;\n border: 3px solid var(--zine-loading-track, light-dark(rgba(0, 0, 0, 0.1), rgba(255, 255, 255, 0.14)));\n border-top-color: var(--zine-loading-accent, light-dark(#0284c7, #7dd3fc));\n border-radius: 50%;\n animation: zine-loading-spin 0.8s linear infinite;\n}\n@keyframes zine-loading-spin { to { transform: rotate(360deg); } }\n.zine-loading-text { font-variant-numeric: tabular-nums; }\n.zine-loading-bar {\n width: min(240px, 70%);\n height: 6px;\n background: var(--zine-loading-track, light-dark(rgba(0, 0, 0, 0.1), rgba(255, 255, 255, 0.14)));\n border-radius: 3px;\n overflow: hidden;\n}\n.zine-loading-fill {\n width: 0;\n height: 100%;\n background: var(--zine-loading-accent, light-dark(#0284c7, #7dd3fc));\n border-radius: 3px;\n transition: width 150ms ease-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .zine-loading { transition: none; }\n .zine-loading-spinner { animation-duration: 2s; }\n .zine-loading-fill { transition: none; }\n}\n", (t.head ?? t.documentElement)?.appendChild(n);
|
|
7
|
+
}
|
|
8
|
+
var n = (e) => `${(e / 1024 / 1024).toFixed(1)} MB`;
|
|
9
|
+
function r(e) {
|
|
10
|
+
let r = e.ownerDocument;
|
|
11
|
+
t(r);
|
|
12
|
+
let i = r.createElement("div");
|
|
13
|
+
i.className = "zine-loading", i.setAttribute("role", "status"), i.setAttribute("aria-live", "polite");
|
|
14
|
+
let a = r.createElement("div");
|
|
15
|
+
a.className = "zine-loading-spinner", a.setAttribute("aria-hidden", "true");
|
|
16
|
+
let o = r.createElement("div");
|
|
17
|
+
o.className = "zine-loading-text", o.textContent = "Opening document…";
|
|
18
|
+
let s = r.createElement("div");
|
|
19
|
+
s.className = "zine-loading-bar", s.style.display = "none";
|
|
20
|
+
let c = r.createElement("div");
|
|
21
|
+
c.className = "zine-loading-fill", s.appendChild(c), i.append(a, o, s), e.appendChild(i);
|
|
22
|
+
let l = null, u = typeof globalThis.getComputedStyle == "function" ? globalThis.getComputedStyle(e) : null;
|
|
23
|
+
(!u || u.position === "static" || u.position === "") && (l = e.style.position, e.style.position = "relative");
|
|
24
|
+
let d = !1, f = setTimeout(() => {
|
|
25
|
+
d = !0, i.classList.add("zine-loading-shown");
|
|
26
|
+
}, 150);
|
|
27
|
+
return {
|
|
28
|
+
update(e) {
|
|
29
|
+
if (e.total > 0) {
|
|
30
|
+
let t = Math.min(100, Math.round(e.loaded / e.total * 100));
|
|
31
|
+
o.textContent = `Downloading document… ${t}%`, s.style.display = "", c.style.width = `${t}%`;
|
|
32
|
+
} else o.textContent = `Downloading document… ${n(e.loaded)}`, s.style.display = "none";
|
|
33
|
+
},
|
|
34
|
+
preparing() {
|
|
35
|
+
o.textContent = "Preparing pages…", s.style.display = "none";
|
|
36
|
+
},
|
|
37
|
+
destroy() {
|
|
38
|
+
d || clearTimeout(f), i.remove(), l !== null && (e.style.position = l);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
//#endregion
|
|
43
|
+
export { r as mountLoading };
|
|
44
|
+
|
|
45
|
+
//# sourceMappingURL=loading-DF88K9yJ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loading-DF88K9yJ.js","names":[],"sources":["../src/loading/loading.ts"],"sourcesContent":["import type { LoadProgress } from '../source/types';\n\n/**\n * The built-in loading overlay: a spinner, a status line, and a download progress bar.\n *\n * This module is the lazy chunk `zine.ts` reaches through a dynamic `import()`, so a book built\n * with `loading: false`, or one whose source never reports progress, never downloads it. The file\n * is named `loading.ts` rather than `index.ts` so the emitted chunk is `loading-<hash>.js`, which\n * is what the size-limit budget watches (the same trick as `controls/controls.ts`).\n *\n * Everything is scoped under `.zine-loading`, and the colours come from custom properties so a\n * consumer can retheme without overriding rules. Each falls back to `light-dark(light, dark)`, which\n * resolves against the used `color-scheme`, so the overlay follows the host page's light or dark\n * theme with no configuration (the same approach as the controls). A consumer who wants a wholly\n * different indicator passes `loading: false` and drives their own UI from the `progress` event and\n * the `ready` promise.\n */\n\nconst STYLE_ID = 'zine-loading-style';\n\n// Wait this long before showing the overlay: a small or cached PDF resolves in a few frames, and\n// flashing a spinner for that is worse than showing nothing.\nconst REVEAL_DELAY_MS = 150;\n\nexport const CSS = `\n.zine-loading {\n position: absolute;\n inset: 0;\n z-index: 3;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 14px;\n /* The overlay shows during download, before the book is measured and the container gets its\n aspect-ratio (and thus its height). A consumer who sizes the container by width alone leaves it\n near-zero-height meanwhile, which would crush the overlay to a strip and clip the spinner.\n A floor keeps it tall enough to lay out; the box snaps to the book's ratio once it paints. */\n min-height: 160px;\n padding: 24px;\n box-sizing: border-box;\n text-align: center;\n font: 500 14px/1.4 system-ui, sans-serif;\n color: var(--zine-loading-fg, light-dark(#3f3f46, #d4d4d8));\n background: var(--zine-loading-bg, light-dark(#fafafa, #0c0c11));\n border-radius: inherit;\n opacity: 0;\n transition: opacity 150ms ease;\n}\n.zine-loading-shown { opacity: 1; }\n.zine-loading-spinner {\n width: 32px;\n height: 32px;\n border: 3px solid var(--zine-loading-track, light-dark(rgba(0, 0, 0, 0.1), rgba(255, 255, 255, 0.14)));\n border-top-color: var(--zine-loading-accent, light-dark(#0284c7, #7dd3fc));\n border-radius: 50%;\n animation: zine-loading-spin 0.8s linear infinite;\n}\n@keyframes zine-loading-spin { to { transform: rotate(360deg); } }\n.zine-loading-text { font-variant-numeric: tabular-nums; }\n.zine-loading-bar {\n width: min(240px, 70%);\n height: 6px;\n background: var(--zine-loading-track, light-dark(rgba(0, 0, 0, 0.1), rgba(255, 255, 255, 0.14)));\n border-radius: 3px;\n overflow: hidden;\n}\n.zine-loading-fill {\n width: 0;\n height: 100%;\n background: var(--zine-loading-accent, light-dark(#0284c7, #7dd3fc));\n border-radius: 3px;\n transition: width 150ms ease-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .zine-loading { transition: none; }\n .zine-loading-spinner { animation-duration: 2s; }\n .zine-loading-fill { transition: none; }\n}\n`;\n\n/** Add the stylesheet to `doc` if it is not already there. */\nfunction ensureStyles(doc: Document): void {\n if (doc.getElementById(STYLE_ID)) return;\n const style = doc.createElement('style');\n style.id = STYLE_ID;\n style.textContent = CSS;\n (doc.head ?? doc.documentElement)?.appendChild(style);\n}\n\n/** Handle the engine uses to drive and tear down the overlay. */\nexport interface LoaderHandle {\n /** Download phase: reflect bytes fetched, showing a percentage bar when a total is known. */\n update(progress: LoadProgress): void;\n /** Downloading is done; the wait is now rasterizing the first spread. */\n preparing(): void;\n /** Remove the overlay and restore anything it changed on the container. */\n destroy(): void;\n}\n\nconst fmtMB = (bytes: number): string => `${(bytes / 1024 / 1024).toFixed(1)} MB`;\n\n/**\n * Build the overlay inside `container` and return a handle to drive it.\n *\n * The overlay is hidden for the first {@link REVEAL_DELAY_MS} so a fast load never flashes it;\n * `destroy()` before then cancels the reveal outright.\n */\nexport function mountLoading(container: HTMLElement): LoaderHandle {\n const doc = container.ownerDocument;\n ensureStyles(doc);\n\n const overlay = doc.createElement('div');\n overlay.className = 'zine-loading';\n overlay.setAttribute('role', 'status');\n overlay.setAttribute('aria-live', 'polite');\n\n const spinner = doc.createElement('div');\n spinner.className = 'zine-loading-spinner';\n spinner.setAttribute('aria-hidden', 'true');\n\n const text = doc.createElement('div');\n text.className = 'zine-loading-text';\n text.textContent = 'Opening document…';\n\n const bar = doc.createElement('div');\n bar.className = 'zine-loading-bar';\n bar.style.display = 'none';\n const fill = doc.createElement('div');\n fill.className = 'zine-loading-fill';\n bar.appendChild(fill);\n\n overlay.append(spinner, text, bar);\n container.appendChild(overlay);\n\n // The overlay is absolutely positioned; a statically-positioned container would let it escape.\n // Give the container a positioning context and restore it on teardown. getComputedStyle is\n // absent under some test DOMs, so guard it.\n let restorePosition: string | null = null;\n const computed =\n typeof globalThis.getComputedStyle === 'function' ? globalThis.getComputedStyle(container) : null;\n if (!computed || computed.position === 'static' || computed.position === '') {\n restorePosition = container.style.position;\n container.style.position = 'relative';\n }\n\n let revealed = false;\n const revealTimer = setTimeout(() => {\n revealed = true;\n overlay.classList.add('zine-loading-shown');\n }, REVEAL_DELAY_MS);\n\n return {\n update(progress: LoadProgress): void {\n if (progress.total > 0) {\n const pct = Math.min(100, Math.round((progress.loaded / progress.total) * 100));\n text.textContent = `Downloading document… ${pct}%`;\n bar.style.display = '';\n fill.style.width = `${pct}%`;\n } else {\n text.textContent = `Downloading document… ${fmtMB(progress.loaded)}`;\n bar.style.display = 'none';\n }\n },\n preparing(): void {\n text.textContent = 'Preparing pages…';\n bar.style.display = 'none';\n },\n destroy(): void {\n if (!revealed) clearTimeout(revealTimer);\n overlay.remove();\n if (restorePosition !== null) container.style.position = restorePosition;\n },\n };\n}\n"],"mappings":";AAkBA,IAAM,IAAW;AAgEjB,SAAS,EAAa,GAAqB;CACzC,IAAI,EAAI,eAAe,CAAQ,GAAG;CAClC,IAAM,IAAQ,EAAI,cAAc,OAAO;CAGvC,AAFA,EAAM,KAAK,GACX,EAAM,cAAc,g9DACnB,EAAI,QAAQ,EAAI,gBAAA,EAAkB,YAAY,CAAK;AACtD;AAYA,IAAM,KAAS,MAA0B,IAAI,IAAQ,OAAO,KAAA,CAAM,QAAQ,CAAC,EAAE;AAQ7E,SAAgB,EAAa,GAAsC;CACjE,IAAM,IAAM,EAAU;CACtB,EAAa,CAAG;CAEhB,IAAM,IAAU,EAAI,cAAc,KAAK;CAGvC,AAFA,EAAQ,YAAY,gBACpB,EAAQ,aAAa,QAAQ,QAAQ,GACrC,EAAQ,aAAa,aAAa,QAAQ;CAE1C,IAAM,IAAU,EAAI,cAAc,KAAK;CAEvC,AADA,EAAQ,YAAY,wBACpB,EAAQ,aAAa,eAAe,MAAM;CAE1C,IAAM,IAAO,EAAI,cAAc,KAAK;CAEpC,AADA,EAAK,YAAY,qBACjB,EAAK,cAAc;CAEnB,IAAM,IAAM,EAAI,cAAc,KAAK;CAEnC,AADA,EAAI,YAAY,oBAChB,EAAI,MAAM,UAAU;CACpB,IAAM,IAAO,EAAI,cAAc,KAAK;CAKpC,AAJA,EAAK,YAAY,qBACjB,EAAI,YAAY,CAAI,GAEpB,EAAQ,OAAO,GAAS,GAAM,CAAG,GACjC,EAAU,YAAY,CAAO;CAK7B,IAAI,IAAiC,MAC/B,IACJ,OAAO,WAAW,oBAAqB,aAAa,WAAW,iBAAiB,CAAS,IAAI;CAC/F,CAAI,CAAC,KAAY,EAAS,aAAa,YAAY,EAAS,aAAa,QACvE,IAAkB,EAAU,MAAM,UAClC,EAAU,MAAM,WAAW;CAG7B,IAAI,IAAW,IACT,IAAc,iBAAiB;EAEnC,AADA,IAAW,IACX,EAAQ,UAAU,IAAI,oBAAoB;CAC5C,GAAG,GAAe;CAElB,OAAO;EACL,OAAO,GAA8B;GACnC,IAAI,EAAS,QAAQ,GAAG;IACtB,IAAM,IAAM,KAAK,IAAI,KAAK,KAAK,MAAO,EAAS,SAAS,EAAS,QAAS,GAAG,CAAC;IAG9E,AAFA,EAAK,cAAc,yBAAyB,EAAI,IAChD,EAAI,MAAM,UAAU,IACpB,EAAK,MAAM,QAAQ,GAAG,EAAI;GAC5B,OAEE,AADA,EAAK,cAAc,yBAAyB,EAAM,EAAS,MAAM,KACjE,EAAI,MAAM,UAAU;EAExB;EACA,YAAkB;GAEhB,AADA,EAAK,cAAc,oBACnB,EAAI,MAAM,UAAU;EACtB;EACA,UAAgB;GAGd,AAFK,KAAU,aAAa,CAAW,GACvC,EAAQ,OAAO,GACX,MAAoB,SAAM,EAAU,MAAM,WAAW;EAC3D;CACF;AACF"}
|
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
/** Create a page texture with clamp + linear filtering (no mipmaps — §8.3). */
|
|
2
2
|
export declare function createTexture(gl: WebGL2RenderingContext): WebGLTexture;
|
|
3
|
-
/**
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Upload a decoded page raster (ImageBitmap or canvas) into `tex`, returning the dimensions actually
|
|
5
|
+
* uploaded (which differ from the source when it was too big for the GPU).
|
|
6
|
+
*
|
|
7
|
+
* A raster larger than `gl.MAX_TEXTURE_SIZE` on either axis cannot be uploaded: `texImage2D` fails
|
|
8
|
+
* and leaves the texture incomplete, which samples as solid black. A zoomed page on a high-DPR phone
|
|
9
|
+
* (small MAX_TEXTURE_SIZE, e.g. 4096) hits this readily. Downscale over-large rasters to fit,
|
|
10
|
+
* preserving aspect, so a slightly-softer page beats a black one.
|
|
11
|
+
*/
|
|
12
|
+
export declare function uploadTexture(gl: WebGL2RenderingContext, tex: WebGLTexture, source: TexImageSource): {
|
|
13
|
+
width: number;
|
|
14
|
+
height: number;
|
|
15
|
+
};
|
package/dist/renderer/types.d.ts
CHANGED
|
@@ -82,6 +82,12 @@ export interface Renderer {
|
|
|
82
82
|
setViewTransform(scale: number, x: number, y: number): void;
|
|
83
83
|
/** Current layout metrics (container + page dimensions). */
|
|
84
84
|
measure(): LayoutMetrics;
|
|
85
|
+
/**
|
|
86
|
+
* Largest texture dimension (px) the renderer can accept, when it has a hard cap (WebGL2's
|
|
87
|
+
* MAX_TEXTURE_SIZE). Sources read it to avoid rasterizing a zoom tile the GPU would reject and
|
|
88
|
+
* paint black. Omit when unbounded (the CSS renderer).
|
|
89
|
+
*/
|
|
90
|
+
readonly maxTextureSize?: number;
|
|
85
91
|
/**
|
|
86
92
|
* Optional: register a handler the renderer calls when it hits an unrecoverable
|
|
87
93
|
* state (e.g. a GPU context that cannot be restored), so the engine can swap to
|
|
@@ -12,6 +12,7 @@ export declare class WebglRenderer implements Renderer {
|
|
|
12
12
|
mount(container: HTMLElement): Promise<void>;
|
|
13
13
|
destroy(): void;
|
|
14
14
|
onFatal(handler: () => void): void;
|
|
15
|
+
get maxTextureSize(): number | undefined;
|
|
15
16
|
renderSpread(_spread: Spread, content: SpreadContent, options?: RenderOptions): void;
|
|
16
17
|
beginFlip(from: SpreadContent, to: SpreadContent, direction: FlipDirection, options?: RenderOptions): void;
|
|
17
18
|
setFlipProgress(t: number, _direction: FlipDirection): void;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as e, r as t, t as n } from "./styles-
|
|
1
|
+
import { i as e, r as t, t as n } from "./styles-C1Qp5pby.js";
|
|
2
2
|
//#region src/controls/socials.ts
|
|
3
3
|
var r = encodeURIComponent, i = [
|
|
4
4
|
{
|
|
@@ -280,4 +280,4 @@ var y = 132, b = 1600, x = class {
|
|
|
280
280
|
//#endregion
|
|
281
281
|
export { x as ShareDialog };
|
|
282
282
|
|
|
283
|
-
//# sourceMappingURL=share-
|
|
283
|
+
//# sourceMappingURL=share-Cq3RsjPr.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"share-WXdscwp3.js","names":["#doc","#lastFocus","#backdrop","#dialog","#linkRow","#socialRow","#qr","#onKey","#trapFocus","#copy","#timer"],"sources":["../src/controls/socials.ts","../src/controls/qr.ts","../src/controls/share.ts"],"sourcesContent":["/**\n * Where a reader can send a link, and how each service wants it.\n *\n * These are plain share-intent URLs — no SDKs, no third-party scripts, nothing that could track\n * the reader before they choose to share. Each brand mark is drawn from its own official glyph;\n * they are filled shapes rather than the stroked outlines used elsewhere in the toolbar, so they\n * carry `fill=\"currentColor\"` and no stroke.\n */\nexport interface Social {\n id: string;\n label: string;\n /** Inner SVG markup for a 24x24 viewBox, filled rather than stroked. */\n icon: string;\n href(url: string, title: string): string;\n}\n\nconst e = encodeURIComponent;\n\nexport const SOCIALS: readonly Social[] = [\n {\n id: 'facebook',\n label: 'Facebook',\n icon: '<path fill=\"currentColor\" stroke=\"none\" d=\"M22 12a10 10 0 1 0-11.56 9.88v-6.99H7.9V12h2.54V9.8c0-2.5 1.49-3.89 3.77-3.89 1.1 0 2.24.2 2.24.2v2.46h-1.26c-1.24 0-1.63.77-1.63 1.56V12h2.78l-.45 2.89h-2.33v6.99A10 10 0 0 0 22 12Z\"/>',\n href: (url) => `https://www.facebook.com/sharer/sharer.php?u=${e(url)}`,\n },\n {\n id: 'x',\n label: 'X (Twitter)',\n icon: '<path fill=\"currentColor\" stroke=\"none\" d=\"M17.53 3h3.06l-6.69 7.64L21.75 21h-6.16l-4.82-6.3L5.24 21H2.18l7.15-8.17L2.25 3h6.32l4.36 5.77L17.53 3Zm-1.07 16.17h1.69L7.62 4.73H5.8l10.66 14.44Z\"/>',\n href: (url, title) => `https://twitter.com/intent/tweet?url=${e(url)}&text=${e(title)}`,\n },\n {\n id: 'linkedin',\n label: 'LinkedIn',\n icon: '<path fill=\"currentColor\" stroke=\"none\" d=\"M20.45 20.45h-3.56v-5.57c0-1.33-.02-3.04-1.85-3.04-1.85 0-2.14 1.45-2.14 2.94v5.67H9.35V9h3.41v1.56h.05a3.74 3.74 0 0 1 3.37-1.85c3.6 0 4.27 2.37 4.27 5.46v6.28ZM5.34 7.43a2.07 2.07 0 1 1 0-4.13 2.07 2.07 0 0 1 0 4.13Zm1.78 13.02H3.55V9h3.57v11.45ZM22.22 0H1.77C.79 0 0 .77 0 1.72v20.56C0 23.23.79 24 1.77 24h20.45c.98 0 1.78-.77 1.78-1.72V1.72C24 .77 23.2 0 22.22 0Z\"/>',\n href: (url) => `https://www.linkedin.com/sharing/share-offsite/?url=${e(url)}`,\n },\n {\n id: 'whatsapp',\n label: 'WhatsApp',\n icon: '<path fill=\"currentColor\" stroke=\"none\" d=\"M12.04 2a9.9 9.9 0 0 0-8.5 14.9L2 22l5.25-1.38A9.9 9.9 0 1 0 12.04 2Zm5.8 14.06c-.25.7-1.44 1.33-1.99 1.38-.53.05-1.02.24-3.44-.72-2.9-1.14-4.73-4.1-4.87-4.29-.14-.19-1.16-1.54-1.16-2.94s.73-2.08 1-2.37c.26-.28.57-.35.76-.35h.55c.17 0 .42-.07.65.5.24.58.8 2 .87 2.14.07.14.12.31.02.5-.09.19-.14.3-.28.47l-.42.48c-.14.14-.28.29-.12.57.16.28.7 1.16 1.51 1.88 1.04.93 1.91 1.21 2.19 1.35.28.15.44.12.6-.07.17-.19.7-.81.88-1.09.19-.28.37-.23.63-.14.25.1 1.63.77 1.9.91.29.14.48.21.55.33.07.11.07.67-.18 1.37Z\"/>',\n href: (url, title) => `https://api.whatsapp.com/send?text=${e(`${title} ${url}`)}`,\n },\n {\n id: 'pinterest',\n label: 'Pinterest',\n icon: '<path fill=\"currentColor\" stroke=\"none\" d=\"M12 2a10 10 0 0 0-3.65 19.31c-.09-.78-.17-1.98.03-2.83.19-.78 1.2-4.98 1.2-4.98s-.3-.61-.3-1.52c0-1.42.82-2.48 1.85-2.48.87 0 1.3.66 1.3 1.44 0 .88-.56 2.2-.85 3.42-.24 1.02.51 1.86 1.52 1.86 1.83 0 3.23-1.93 3.23-4.7 0-2.46-1.77-4.18-4.29-4.18-2.92 0-4.64 2.19-4.64 4.46 0 .88.34 1.83.76 2.35a.3.3 0 0 1 .07.29l-.28 1.16c-.05.19-.15.23-.34.14-1.28-.6-2.08-2.47-2.08-3.98 0-3.24 2.35-6.21 6.79-6.21 3.56 0 6.33 2.54 6.33 5.93 0 3.54-2.23 6.39-5.32 6.39-1.04 0-2.02-.54-2.35-1.18l-.64 2.44c-.23.89-.86 2.01-1.28 2.69A10 10 0 1 0 12 2Z\"/>',\n href: (url, title) => `https://pinterest.com/pin/create/button/?url=${e(url)}&description=${e(title)}`,\n },\n {\n id: 'email',\n label: 'Email',\n icon: '<path fill=\"currentColor\" stroke=\"none\" d=\"M20 4H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2Zm0 4.24-7.47 4.67a1 1 0 0 1-1.06 0L4 8.24V6.4l8 5 8-5v1.84Z\"/>',\n href: (url, title) => `mailto:?subject=${e(title)}&body=${e(url)}`,\n },\n];\n","/**\n * A small QR encoder, enough to turn a URL into a scannable grid.\n *\n * Written rather than pulled in so the package keeps its \"no dependencies\" promise, and so no\n * reader's document URL is handed to a third-party image service to be logged. Deliberately\n * narrow: byte mode, error-correction level M, versions 1-10 (up to 271 characters at level M),\n * which covers any sane page URL. Longer input throws rather than silently producing a code that\n * will not scan.\n */\n\n/** Data codewords available per version at error-correction level M, versions 1-10. */\nconst DATA_CODEWORDS = [16, 28, 44, 64, 86, 108, 124, 154, 182, 216];\n/** Error-correction codewords per block, level M, versions 1-10. */\nconst EC_PER_BLOCK = [10, 16, 26, 18, 24, 16, 18, 22, 22, 26];\n/** How many EC blocks the data is split into, level M, versions 1-10. */\nconst BLOCKS = [1, 1, 1, 2, 2, 4, 4, 4, 5, 5];\n/** Where alignment patterns sit, per version (empty for version 1). */\nconst ALIGNMENT = [\n [],\n [6, 18],\n [6, 22],\n [6, 26],\n [6, 30],\n [6, 34],\n [6, 22, 38],\n [6, 24, 42],\n [6, 26, 46],\n [6, 28, 50],\n];\n\n// Galois field tables for GF(256), used by the Reed-Solomon error correction.\nconst EXP = new Uint8Array(512);\nconst LOG = new Uint8Array(256);\nfor (let i = 0, x = 1; i < 255; i++) {\n EXP[i] = x;\n LOG[x] = i;\n x <<= 1;\n if (x & 0x100) x ^= 0x11d; // the QR field's primitive polynomial\n}\nfor (let i = 255; i < 512; i++) EXP[i] = EXP[i - 255]!;\n\nconst mul = (a: number, b: number): number =>\n a === 0 || b === 0 ? 0 : EXP[(LOG[a]! + LOG[b]!) % 255]!;\n\n/** The generator polynomial for `degree` error-correction codewords. */\nfunction generator(degree: number): Uint8Array {\n let poly = new Uint8Array([1]);\n for (let i = 0; i < degree; i++) {\n const next = new Uint8Array(poly.length + 1);\n for (let j = 0; j < poly.length; j++) {\n // Written as explicit reads: a compound assignment on an indexed element is what\n // noUncheckedIndexedAccess objects to, since the target is possibly-undefined.\n next[j] = next[j]! ^ poly[j]!;\n next[j + 1] = next[j + 1]! ^ mul(poly[j]!, EXP[i]!);\n }\n poly = next;\n }\n return poly;\n}\n\n/** Reed-Solomon remainder: the error-correction codewords for one block. */\nfunction ecBlock(data: Uint8Array, count: number): Uint8Array {\n const gen = generator(count);\n const out = new Uint8Array(count);\n for (const byte of data) {\n const factor = byte ^ out[0]!;\n out.copyWithin(0, 1);\n out[count - 1] = 0;\n if (factor !== 0) {\n for (let i = 0; i < count; i++) out[i] = out[i]! ^ mul(gen[i + 1]!, factor);\n }\n }\n return out;\n}\n\n/** The 15-bit format string for level M and a mask, with its BCH check bits. */\nfunction formatBits(mask: number): number {\n const data = (0b00 << 3) | mask; // 00 = level M\n let rem = data;\n for (let i = 0; i < 10; i++) rem = (rem << 1) ^ ((rem >>> 9) * 0x537);\n return ((data << 10) | rem) ^ 0x5412;\n}\n\ntype Grid = { size: number; modules: Uint8Array; reserved: Uint8Array };\n\nconst at = (g: Grid, x: number, y: number): number => g.modules[y * g.size + x]!;\nconst set = (g: Grid, x: number, y: number, dark: boolean, reserve = true): void => {\n g.modules[y * g.size + x] = dark ? 1 : 0;\n if (reserve) g.reserved[y * g.size + x] = 1;\n};\n\nfunction placeFinder(g: Grid, cx: number, cy: number): void {\n for (let dy = -1; dy <= 7; dy++) {\n for (let dx = -1; dx <= 7; dx++) {\n const x = cx + dx;\n const y = cy + dy;\n if (x < 0 || y < 0 || x >= g.size || y >= g.size) continue;\n const d = Math.max(Math.abs(dx - 3), Math.abs(dy - 3));\n set(g, x, y, d !== 2 && d <= 3);\n }\n }\n}\n\n/** Encode `text` as a QR matrix: a square of 0/1 by row. */\nexport function encodeQr(text: string): { size: number; modules: Uint8Array } {\n const bytes = new TextEncoder().encode(text);\n const version = DATA_CODEWORDS.findIndex((cap) => bytes.length + 2 <= cap) + 1;\n if (version === 0) {\n throw new Error(`qr: ${bytes.length} bytes is too long for this encoder (max 214).`);\n }\n\n // --- bit stream: mode, length, payload, terminator, padding ---\n const capacity = DATA_CODEWORDS[version - 1]!;\n const bits: number[] = [];\n const push = (value: number, width: number): void => {\n for (let i = width - 1; i >= 0; i--) bits.push((value >>> i) & 1);\n };\n push(0b0100, 4); // byte mode\n push(bytes.length, version < 10 ? 8 : 16);\n for (const byte of bytes) push(byte, 8);\n push(0, Math.min(4, capacity * 8 - bits.length)); // terminator\n while (bits.length % 8) bits.push(0);\n const data = new Uint8Array(capacity);\n for (let i = 0; i < bits.length; i += 8) {\n let byte = 0;\n for (let b = 0; b < 8; b++) byte = (byte << 1) | bits[i + b]!;\n data[i / 8] = byte;\n }\n for (let i = bits.length / 8, pad = 0; i < capacity; i++, pad++) {\n data[i] = pad % 2 === 0 ? 0xec : 0x11; // the spec's alternating pad bytes\n }\n\n // --- split into blocks, compute EC, interleave ---\n const blockCount = BLOCKS[version - 1]!;\n const ecCount = EC_PER_BLOCK[version - 1]!;\n const shortLen = Math.floor(capacity / blockCount);\n const longCount = capacity % blockCount;\n const dataBlocks: Uint8Array[] = [];\n const ecBlocks: Uint8Array[] = [];\n for (let i = 0, offset = 0; i < blockCount; i++) {\n const len = shortLen + (i >= blockCount - longCount ? 1 : 0);\n const block = data.subarray(offset, offset + len);\n offset += len;\n dataBlocks.push(block);\n ecBlocks.push(ecBlock(block, ecCount));\n }\n const codewords: number[] = [];\n for (let i = 0; i < shortLen + 1; i++) {\n for (const block of dataBlocks) if (i < block.length) codewords.push(block[i]!);\n }\n for (let i = 0; i < ecCount; i++) {\n for (const block of ecBlocks) codewords.push(block[i]!);\n }\n\n // --- lay out the matrix ---\n const size = version * 4 + 17;\n const g: Grid = {\n size,\n modules: new Uint8Array(size * size),\n reserved: new Uint8Array(size * size),\n };\n placeFinder(g, 0, 0);\n placeFinder(g, size - 7, 0);\n placeFinder(g, 0, size - 7);\n for (let i = 8; i < size - 8; i++) {\n const dark = i % 2 === 0; // timing patterns\n set(g, i, 6, dark);\n set(g, 6, i, dark);\n }\n for (const ax of ALIGNMENT[version - 1]!) {\n for (const ay of ALIGNMENT[version - 1]!) {\n // Alignment patterns skip the corners already held by finders.\n if ((ax < 9 && ay < 9) || (ax < 9 && ay > size - 10) || (ax > size - 10 && ay < 9)) continue;\n for (let dy = -2; dy <= 2; dy++) {\n for (let dx = -2; dx <= 2; dx++) {\n set(g, ax + dx, ay + dy, Math.max(Math.abs(dx), Math.abs(dy)) !== 1);\n }\n }\n }\n }\n set(g, 8, size - 8, true); // the always-dark module\n // Reserve the format areas so data does not land in them.\n for (let i = 0; i < 9; i++) {\n if (i !== 6) set(g, i, 8, false), set(g, 8, i, false);\n }\n for (let i = 0; i < 8; i++) set(g, size - 1 - i, 8, false), set(g, 8, size - 1 - i, false);\n set(g, 8, size - 8, true);\n\n // --- weave the codewords in, masking as we go (mask 0: (x + y) % 2) ---\n let bit = 0;\n const total = codewords.length * 8;\n for (let right = size - 1; right >= 1; right -= 2) {\n if (right === 6) right = 5; // the vertical timing column is skipped\n for (let step = 0; step < size; step++) {\n for (let col = 0; col < 2; col++) {\n const x = right - col;\n const upward = ((right + 1) & 2) === 0;\n const y = upward ? size - 1 - step : step;\n if (g.reserved[y * size + x]) continue;\n let dark = false;\n if (bit < total) {\n dark = ((codewords[bit >>> 3]! >>> (7 - (bit & 7))) & 1) === 1;\n bit++;\n }\n if ((x + y) % 2 === 0) dark = !dark; // mask 0\n set(g, x, y, dark, false);\n }\n }\n }\n\n // --- format information, now that the mask is known ---\n const format = formatBits(0);\n for (let i = 0; i < 15; i++) {\n const dark = ((format >>> i) & 1) === 1;\n // Around the top-left finder...\n const a = i < 6 ? i : i < 8 ? i + 1 : 8;\n const b = i < 8 ? 8 : i < 9 ? 7 : 14 - i;\n set(g, a, 8, dark);\n set(g, 8, b, dark);\n // ...and the copy split between the other two.\n if (i < 8) set(g, size - 1 - i, 8, dark);\n else set(g, 8, size - 15 + i, dark);\n }\n set(g, 8, size - 8, true);\n\n return { size, modules: g.modules };\n}\n\n/** Render a QR matrix as an SVG string, sized to `px`. */\nexport function qrSvg(text: string, px: number): string {\n const { size, modules } = encodeQr(text);\n let path = '';\n for (let y = 0; y < size; y++) {\n for (let x = 0; x < size; x++) {\n if (modules[y * size + x]) path += `M${x} ${y}h1v1h-1z`;\n }\n }\n const quiet = 2; // the spec asks for 4; 2 scans fine and wastes less space on screen\n const box = size + quiet * 2;\n return `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${px}\" height=\"${px}\" viewBox=\"0 0 ${box} ${box}\" shape-rendering=\"crispEdges\"><rect width=\"${box}\" height=\"${box}\" fill=\"#fff\"/><g transform=\"translate(${quiet} ${quiet})\" fill=\"#000\">${`<path d=\"${path}\"/>`}</g></svg>`;\n}\n","import { createIcon, ICONS } from './icons';\nimport { SOCIALS } from './socials';\nimport { qrSvg } from './qr';\nimport { applyColorScheme } from './styles';\nimport type { ControlsColorScheme } from './types';\nimport type { Zine } from '../zine';\n\n/** QR edge length in CSS px. */\nconst QR_SIZE = 132;\n/** How long the copy button confirms for. */\nconst COPIED_MS = 1600;\n\n/**\n * The share dialog: a QR code, the link, a copy button and the usual social destinations.\n *\n * Loaded on demand — the whole module, its icons and the QR encoder arrive only when a reader\n * presses Share, so a book nobody shares pays nothing for it.\n */\nexport class ShareDialog {\n #doc: Document;\n #backdrop: HTMLElement;\n #dialog: HTMLElement;\n #lastFocus: Element | null;\n #timer: ReturnType<typeof setTimeout> | null = null;\n #onKey: (e: KeyboardEvent) => void;\n\n constructor(zine: Zine, container: HTMLElement, colorScheme: ControlsColorScheme = 'auto') {\n const doc = container.ownerDocument!;\n this.#doc = doc;\n this.#lastFocus = doc.activeElement;\n\n const url = zine.pageLink();\n const title = doc.title || 'Flipbook';\n\n this.#backdrop = doc.createElement('div');\n this.#backdrop.className = 'zine-share-backdrop';\n // The dialog is mounted on <body>, outside the toolbar, so it does not inherit the stamp;\n // apply it here. color-scheme then inherits down to the dialog and its fields.\n applyColorScheme(this.#backdrop, colorScheme);\n this.#backdrop.addEventListener('pointerdown', (e) => {\n if (e.target === this.#backdrop) this.close();\n });\n\n this.#dialog = doc.createElement('div');\n this.#dialog.className = 'zine-share';\n this.#dialog.setAttribute('role', 'dialog');\n this.#dialog.setAttribute('aria-modal', 'true');\n this.#dialog.setAttribute('aria-label', 'Share');\n this.#backdrop.appendChild(this.#dialog);\n\n const head = doc.createElement('div');\n head.className = 'zine-share-head';\n const heading = doc.createElement('strong');\n heading.textContent = 'Share';\n const close = doc.createElement('button');\n close.type = 'button';\n close.className = 'zine-share-close';\n close.setAttribute('aria-label', 'Close');\n close.appendChild(createIcon(doc, ICONS.close));\n close.addEventListener('click', () => this.close());\n head.append(heading, close);\n\n this.#dialog.append(head, this.#linkRow(doc, url), this.#socialRow(doc, url, title));\n const qr = this.#qr(doc, url);\n if (qr) this.#dialog.insertBefore(qr, this.#dialog.children[1]!);\n\n container.ownerDocument.body.appendChild(this.#backdrop);\n\n this.#onKey = (e): void => {\n if (e.key === 'Escape') this.close();\n if (e.key === 'Tab') this.#trapFocus(e);\n };\n this.#backdrop.addEventListener('keydown', this.#onKey);\n close.focus();\n }\n\n /** The QR, or nothing if the URL is too long to encode — better no code than a broken one. */\n #qr(doc: Document, url: string): HTMLElement | null {\n if (!url) return null;\n try {\n const wrap = doc.createElement('div');\n wrap.className = 'zine-share-qr';\n wrap.innerHTML = qrSvg(url, QR_SIZE);\n wrap.setAttribute('aria-label', 'QR code for this page');\n wrap.setAttribute('role', 'img');\n return wrap;\n } catch {\n return null;\n }\n }\n\n #linkRow(doc: Document, url: string): HTMLElement {\n const row = doc.createElement('div');\n row.className = 'zine-share-link';\n const input = doc.createElement('input');\n input.type = 'text';\n input.readOnly = true;\n input.value = url;\n input.setAttribute('aria-label', 'Link to this page');\n input.addEventListener('focus', () => input.select());\n\n const copy = doc.createElement('button');\n copy.type = 'button';\n copy.className = 'zine-share-copy';\n copy.textContent = 'Copy';\n copy.addEventListener('click', () => {\n void this.#copy(url, copy);\n });\n row.append(input, copy);\n return row;\n }\n\n async #copy(url: string, button: HTMLButtonElement): Promise<void> {\n try {\n await navigator.clipboard?.writeText(url);\n button.textContent = 'Copied';\n } catch {\n button.textContent = 'Press Ctrl+C';\n }\n if (this.#timer) clearTimeout(this.#timer);\n this.#timer = setTimeout(() => {\n button.textContent = 'Copy';\n }, COPIED_MS);\n }\n\n #socialRow(doc: Document, url: string, title: string): HTMLElement {\n const row = doc.createElement('div');\n row.className = 'zine-share-socials';\n for (const social of SOCIALS) {\n const link = doc.createElement('a');\n link.className = 'zine-share-social';\n link.href = social.href(url, title);\n link.title = social.label;\n link.setAttribute('aria-label', `Share on ${social.label}`);\n if (social.id !== 'email') {\n link.target = '_blank';\n link.rel = 'noopener noreferrer';\n }\n link.appendChild(createIcon(doc, social.icon));\n row.appendChild(link);\n }\n return row;\n }\n\n /** Keep Tab inside the dialog while it is open. */\n #trapFocus(event: KeyboardEvent): void {\n const focusable = [\n ...this.#dialog.querySelectorAll<HTMLElement>('button, a[href], input'),\n ].filter((el) => !el.hasAttribute('disabled'));\n if (focusable.length === 0) return;\n const first = focusable[0]!;\n const last = focusable.at(-1)!;\n const active = this.#doc.activeElement;\n if (event.shiftKey && active === first) {\n last.focus();\n event.preventDefault();\n } else if (!event.shiftKey && active === last) {\n first.focus();\n event.preventDefault();\n }\n }\n\n close(): void {\n this.destroy();\n }\n\n destroy(): void {\n if (this.#timer) clearTimeout(this.#timer);\n this.#timer = null;\n this.#backdrop.removeEventListener('keydown', this.#onKey);\n this.#backdrop.remove();\n // Hand focus back to whatever opened the dialog.\n (this.#lastFocus as HTMLElement | null)?.focus?.();\n }\n}\n"],"mappings":";;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;;;ACxOA,IAAM,IAAU,KAEV,IAAY,MAQL,IAAb,MAAyB;CACvB;CACA;CACA;CACA;CACA,KAA+C;CAC/C;CAEA,YAAY,GAAY,GAAwB,IAAmC,QAAQ;EACzF,IAAM,IAAM,EAAU;EAEtB,AADA,KAAKA,KAAO,GACZ,KAAKC,KAAa,EAAI;EAEtB,IAAM,IAAM,EAAK,SAAS,GACpB,IAAQ,EAAI,SAAS;EAgB3B,AAdA,KAAKC,KAAY,EAAI,cAAc,KAAK,GACxC,KAAKA,GAAU,YAAY,uBAG3B,EAAiB,KAAKA,IAAW,CAAW,GAC5C,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"}
|
|
1
|
+
{"version":3,"file":"share-Cq3RsjPr.js","names":["#doc","#lastFocus","#backdrop","#dialog","#linkRow","#socialRow","#qr","#onKey","#trapFocus","#copy","#timer"],"sources":["../src/controls/socials.ts","../src/controls/qr.ts","../src/controls/share.ts"],"sourcesContent":["/**\n * Where a reader can send a link, and how each service wants it.\n *\n * These are plain share-intent URLs — no SDKs, no third-party scripts, nothing that could track\n * the reader before they choose to share. Each brand mark is drawn from its own official glyph;\n * they are filled shapes rather than the stroked outlines used elsewhere in the toolbar, so they\n * carry `fill=\"currentColor\"` and no stroke.\n */\nexport interface Social {\n id: string;\n label: string;\n /** Inner SVG markup for a 24x24 viewBox, filled rather than stroked. */\n icon: string;\n href(url: string, title: string): string;\n}\n\nconst e = encodeURIComponent;\n\nexport const SOCIALS: readonly Social[] = [\n {\n id: 'facebook',\n label: 'Facebook',\n icon: '<path fill=\"currentColor\" stroke=\"none\" d=\"M22 12a10 10 0 1 0-11.56 9.88v-6.99H7.9V12h2.54V9.8c0-2.5 1.49-3.89 3.77-3.89 1.1 0 2.24.2 2.24.2v2.46h-1.26c-1.24 0-1.63.77-1.63 1.56V12h2.78l-.45 2.89h-2.33v6.99A10 10 0 0 0 22 12Z\"/>',\n href: (url) => `https://www.facebook.com/sharer/sharer.php?u=${e(url)}`,\n },\n {\n id: 'x',\n label: 'X (Twitter)',\n icon: '<path fill=\"currentColor\" stroke=\"none\" d=\"M17.53 3h3.06l-6.69 7.64L21.75 21h-6.16l-4.82-6.3L5.24 21H2.18l7.15-8.17L2.25 3h6.32l4.36 5.77L17.53 3Zm-1.07 16.17h1.69L7.62 4.73H5.8l10.66 14.44Z\"/>',\n href: (url, title) => `https://twitter.com/intent/tweet?url=${e(url)}&text=${e(title)}`,\n },\n {\n id: 'linkedin',\n label: 'LinkedIn',\n icon: '<path fill=\"currentColor\" stroke=\"none\" d=\"M20.45 20.45h-3.56v-5.57c0-1.33-.02-3.04-1.85-3.04-1.85 0-2.14 1.45-2.14 2.94v5.67H9.35V9h3.41v1.56h.05a3.74 3.74 0 0 1 3.37-1.85c3.6 0 4.27 2.37 4.27 5.46v6.28ZM5.34 7.43a2.07 2.07 0 1 1 0-4.13 2.07 2.07 0 0 1 0 4.13Zm1.78 13.02H3.55V9h3.57v11.45ZM22.22 0H1.77C.79 0 0 .77 0 1.72v20.56C0 23.23.79 24 1.77 24h20.45c.98 0 1.78-.77 1.78-1.72V1.72C24 .77 23.2 0 22.22 0Z\"/>',\n href: (url) => `https://www.linkedin.com/sharing/share-offsite/?url=${e(url)}`,\n },\n {\n id: 'whatsapp',\n label: 'WhatsApp',\n icon: '<path fill=\"currentColor\" stroke=\"none\" d=\"M12.04 2a9.9 9.9 0 0 0-8.5 14.9L2 22l5.25-1.38A9.9 9.9 0 1 0 12.04 2Zm5.8 14.06c-.25.7-1.44 1.33-1.99 1.38-.53.05-1.02.24-3.44-.72-2.9-1.14-4.73-4.1-4.87-4.29-.14-.19-1.16-1.54-1.16-2.94s.73-2.08 1-2.37c.26-.28.57-.35.76-.35h.55c.17 0 .42-.07.65.5.24.58.8 2 .87 2.14.07.14.12.31.02.5-.09.19-.14.3-.28.47l-.42.48c-.14.14-.28.29-.12.57.16.28.7 1.16 1.51 1.88 1.04.93 1.91 1.21 2.19 1.35.28.15.44.12.6-.07.17-.19.7-.81.88-1.09.19-.28.37-.23.63-.14.25.1 1.63.77 1.9.91.29.14.48.21.55.33.07.11.07.67-.18 1.37Z\"/>',\n href: (url, title) => `https://api.whatsapp.com/send?text=${e(`${title} ${url}`)}`,\n },\n {\n id: 'pinterest',\n label: 'Pinterest',\n icon: '<path fill=\"currentColor\" stroke=\"none\" d=\"M12 2a10 10 0 0 0-3.65 19.31c-.09-.78-.17-1.98.03-2.83.19-.78 1.2-4.98 1.2-4.98s-.3-.61-.3-1.52c0-1.42.82-2.48 1.85-2.48.87 0 1.3.66 1.3 1.44 0 .88-.56 2.2-.85 3.42-.24 1.02.51 1.86 1.52 1.86 1.83 0 3.23-1.93 3.23-4.7 0-2.46-1.77-4.18-4.29-4.18-2.92 0-4.64 2.19-4.64 4.46 0 .88.34 1.83.76 2.35a.3.3 0 0 1 .07.29l-.28 1.16c-.05.19-.15.23-.34.14-1.28-.6-2.08-2.47-2.08-3.98 0-3.24 2.35-6.21 6.79-6.21 3.56 0 6.33 2.54 6.33 5.93 0 3.54-2.23 6.39-5.32 6.39-1.04 0-2.02-.54-2.35-1.18l-.64 2.44c-.23.89-.86 2.01-1.28 2.69A10 10 0 1 0 12 2Z\"/>',\n href: (url, title) => `https://pinterest.com/pin/create/button/?url=${e(url)}&description=${e(title)}`,\n },\n {\n id: 'email',\n label: 'Email',\n icon: '<path fill=\"currentColor\" stroke=\"none\" d=\"M20 4H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2Zm0 4.24-7.47 4.67a1 1 0 0 1-1.06 0L4 8.24V6.4l8 5 8-5v1.84Z\"/>',\n href: (url, title) => `mailto:?subject=${e(title)}&body=${e(url)}`,\n },\n];\n","/**\n * A small QR encoder, enough to turn a URL into a scannable grid.\n *\n * Written rather than pulled in so the package keeps its \"no dependencies\" promise, and so no\n * reader's document URL is handed to a third-party image service to be logged. Deliberately\n * narrow: byte mode, error-correction level M, versions 1-10 (up to 271 characters at level M),\n * which covers any sane page URL. Longer input throws rather than silently producing a code that\n * will not scan.\n */\n\n/** Data codewords available per version at error-correction level M, versions 1-10. */\nconst DATA_CODEWORDS = [16, 28, 44, 64, 86, 108, 124, 154, 182, 216];\n/** Error-correction codewords per block, level M, versions 1-10. */\nconst EC_PER_BLOCK = [10, 16, 26, 18, 24, 16, 18, 22, 22, 26];\n/** How many EC blocks the data is split into, level M, versions 1-10. */\nconst BLOCKS = [1, 1, 1, 2, 2, 4, 4, 4, 5, 5];\n/** Where alignment patterns sit, per version (empty for version 1). */\nconst ALIGNMENT = [\n [],\n [6, 18],\n [6, 22],\n [6, 26],\n [6, 30],\n [6, 34],\n [6, 22, 38],\n [6, 24, 42],\n [6, 26, 46],\n [6, 28, 50],\n];\n\n// Galois field tables for GF(256), used by the Reed-Solomon error correction.\nconst EXP = new Uint8Array(512);\nconst LOG = new Uint8Array(256);\nfor (let i = 0, x = 1; i < 255; i++) {\n EXP[i] = x;\n LOG[x] = i;\n x <<= 1;\n if (x & 0x100) x ^= 0x11d; // the QR field's primitive polynomial\n}\nfor (let i = 255; i < 512; i++) EXP[i] = EXP[i - 255]!;\n\nconst mul = (a: number, b: number): number =>\n a === 0 || b === 0 ? 0 : EXP[(LOG[a]! + LOG[b]!) % 255]!;\n\n/** The generator polynomial for `degree` error-correction codewords. */\nfunction generator(degree: number): Uint8Array {\n let poly = new Uint8Array([1]);\n for (let i = 0; i < degree; i++) {\n const next = new Uint8Array(poly.length + 1);\n for (let j = 0; j < poly.length; j++) {\n // Written as explicit reads: a compound assignment on an indexed element is what\n // noUncheckedIndexedAccess objects to, since the target is possibly-undefined.\n next[j] = next[j]! ^ poly[j]!;\n next[j + 1] = next[j + 1]! ^ mul(poly[j]!, EXP[i]!);\n }\n poly = next;\n }\n return poly;\n}\n\n/** Reed-Solomon remainder: the error-correction codewords for one block. */\nfunction ecBlock(data: Uint8Array, count: number): Uint8Array {\n const gen = generator(count);\n const out = new Uint8Array(count);\n for (const byte of data) {\n const factor = byte ^ out[0]!;\n out.copyWithin(0, 1);\n out[count - 1] = 0;\n if (factor !== 0) {\n for (let i = 0; i < count; i++) out[i] = out[i]! ^ mul(gen[i + 1]!, factor);\n }\n }\n return out;\n}\n\n/** The 15-bit format string for level M and a mask, with its BCH check bits. */\nfunction formatBits(mask: number): number {\n const data = (0b00 << 3) | mask; // 00 = level M\n let rem = data;\n for (let i = 0; i < 10; i++) rem = (rem << 1) ^ ((rem >>> 9) * 0x537);\n return ((data << 10) | rem) ^ 0x5412;\n}\n\ntype Grid = { size: number; modules: Uint8Array; reserved: Uint8Array };\n\nconst at = (g: Grid, x: number, y: number): number => g.modules[y * g.size + x]!;\nconst set = (g: Grid, x: number, y: number, dark: boolean, reserve = true): void => {\n g.modules[y * g.size + x] = dark ? 1 : 0;\n if (reserve) g.reserved[y * g.size + x] = 1;\n};\n\nfunction placeFinder(g: Grid, cx: number, cy: number): void {\n for (let dy = -1; dy <= 7; dy++) {\n for (let dx = -1; dx <= 7; dx++) {\n const x = cx + dx;\n const y = cy + dy;\n if (x < 0 || y < 0 || x >= g.size || y >= g.size) continue;\n const d = Math.max(Math.abs(dx - 3), Math.abs(dy - 3));\n set(g, x, y, d !== 2 && d <= 3);\n }\n }\n}\n\n/** Encode `text` as a QR matrix: a square of 0/1 by row. */\nexport function encodeQr(text: string): { size: number; modules: Uint8Array } {\n const bytes = new TextEncoder().encode(text);\n const version = DATA_CODEWORDS.findIndex((cap) => bytes.length + 2 <= cap) + 1;\n if (version === 0) {\n throw new Error(`qr: ${bytes.length} bytes is too long for this encoder (max 214).`);\n }\n\n // --- bit stream: mode, length, payload, terminator, padding ---\n const capacity = DATA_CODEWORDS[version - 1]!;\n const bits: number[] = [];\n const push = (value: number, width: number): void => {\n for (let i = width - 1; i >= 0; i--) bits.push((value >>> i) & 1);\n };\n push(0b0100, 4); // byte mode\n push(bytes.length, version < 10 ? 8 : 16);\n for (const byte of bytes) push(byte, 8);\n push(0, Math.min(4, capacity * 8 - bits.length)); // terminator\n while (bits.length % 8) bits.push(0);\n const data = new Uint8Array(capacity);\n for (let i = 0; i < bits.length; i += 8) {\n let byte = 0;\n for (let b = 0; b < 8; b++) byte = (byte << 1) | bits[i + b]!;\n data[i / 8] = byte;\n }\n for (let i = bits.length / 8, pad = 0; i < capacity; i++, pad++) {\n data[i] = pad % 2 === 0 ? 0xec : 0x11; // the spec's alternating pad bytes\n }\n\n // --- split into blocks, compute EC, interleave ---\n const blockCount = BLOCKS[version - 1]!;\n const ecCount = EC_PER_BLOCK[version - 1]!;\n const shortLen = Math.floor(capacity / blockCount);\n const longCount = capacity % blockCount;\n const dataBlocks: Uint8Array[] = [];\n const ecBlocks: Uint8Array[] = [];\n for (let i = 0, offset = 0; i < blockCount; i++) {\n const len = shortLen + (i >= blockCount - longCount ? 1 : 0);\n const block = data.subarray(offset, offset + len);\n offset += len;\n dataBlocks.push(block);\n ecBlocks.push(ecBlock(block, ecCount));\n }\n const codewords: number[] = [];\n for (let i = 0; i < shortLen + 1; i++) {\n for (const block of dataBlocks) if (i < block.length) codewords.push(block[i]!);\n }\n for (let i = 0; i < ecCount; i++) {\n for (const block of ecBlocks) codewords.push(block[i]!);\n }\n\n // --- lay out the matrix ---\n const size = version * 4 + 17;\n const g: Grid = {\n size,\n modules: new Uint8Array(size * size),\n reserved: new Uint8Array(size * size),\n };\n placeFinder(g, 0, 0);\n placeFinder(g, size - 7, 0);\n placeFinder(g, 0, size - 7);\n for (let i = 8; i < size - 8; i++) {\n const dark = i % 2 === 0; // timing patterns\n set(g, i, 6, dark);\n set(g, 6, i, dark);\n }\n for (const ax of ALIGNMENT[version - 1]!) {\n for (const ay of ALIGNMENT[version - 1]!) {\n // Alignment patterns skip the corners already held by finders.\n if ((ax < 9 && ay < 9) || (ax < 9 && ay > size - 10) || (ax > size - 10 && ay < 9)) continue;\n for (let dy = -2; dy <= 2; dy++) {\n for (let dx = -2; dx <= 2; dx++) {\n set(g, ax + dx, ay + dy, Math.max(Math.abs(dx), Math.abs(dy)) !== 1);\n }\n }\n }\n }\n set(g, 8, size - 8, true); // the always-dark module\n // Reserve the format areas so data does not land in them.\n for (let i = 0; i < 9; i++) {\n if (i !== 6) set(g, i, 8, false), set(g, 8, i, false);\n }\n for (let i = 0; i < 8; i++) set(g, size - 1 - i, 8, false), set(g, 8, size - 1 - i, false);\n set(g, 8, size - 8, true);\n\n // --- weave the codewords in, masking as we go (mask 0: (x + y) % 2) ---\n let bit = 0;\n const total = codewords.length * 8;\n for (let right = size - 1; right >= 1; right -= 2) {\n if (right === 6) right = 5; // the vertical timing column is skipped\n for (let step = 0; step < size; step++) {\n for (let col = 0; col < 2; col++) {\n const x = right - col;\n const upward = ((right + 1) & 2) === 0;\n const y = upward ? size - 1 - step : step;\n if (g.reserved[y * size + x]) continue;\n let dark = false;\n if (bit < total) {\n dark = ((codewords[bit >>> 3]! >>> (7 - (bit & 7))) & 1) === 1;\n bit++;\n }\n if ((x + y) % 2 === 0) dark = !dark; // mask 0\n set(g, x, y, dark, false);\n }\n }\n }\n\n // --- format information, now that the mask is known ---\n const format = formatBits(0);\n for (let i = 0; i < 15; i++) {\n const dark = ((format >>> i) & 1) === 1;\n // Around the top-left finder...\n const a = i < 6 ? i : i < 8 ? i + 1 : 8;\n const b = i < 8 ? 8 : i < 9 ? 7 : 14 - i;\n set(g, a, 8, dark);\n set(g, 8, b, dark);\n // ...and the copy split between the other two.\n if (i < 8) set(g, size - 1 - i, 8, dark);\n else set(g, 8, size - 15 + i, dark);\n }\n set(g, 8, size - 8, true);\n\n return { size, modules: g.modules };\n}\n\n/** Render a QR matrix as an SVG string, sized to `px`. */\nexport function qrSvg(text: string, px: number): string {\n const { size, modules } = encodeQr(text);\n let path = '';\n for (let y = 0; y < size; y++) {\n for (let x = 0; x < size; x++) {\n if (modules[y * size + x]) path += `M${x} ${y}h1v1h-1z`;\n }\n }\n const quiet = 2; // the spec asks for 4; 2 scans fine and wastes less space on screen\n const box = size + quiet * 2;\n return `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${px}\" height=\"${px}\" viewBox=\"0 0 ${box} ${box}\" shape-rendering=\"crispEdges\"><rect width=\"${box}\" height=\"${box}\" fill=\"#fff\"/><g transform=\"translate(${quiet} ${quiet})\" fill=\"#000\">${`<path d=\"${path}\"/>`}</g></svg>`;\n}\n","import { createIcon, ICONS } from './icons';\nimport { SOCIALS } from './socials';\nimport { qrSvg } from './qr';\nimport { applyColorScheme } from './styles';\nimport type { ControlsColorScheme } from './types';\nimport type { Zine } from '../zine';\n\n/** QR edge length in CSS px. */\nconst QR_SIZE = 132;\n/** How long the copy button confirms for. */\nconst COPIED_MS = 1600;\n\n/**\n * The share dialog: a QR code, the link, a copy button and the usual social destinations.\n *\n * Loaded on demand — the whole module, its icons and the QR encoder arrive only when a reader\n * presses Share, so a book nobody shares pays nothing for it.\n */\nexport class ShareDialog {\n #doc: Document;\n #backdrop: HTMLElement;\n #dialog: HTMLElement;\n #lastFocus: Element | null;\n #timer: ReturnType<typeof setTimeout> | null = null;\n #onKey: (e: KeyboardEvent) => void;\n\n constructor(zine: Zine, container: HTMLElement, colorScheme: ControlsColorScheme = 'auto') {\n const doc = container.ownerDocument!;\n this.#doc = doc;\n this.#lastFocus = doc.activeElement;\n\n const url = zine.pageLink();\n const title = doc.title || 'Flipbook';\n\n this.#backdrop = doc.createElement('div');\n this.#backdrop.className = 'zine-share-backdrop';\n // The dialog is mounted on <body>, outside the toolbar, so it does not inherit the stamp;\n // apply it here. color-scheme then inherits down to the dialog and its fields.\n applyColorScheme(this.#backdrop, colorScheme);\n this.#backdrop.addEventListener('pointerdown', (e) => {\n if (e.target === this.#backdrop) this.close();\n });\n\n this.#dialog = doc.createElement('div');\n this.#dialog.className = 'zine-share';\n this.#dialog.setAttribute('role', 'dialog');\n this.#dialog.setAttribute('aria-modal', 'true');\n this.#dialog.setAttribute('aria-label', 'Share');\n this.#backdrop.appendChild(this.#dialog);\n\n const head = doc.createElement('div');\n head.className = 'zine-share-head';\n const heading = doc.createElement('strong');\n heading.textContent = 'Share';\n const close = doc.createElement('button');\n close.type = 'button';\n close.className = 'zine-share-close';\n close.setAttribute('aria-label', 'Close');\n close.appendChild(createIcon(doc, ICONS.close));\n close.addEventListener('click', () => this.close());\n head.append(heading, close);\n\n this.#dialog.append(head, this.#linkRow(doc, url), this.#socialRow(doc, url, title));\n const qr = this.#qr(doc, url);\n if (qr) this.#dialog.insertBefore(qr, this.#dialog.children[1]!);\n\n container.ownerDocument.body.appendChild(this.#backdrop);\n\n this.#onKey = (e): void => {\n if (e.key === 'Escape') this.close();\n if (e.key === 'Tab') this.#trapFocus(e);\n };\n this.#backdrop.addEventListener('keydown', this.#onKey);\n close.focus();\n }\n\n /** The QR, or nothing if the URL is too long to encode — better no code than a broken one. */\n #qr(doc: Document, url: string): HTMLElement | null {\n if (!url) return null;\n try {\n const wrap = doc.createElement('div');\n wrap.className = 'zine-share-qr';\n wrap.innerHTML = qrSvg(url, QR_SIZE);\n wrap.setAttribute('aria-label', 'QR code for this page');\n wrap.setAttribute('role', 'img');\n return wrap;\n } catch {\n return null;\n }\n }\n\n #linkRow(doc: Document, url: string): HTMLElement {\n const row = doc.createElement('div');\n row.className = 'zine-share-link';\n const input = doc.createElement('input');\n input.type = 'text';\n input.readOnly = true;\n input.value = url;\n input.setAttribute('aria-label', 'Link to this page');\n input.addEventListener('focus', () => input.select());\n\n const copy = doc.createElement('button');\n copy.type = 'button';\n copy.className = 'zine-share-copy';\n copy.textContent = 'Copy';\n copy.addEventListener('click', () => {\n void this.#copy(url, copy);\n });\n row.append(input, copy);\n return row;\n }\n\n async #copy(url: string, button: HTMLButtonElement): Promise<void> {\n try {\n await navigator.clipboard?.writeText(url);\n button.textContent = 'Copied';\n } catch {\n button.textContent = 'Press Ctrl+C';\n }\n if (this.#timer) clearTimeout(this.#timer);\n this.#timer = setTimeout(() => {\n button.textContent = 'Copy';\n }, COPIED_MS);\n }\n\n #socialRow(doc: Document, url: string, title: string): HTMLElement {\n const row = doc.createElement('div');\n row.className = 'zine-share-socials';\n for (const social of SOCIALS) {\n const link = doc.createElement('a');\n link.className = 'zine-share-social';\n link.href = social.href(url, title);\n link.title = social.label;\n link.setAttribute('aria-label', `Share on ${social.label}`);\n if (social.id !== 'email') {\n link.target = '_blank';\n link.rel = 'noopener noreferrer';\n }\n link.appendChild(createIcon(doc, social.icon));\n row.appendChild(link);\n }\n return row;\n }\n\n /** Keep Tab inside the dialog while it is open. */\n #trapFocus(event: KeyboardEvent): void {\n const focusable = [\n ...this.#dialog.querySelectorAll<HTMLElement>('button, a[href], input'),\n ].filter((el) => !el.hasAttribute('disabled'));\n if (focusable.length === 0) return;\n const first = focusable[0]!;\n const last = focusable.at(-1)!;\n const active = this.#doc.activeElement;\n if (event.shiftKey && active === first) {\n last.focus();\n event.preventDefault();\n } else if (!event.shiftKey && active === last) {\n first.focus();\n event.preventDefault();\n }\n }\n\n close(): void {\n this.destroy();\n }\n\n destroy(): void {\n if (this.#timer) clearTimeout(this.#timer);\n this.#timer = null;\n this.#backdrop.removeEventListener('keydown', this.#onKey);\n this.#backdrop.remove();\n // Hand focus back to whatever opened the dialog.\n (this.#lastFocus as HTMLElement | null)?.focus?.();\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bundled default page-flip sound, as a base64 `data:` URI.
|
|
3
|
+
*
|
|
4
|
+
* Generated by `scripts/embed-flip-clip.mjs` from `packages/core/assets/flip.mp3` — do not
|
|
5
|
+
* hand-edit. Inlined rather than shipped as a separate asset so the default clip needs no bundler
|
|
6
|
+
* URL resolution and no network fetch (works offline / under strict CSP); consumers who want an
|
|
7
|
+
* external file use the `sound.url` option instead.
|
|
8
|
+
*/
|
|
9
|
+
export declare const FLIP_CLIP = "data:audio/mpeg;base64,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VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//PkZAAA4AQAAHgAAAHACAAA8AAAMBkak0dlxzAYGoNHlcZMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//PkZAAAAAGkAAAAAAAAA0gAAAAATEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//PkZAAAAAGkAAAAAAAAA0gAAAAATEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//PkZAAAAAGkAAAAAAAAA0gAAAAATEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//PkZAAAAAGkAAAAAAAAA0gAAAAATEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//PkZAAAAAGkAAAAAAAAA0gAAAAATEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//PkZAAAAAGkAAAAAAAAA0gAAAAATEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//PkZAAAAAGkAAAAAAAAA0gAAAAATEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//PkZAAAAAGkAAAAAAAAA0gAAAAATEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//PkZAAAAAGkAAAAAAAAA0gAAAAATEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//PkZAAAAAGkAAAAAAAAA0gAAAAATEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV";
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export interface FlipSoundOptions {
|
|
2
|
+
/** A clip URL that overrides the bundled default (fetched at runtime). */
|
|
3
|
+
url?: string;
|
|
4
|
+
/** Playback volume, 0..1. Default 0.5. */
|
|
5
|
+
volume?: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Plays a short sound on each page turn.
|
|
9
|
+
*
|
|
10
|
+
* Everything here is best-effort: a browser that gates audio behind a gesture, has no Web Audio, or
|
|
11
|
+
* cannot decode the clip simply stays silent. A page flip must never throw or stall because of
|
|
12
|
+
* sound, so every path is guarded and failures are swallowed.
|
|
13
|
+
*
|
|
14
|
+
* Web Audio (not `<audio>`) so rapid flips overlap cleanly — each turn plays a fresh buffer source
|
|
15
|
+
* through one shared gain node (volume + mute).
|
|
16
|
+
*/
|
|
17
|
+
export declare class FlipSound {
|
|
18
|
+
#private;
|
|
19
|
+
constructor(options?: FlipSoundOptions);
|
|
20
|
+
/** Whether this environment can play at all — no context constructor means never. */
|
|
21
|
+
get available(): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Play the clip once. Best-effort: silent if audio is unavailable, muted, still gesture-locked,
|
|
24
|
+
* or the clip has not decoded yet (the first flip may miss while it decodes; the rest play).
|
|
25
|
+
*/
|
|
26
|
+
play(): void;
|
|
27
|
+
setMuted(muted: boolean): void;
|
|
28
|
+
isMuted(): boolean;
|
|
29
|
+
destroy(): void;
|
|
30
|
+
}
|
package/dist/source/types.d.ts
CHANGED
|
@@ -71,6 +71,11 @@ export interface PageRequest {
|
|
|
71
71
|
width: number;
|
|
72
72
|
height: number;
|
|
73
73
|
};
|
|
74
|
+
/**
|
|
75
|
+
* Upper bound (px) on either raster dimension the caller can upload; a source honouring it caps
|
|
76
|
+
* its render scale so the tile fits the GPU. A hint, like the rest of PageRequest.
|
|
77
|
+
*/
|
|
78
|
+
maxSize?: number;
|
|
74
79
|
}
|
|
75
80
|
/** One entry in a document's table of contents. */
|
|
76
81
|
export interface OutlineItem {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
//#region src/controls/icons.ts
|
|
2
|
+
var e = {
|
|
3
|
+
prev: "<path d=\"m15 18-6-6 6-6\"/>",
|
|
4
|
+
next: "<path d=\"m9 18 6-6-6-6\"/>",
|
|
5
|
+
first: "<path d=\"m11 17-5-5 5-5\"/><path d=\"m18 17-5-5 5-5\"/>",
|
|
6
|
+
last: "<path d=\"m6 17 5-5-5-5\"/><path d=\"m13 17 5-5-5-5\"/>",
|
|
7
|
+
zoomIn: "<circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M8 12h8\"/><path d=\"M12 8v8\"/>",
|
|
8
|
+
zoomOut: "<circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M8 12h8\"/>",
|
|
9
|
+
search: "<circle cx=\"11\" cy=\"11\" r=\"8\"/><path d=\"m21 21-4.3-4.3\"/>",
|
|
10
|
+
share: "<circle cx=\"18\" cy=\"5\" r=\"3\"/><circle cx=\"6\" cy=\"12\" r=\"3\"/><circle cx=\"18\" cy=\"19\" r=\"3\"/><path d=\"m8.6 13.5 6.8 4\"/><path d=\"m15.4 6.5-6.8 4\"/>",
|
|
11
|
+
menu: "<circle cx=\"12\" cy=\"12\" r=\"1\"/><circle cx=\"12\" cy=\"5\" r=\"1\"/><circle cx=\"12\" cy=\"19\" r=\"1\"/>",
|
|
12
|
+
fullscreen: "<path d=\"M8 3H5a2 2 0 0 0-2 2v3\"/><path d=\"M21 8V5a2 2 0 0 0-2-2h-3\"/><path d=\"M3 16v3a2 2 0 0 0 2 2h3\"/><path d=\"M16 21h3a2 2 0 0 0 2-2v-3\"/>",
|
|
13
|
+
exitFullscreen: "<path d=\"M8 3v3a2 2 0 0 1-2 2H3\"/><path d=\"M21 8h-3a2 2 0 0 1-2-2V3\"/><path d=\"M3 16h3a2 2 0 0 1 2 2v3\"/><path d=\"M16 21v-3a2 2 0 0 1 2-2h3\"/>",
|
|
14
|
+
thumbnails: "<path d=\"M7 2h10\"/><path d=\"M5 6h14\"/><rect width=\"18\" height=\"12\" x=\"3\" y=\"10\" rx=\"2\"/>",
|
|
15
|
+
outline: "<path d=\"M21 12h-8\"/><path d=\"M21 6h-8\"/><path d=\"M21 18h-8\"/><path d=\"M3 6v4c0 1.1.9 2 2 2h3\"/><path d=\"M3 10v6c0 1.1.9 2 2 2h3\"/>",
|
|
16
|
+
twoPages: "<path d=\"M12 7v14\"/><path d=\"M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z\"/>",
|
|
17
|
+
onePage: "<path d=\"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z\"/><path d=\"M14 2v5h6\"/>",
|
|
18
|
+
print: "<path d=\"M6 9V2h12v7\"/><path d=\"M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2\"/><rect width=\"12\" height=\"8\" x=\"6\" y=\"14\" rx=\"1\"/>",
|
|
19
|
+
download: "<path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\"/><path d=\"M7 10l5 5 5-5\"/><path d=\"M12 15V3\"/>",
|
|
20
|
+
close: "<path d=\"M18 6 6 18\"/><path d=\"m6 6 12 12\"/>",
|
|
21
|
+
soundOn: "<path d=\"M11 4.7a.7.7 0 0 0-1.2-.5L6 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h3l3.8 3.8a.7.7 0 0 0 1.2-.5z\"/><path d=\"M16 9a5 5 0 0 1 0 6\"/><path d=\"M19.4 5.6a10 10 0 0 1 0 12.8\"/>",
|
|
22
|
+
soundOff: "<path d=\"M11 4.7a.7.7 0 0 0-1.2-.5L6 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h3l3.8 3.8a.7.7 0 0 0 1.2-.5z\"/><path d=\"m16 9 5 6\"/><path d=\"m21 9-5 6\"/>"
|
|
23
|
+
};
|
|
24
|
+
function t(e, t) {
|
|
25
|
+
let n = e.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
26
|
+
return n.setAttribute("viewBox", "0 0 24 24"), n.setAttribute("fill", "none"), n.setAttribute("stroke", "currentColor"), n.setAttribute("stroke-width", "1.75"), n.setAttribute("stroke-linecap", "round"), n.setAttribute("stroke-linejoin", "round"), n.setAttribute("aria-hidden", "true"), n.setAttribute("focusable", "false"), n.innerHTML = t, n;
|
|
27
|
+
}
|
|
28
|
+
//#endregion
|
|
29
|
+
//#region src/controls/styles.ts
|
|
30
|
+
var n = "zine-controls-style", r = "\n/* Docked mode wraps the book so the bar can sit beside it without shrinking the container,\n whose measured box the renderer and hit-testing both depend on.\n The wrapper deliberately does not stretch or grow its children: the container keeps whatever\n width, aspect-ratio and auto-margins the consumer's own CSS gave it. */\n.zine-controls-wrap { display: flex; }\n/* Stacked: children keep their own width (so a percentage or auto-margin still resolves against\n the wrapper) and take only the height they ask for. */\n.zine-controls-wrap-top,\n.zine-controls-wrap-bottom { flex-direction: column; align-items: stretch; }\n/* Side by side: the bar is only as wide as its buttons, and both are vertically centred. */\n.zine-controls-wrap-left,\n.zine-controls-wrap-right { flex-direction: row; align-items: center; }\n/* min-height:0 matters as much as min-width here. A flex item's automatic minimum size floors it\n at its content, so once the canvas has grown for a taller layout the book cannot shrink back —\n its aspect-ratio is restored but ignored, leaving the extra height behind. */\n.zine-controls-wrap > * { flex: 0 0 auto; min-width: 0; min-height: 0; }\n\n.zine-controls {\n position: relative;\n display: flex;\n gap: 4px;\n padding: 5px;\n z-index: 2;\n box-sizing: border-box;\n font: 500 12px/1.2 system-ui, sans-serif;\n color: var(--zine-controls-fg, light-dark(#18181b, #f4f4f5));\n touch-action: auto;\n -webkit-user-select: none;\n user-select: none;\n}\n.zine-controls-docked { flex: 0 0 auto; justify-content: center; align-items: center; }\n.zine-controls-floating {\n position: absolute;\n /* Transparent to the book except on the buttons themselves, so gaps stay draggable. */\n pointer-events: none;\n}\n.zine-controls-bar {\n display: flex;\n align-items: center;\n gap: 1px;\n padding: 3px;\n border-radius: 8px;\n pointer-events: auto;\n background: var(--zine-controls-bg, light-dark(rgba(255, 255, 255, 0.94), rgba(24, 24, 27, 0.82)));\n backdrop-filter: blur(8px);\n}\n/* Floating: lifted off the page it covers. Docked: flat, with an outline so it reads as a\n control strip rather than a shadow hanging in empty space. */\n.zine-controls-floating .zine-controls-bar { box-shadow: 0 2px 12px rgba(0, 0, 0, 0.28); }\n.zine-controls-docked .zine-controls-bar {\n border: 1px solid var(--zine-controls-hover, light-dark(rgba(0, 0, 0, 0.08), rgba(255, 255, 255, 0.14)));\n}\n.zine-controls-floating.zine-controls-bottom { inset: auto 0 0 0; justify-content: center; }\n.zine-controls-floating.zine-controls-top { inset: 0 0 auto 0; justify-content: center; }\n.zine-controls-floating.zine-controls-left { inset: 0 auto 0 0; align-items: center; }\n.zine-controls-floating.zine-controls-right { inset: 0 0 0 auto; align-items: center; }\n/* A bar on a vertical edge stacks its buttons, docked or floating. */\n.zine-controls-left .zine-controls-bar,\n.zine-controls-right .zine-controls-bar { flex-direction: column; }\n\n.zine-controls-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 26px;\n height: 26px;\n padding: 0;\n border: 0;\n border-radius: 6px;\n background: transparent;\n color: inherit;\n cursor: pointer;\n font: inherit;\n}\n.zine-controls-btn svg { width: 15px; height: 15px; }\n.zine-controls-btn:hover:not(:disabled) { background: var(--zine-controls-hover, light-dark(rgba(0,0,0,0.08), rgba(255,255,255,0.14))); }\n.zine-controls-btn:focus-visible {\n outline: 2px solid var(--zine-controls-accent, light-dark(#0284c7, #7dd3fc));\n outline-offset: 1px;\n}\n.zine-controls-btn:disabled { opacity: 0.38; cursor: default; }\n/* A button mid-slow-action (fetching a file to save or print): a spinner, kept full-strength\n rather than dimmed so it reads as \"working\", not \"disabled\". */\n.zine-controls-btn.zine-controls-busy { opacity: 1; cursor: default; position: relative; }\n.zine-controls-btn.zine-controls-busy::after {\n content: '';\n box-sizing: border-box;\n width: 14px;\n height: 14px;\n border: 2px solid currentColor;\n border-top-color: transparent;\n border-radius: 50%;\n opacity: 0.7;\n animation: zine-controls-spin 0.7s linear infinite;\n}\n/* Icon-only bar button: hide the glyph and center the spinner over its place. */\n.zine-controls-btn.zine-controls-busy > svg { visibility: hidden; }\n.zine-controls-btn.zine-controls-busy::after {\n position: absolute;\n top: 50%;\n left: 50%;\n margin: -7px 0 0 -7px;\n}\n/* Menu item (glyph + label): let the glyph and label stand, and trail the spinner after them. */\n.zine-controls-menu .zine-controls-btn.zine-controls-busy > svg { visibility: visible; }\n.zine-controls-menu .zine-controls-btn.zine-controls-busy::after { position: static; }\n@keyframes zine-controls-spin { to { transform: rotate(360deg); } }\n@media (prefers-reduced-motion: reduce) {\n .zine-controls-btn.zine-controls-busy::after { animation-duration: 1.6s; }\n}\n/* A custom widget cannot always be disabled itself, so it is marked instead and reads the same. */\n.zine-controls-off { opacity: 0.38; }\n.zine-controls-off[aria-disabled='true'] { pointer-events: none; }\n.zine-controls-btn[aria-pressed=\"true\"],\n.zine-controls-btn[aria-expanded=\"true\"] { background: var(--zine-controls-hover, light-dark(rgba(0,0,0,0.08), rgba(255,255,255,0.14))); }\n\n.zine-controls-sep {\n width: 1px;\n align-self: stretch;\n margin: 3px 3px;\n background: currentColor;\n opacity: 0.22;\n}\n.zine-controls-left .zine-controls-sep,\n.zine-controls-right .zine-controls-sep { width: auto; height: 1px; margin: 3px 3px; }\n\n.zine-controls-page {\n display: inline-flex;\n align-items: center;\n gap: 3px;\n padding: 0 3px;\n pointer-events: auto;\n white-space: nowrap;\n}\n.zine-controls-page input {\n width: 2.2em;\n padding: 2px 3px;\n border: 1px solid var(--zine-controls-hover, light-dark(rgba(0,0,0,0.14), rgba(255,255,255,0.2)));\n border-radius: 4px;\n background: light-dark(rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.25));\n color: inherit;\n font: inherit;\n text-align: center;\n -moz-appearance: textfield;\n}\n.zine-controls-page input::-webkit-outer-spin-button,\n.zine-controls-page input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }\n.zine-controls-page input:focus-visible {\n outline: 2px solid var(--zine-controls-accent, light-dark(#0284c7, #7dd3fc));\n outline-offset: 0;\n}\n\n.zine-controls-menu {\n position: absolute;\n display: flex;\n flex-direction: column;\n gap: 1px;\n min-width: 152px;\n padding: 4px;\n border-radius: 8px;\n pointer-events: auto;\n background: var(--zine-controls-bg, light-dark(rgba(255, 255, 255, 0.94), rgba(24, 24, 27, 0.94)));\n box-shadow: 0 6px 22px rgba(0, 0, 0, 0.36);\n backdrop-filter: blur(8px);\n}\n.zine-controls-menu .zine-controls-btn {\n width: 100%;\n height: auto;\n gap: 8px;\n padding: 6px 8px;\n justify-content: flex-start;\n}\n\n/* The right-click menu opens in a fixed, viewport-spanning layer so the menu's absolute left/top\n are plain client coordinates — it lands under the cursor without the container having to be a\n positioning context. The layer itself is click-through; only the menu inside it takes events. */\n.zine-context-layer {\n position: fixed;\n inset: 0;\n z-index: 5;\n pointer-events: none;\n font: 500 12px/1.2 system-ui, sans-serif;\n color: var(--zine-controls-fg, light-dark(#18181b, #f4f4f5));\n -webkit-user-select: none;\n user-select: none;\n}\n\n/* Search shares the rail: a query field pinned at the top over a scrolling list of hits. */\n.zine-search { gap: 0; overflow: hidden; }\n.zine-search-input {\n flex: 0 0 auto;\n width: 100%;\n padding: 6px 8px;\n border: 1px solid var(--zine-controls-hover, light-dark(rgba(0,0,0,0.14), rgba(255,255,255,0.2)));\n border-radius: 6px;\n background: light-dark(rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.25));\n color: inherit;\n font: inherit;\n box-sizing: border-box;\n}\n/* Replace the browser's default (white) focus ring with the accent ring the other controls use. */\n.zine-search-input:focus-visible {\n outline: none;\n border-color: var(--zine-controls-accent, light-dark(#0284c7, #7dd3fc));\n box-shadow: 0 0 0 1px var(--zine-controls-accent, light-dark(#0284c7, #7dd3fc));\n}\n.zine-search-hits {\n flex: 1 1 auto;\n overflow-y: auto;\n /* Chain overscroll up to the page: once the list is at its end (or too short to scroll at all),\n a further wheel scrolls the page behind, the same as scrolling over the book itself does. */\n overscroll-behavior: auto;\n margin-top: 5px;\n scrollbar-width: thin;\n}\n.zine-search-hit {\n display: block;\n width: 100%;\n padding: 6px 8px;\n border: 0;\n border-radius: 5px;\n background: transparent;\n color: inherit;\n font: inherit;\n text-align: left;\n cursor: pointer;\n box-sizing: border-box;\n}\n.zine-search-hit:hover { background: var(--zine-controls-hover, light-dark(rgba(0,0,0,0.08), rgba(255,255,255,0.14))); }\n.zine-search-hit:focus-visible {\n outline: 2px solid var(--zine-controls-accent, light-dark(#0284c7, #7dd3fc));\n outline-offset: -2px;\n}\n.zine-search-page { display: block; font-size: 0.9em; opacity: 0.85; }\n.zine-search-hit small {\n display: block;\n margin-top: 2px;\n opacity: 0.62;\n line-height: 1.35;\n overflow-wrap: anywhere;\n}\n\n/* Page-turn arrows flanking the book. Like the docked toolbar and the side panels, they sit\n outside the container so they never shrink the book or intercept its gestures. */\n.zine-arrows-wrap {\n display: flex;\n align-items: center;\n gap: 4px;\n min-width: 0;\n}\n.zine-arrows-wrap > *:not(.zine-arrow) { flex: 1 1 auto; min-width: 0; min-height: 0; }\n.zine-arrow {\n flex: 0 0 auto;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 44px;\n height: 44px;\n padding: 0;\n border: 0;\n background: none;\n /* Light on a light page, dark on a dark one. The shadow below carries the contrast. */\n color: var(--zine-controls-fg, light-dark(#f4f4f5, #18181b));\n cursor: pointer;\n -webkit-user-select: none;\n user-select: none;\n /* No grey tap box on touch; the :active tint below is the press feedback instead. */\n -webkit-tap-highlight-color: transparent;\n}\n.zine-arrow svg {\n width: 36px;\n height: 36px;\n stroke-width: 2.25;\n /* No backing disc, so a subtle offset shadow keeps the chevron from disappearing into a\n matching background. It contrasts the fill: a light chevron (light mode) casts a soft dark\n shadow, a dark chevron (dark mode) a soft light one, offset rather than a glowing halo. */\n filter: drop-shadow(0 1px 2px light-dark(rgba(0, 0, 0, 0.5), rgba(255, 255, 255, 0.35)));\n}\n/* Feedback intensifies the chevron toward its extreme (white in light mode, black in dark)\n rather than tinting it, so no new colour is introduced. Not tied to --zine-controls-fg: that\n sets the resting fill, and hover has to differ from it to read as feedback. :active clears on\n release, so tap and click feel the same; :hover is behind (hover: hover) so it never sticks on\n touch. */\n.zine-arrow:active:not(:disabled) { color: light-dark(#ffffff, #000000); }\n@media (hover: hover) {\n .zine-arrow:hover:not(:disabled) { color: light-dark(#ffffff, #000000); }\n}\n.zine-arrow:focus-visible {\n outline: 2px solid var(--zine-controls-accent, light-dark(#0284c7, #7dd3fc));\n outline-offset: 2px;\n border-radius: 8px;\n}\n/* Nothing to turn to: invisible, but still occupying its slot so the book does not slide across\n as the reader reaches a cover. */\n.zine-arrow-hidden { visibility: hidden; }\n/* Device-scoped arrows (controls.arrows: 'desktop' | 'mobile'). The split follows the same 640px\n breakpoint as the flank/overlay layouts below, so the arrows appear and disappear live as the\n window crosses it. */\n@media (max-width: 640px) {\n .zine-arrows-desktop > .zine-arrow { display: none; }\n}\n@media (min-width: 641px) {\n .zine-arrows-mobile > .zine-arrow { display: none; }\n}\n/* Too narrow to flank without squeezing the pages, so overlay the arrows on the book's edges\n instead. The wrap sits outside the container, so painting them over it leaves the container's\n measured box (which sizes the book and maps taps) untouched. */\n@media (max-width: 640px) {\n .zine-arrows-wrap { position: relative; }\n .zine-arrow {\n position: absolute;\n top: 50%;\n transform: translateY(-50%);\n z-index: 2;\n /* Sit back over the page so the reader can look at the book. */\n opacity: 0.7;\n }\n .zine-arrow:first-child { left: -2px; }\n .zine-arrow:last-child { right: -2px; }\n}\n@media (prefers-reduced-motion: no-preference) {\n .zine-arrow { transition: background 120ms ease; }\n}\n\n/* Side panels (thumbnails, outline, search). The rail wraps the book and its docked toolbar so it\n sits beside them as a flex sibling. On a wide screen the book shrinks to make room (below); on a\n narrow one the rail becomes a drawer over the book.\n align-items:flex-start, not stretch: the book sizes its height from its width through its own\n aspect-ratio, and that height is 'auto' from flex's point of view, so a stretch would override it\n and blow the book (and the rail matched to it) up to the flex line's cross size. Left as auto, the\n book keeps its aspect height and the holder alone stretches down to meet it (below). */\n.zine-panel-wrap { display: flex; align-items: flex-start; gap: 8px; position: relative; }\n\n/* The book-side slot (the bare container, or the docked-toolbar wrapper around it) yields space to\n the fixed-width rail instead of overflowing: flex-shrink lets it fall below its own width, and\n min-width:0 removes the automatic content floor that would otherwise stop it. The book only\n actually shrinks if its width is elastic (a %, max-width, or the demo's min(...) with a % term);\n a hard-coded pixel width has nothing for the % to resolve smaller against. */\n.zine-panel-wrap > *:not(.zine-panel-holder):not(.zine-panel-scrim) {\n flex: 0 1 auto;\n min-width: 0;\n}\n/* With a docked toolbar the book lives inside .zine-controls-wrap, whose children are pinned\n flex:0 0 auto (so the bar keeps its size). Inside a panel wrap the book child — everything but\n the bar itself — must instead be allowed to shrink, so left/right docking flanks like the rest.\n Top/bottom docking already shrinks through the container's own % width. */\n.zine-panel-wrap .zine-controls-wrap > *:not(.zine-controls) {\n flex: 0 1 auto;\n min-width: 0;\n min-height: 0;\n}\n@media (prefers-reduced-motion: no-preference) {\n .zine-panel-wrap > *:not(.zine-panel-holder):not(.zine-panel-scrim) {\n transition: flex-basis 160ms ease, width 160ms ease;\n }\n}\n\n/* The holder stretches to the book's height; the rail is absolutely positioned inside it so a\n long list scrolls rather than growing the row and running past the bottom of the book. */\n.zine-panel-holder {\n position: relative;\n flex: 0 0 auto;\n align-self: stretch;\n min-height: 0;\n}\n/* The scrim dims the book behind the narrow-screen drawer. It exists on every screen but only\n shows under the breakpoint below, so on a wide screen it takes no flex slot and never intercepts\n a click. */\n.zine-panel-scrim { display: none; }\n.zine-panel {\n position: absolute;\n inset: 0;\n display: flex;\n flex-direction: column;\n gap: 6px;\n overflow-y: auto;\n /* Chain overscroll up to the page: once the list is at its end (or too short to scroll at all),\n a further wheel scrolls the page behind, the same as scrolling over the book itself does. */\n overscroll-behavior: auto;\n /* Reserve the scrollbar's lane whether or not it is showing, so rows never sit under the bar\n and the close button (inset past this lane below) has a fixed edge to clear. */\n scrollbar-gutter: stable;\n padding: 6px;\n box-sizing: border-box;\n border-radius: 8px;\n background: var(--zine-controls-bg, light-dark(rgba(255, 255, 255, 0.94), rgba(24, 24, 27, 0.82)));\n color: var(--zine-controls-fg, light-dark(#18181b, #f4f4f5));\n font: 500 11px/1.2 system-ui, sans-serif;\n scrollbar-width: thin;\n touch-action: auto;\n -webkit-user-select: none;\n user-select: none;\n}\n/* The rail sits flush against the book's reading-start edge (left, or right in RTL), so its outer\n corners there would round away from that edge and leave a gap. Square them; keep the interior\n corners, which face the page, rounded. */\n.zine-panel { border-radius: 0 8px 8px 0; }\n.zine-panel-wrap-rtl .zine-panel { border-radius: 8px 0 0 8px; }\n/* Overlay fallback, used when the container has no parent to wrap. */\n.zine-panel-overlay { position: absolute; inset: 0 auto 0 0; z-index: 3; }\n.zine-panel-active { background: var(--zine-controls-hover, light-dark(rgba(0,0,0,0.08), rgba(255,255,255,0.18))); }\n.zine-panel-note { padding: 8px; opacity: 0.66; line-height: 1.4; }\n/* Discoverable dismiss, floated in the rail's trailing-top (interior) corner, away from the book.\n It sits in the holder above the scrolling list so the list scrolls under it. A translucent\n backdrop keeps the glyph legible over a thumbnail or a line of text. */\n.zine-panel-close {\n position: absolute;\n top: 6px;\n /* Inset past the reserved scrollbar gutter so the button never overlaps the bar. */\n right: 12px;\n z-index: 1;\n display: inline-flex;\n width: 24px;\n height: 24px;\n align-items: center;\n justify-content: center;\n padding: 0;\n border: 0;\n border-radius: 6px;\n /* Opaque so a light thumbnail underneath never shows through and dims the glyph. */\n background: light-dark(#f4f4f5, #27272a);\n color: inherit;\n cursor: pointer;\n}\n.zine-panel-close svg { width: 15px; height: 15px; }\n/* Opaque hover, not the translucent hover token: a see-through backdrop over a light thumbnail\n would wash the glyph out. Solid neutrals keep the X readable in both themes over anything. */\n.zine-panel-close:hover { background: light-dark(#e4e4e7, #3f3f46); }\n.zine-panel-close:focus-visible {\n outline: 2px solid var(--zine-controls-accent, light-dark(#0284c7, #7dd3fc));\n outline-offset: -2px;\n}\n.zine-panel-wrap-rtl .zine-panel-close { right: auto; left: 12px; }\n/* End the search field just before the close button rather than running under it, so the two sit\n side by side and the field's border reads cleanly. The button's inner edge is ~30px from the\n panel's content edge (24px wide, its right edge 6px in once the reserved scrollbar gutter is\n accounted for), so 30px leaves them flush with a hair of breathing room. width:auto lets the\n column's stretch fill the rest, so the margin shortens the field instead of overflowing 100%. */\n.zine-search .zine-search-input { width: auto; align-self: stretch; margin-right: 30px; }\n.zine-panel-wrap-rtl .zine-search .zine-search-input { margin-right: 0; margin-left: 30px; }\n\n/* Overlay panels (outline, search) on a wide screen: float over the book's start edge instead of\n flanking it, so the book never resizes. The wrap drops to a block, laying the book out exactly as\n it was before the panel opened; the holder sits absolute over it at full book height, with a\n shadow so it reads as lifted off the page. thumbnails is not overlaid — it stays a flex flank and\n shrinks the book only when the two will not otherwise both fit. */\n@media (min-width: 641px) {\n .zine-panel-wrap-overlay { display: block; }\n .zine-panel-wrap-overlay .zine-panel-holder {\n position: absolute;\n inset: 0 auto 0 0;\n z-index: 4;\n box-shadow: 0 0 24px light-dark(rgba(0, 0, 0, 0.22), rgba(0, 0, 0, 0.55));\n }\n .zine-panel-wrap-overlay.zine-panel-wrap-rtl .zine-panel-holder { inset: 0 0 0 auto; }\n /* Opaque over the page it floats on, unlike a flanking rail that sits against empty margin. */\n .zine-panel-wrap-overlay .zine-panel {\n background: var(--zine-controls-bg, light-dark(#ffffff, #18181b));\n }\n /* A click-away layer over the book, so pressing the page behind the floating rail closes it the\n way tapping the scrim does on a narrow screen. Transparent here — the wide-screen rail only\n covers an edge, so there is no need to dim the page the reader is still looking at. It sits\n under the holder (z-index) so the rail's own rows still take their clicks. */\n .zine-panel-wrap-overlay .zine-panel-scrim {\n display: block;\n position: absolute;\n inset: 0;\n z-index: 3;\n background: transparent;\n }\n}\n@media (min-width: 641px) and (prefers-reduced-motion: no-preference) {\n .zine-panel-wrap-overlay .zine-panel-holder { animation: zine-drawer-in 180ms ease; }\n .zine-panel-wrap-overlay.zine-panel-wrap-rtl .zine-panel-holder { animation-name: zine-drawer-in-rtl; }\n}\n\n/* Narrow screen: no room to flank, so the rail becomes a drawer over the book and the scrim dims\n the page behind it. The wrap is position:relative, so the absolute holder and scrim below are\n measured against it — i.e. against the book's own box. */\n@media (max-width: 640px) {\n /* No flanking here — the rail and scrim both overlay absolutely — so drop the flex row entirely.\n As a plain block wrap the book keeps the exact box it had before a panel opened (its own width,\n margin and aspect-ratio height); a flex row would instead re-resolve the book's width and, with\n it, its aspect-derived height, enlarging the canvas and pushing the page down. */\n .zine-panel-wrap {\n display: block;\n }\n .zine-panel-holder {\n position: absolute;\n inset: 0 auto 0 0;\n z-index: 4;\n /* Cap the drawer so it never eats the whole book; its inline width still applies under this. */\n max-width: 80%;\n box-shadow: 0 0 24px light-dark(rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.6));\n }\n /* RTL reads from the right, so the drawer enters from there. */\n .zine-panel-wrap-rtl .zine-panel-holder { inset: 0 0 0 auto; }\n /* Opaque over the page, unlike the wide-screen rail which can sit against empty margin. */\n .zine-panel {\n background: var(--zine-controls-bg, light-dark(#ffffff, #18181b));\n }\n .zine-panel-scrim {\n display: block;\n position: absolute;\n inset: 0;\n z-index: 3;\n background: rgba(0, 0, 0, 0.4);\n }\n}\n@media (max-width: 640px) and (prefers-reduced-motion: no-preference) {\n .zine-panel-holder { animation: zine-drawer-in 180ms ease; }\n .zine-panel-wrap-rtl .zine-panel-holder { animation-name: zine-drawer-in-rtl; }\n .zine-panel-scrim { animation: zine-scrim-in 180ms ease; }\n}\n@keyframes zine-drawer-in { from { transform: translateX(-100%); } to { transform: translateX(0); } }\n@keyframes zine-drawer-in-rtl { from { transform: translateX(100%); } to { transform: translateX(0); } }\n@keyframes zine-scrim-in { from { opacity: 0; } to { opacity: 1; } }\n\n.zine-thumbs { align-items: center; }\n\n.zine-thumbs-row {\n display: flex;\n flex-wrap: wrap;\n justify-content: center;\n gap: 2px;\n width: 100%;\n padding: 4px;\n border: 0;\n border-radius: 6px;\n background: transparent;\n color: inherit;\n font: inherit;\n cursor: pointer;\n box-sizing: border-box;\n}\n.zine-thumbs-row:hover { background: var(--zine-controls-hover, light-dark(rgba(0,0,0,0.08), rgba(255,255,255,0.14))); }\n.zine-thumbs-row:focus-visible {\n outline: 2px solid var(--zine-controls-accent, light-dark(#0284c7, #7dd3fc));\n outline-offset: -2px;\n}\n.zine-thumbs-row.zine-panel-active .zine-thumbs-cell {\n outline: 1px solid var(--zine-controls-accent, light-dark(#0284c7, #7dd3fc));\n}\n\n/* Outline: a single column of headings, indented by depth. */\n/* No gap: a hairline between rows is the separator instead, so a title that wraps to two lines\n still reads as one entry rather than blending into its neighbours. */\n.zine-outline { gap: 0; }\n.zine-outline-row {\n display: block;\n width: 100%;\n padding: 6px 8px;\n border: 0;\n border-bottom: 1px solid var(--zine-controls-divider, light-dark(rgba(0, 0, 0, 0.08), rgba(255, 255, 255, 0.1)));\n border-radius: 5px;\n background: transparent;\n color: inherit;\n font: inherit;\n text-align: left;\n line-height: 1.35;\n cursor: pointer;\n box-sizing: border-box;\n overflow-wrap: anywhere;\n}\n.zine-outline-row:last-child { border-bottom: 0; }\n.zine-outline-row:hover:not(:disabled) {\n background: var(--zine-controls-hover, light-dark(rgba(0,0,0,0.08), rgba(255,255,255,0.14)));\n}\n.zine-outline-row:focus-visible {\n outline: 2px solid var(--zine-controls-accent, light-dark(#0284c7, #7dd3fc));\n outline-offset: -2px;\n}\n/* A heading whose destination could not be resolved: shown, but nothing to click through to. */\n.zine-outline-row:disabled { opacity: 0.5; cursor: default; }\n\n.zine-thumbs-cell {\n flex: 1 1 0;\n min-width: 0;\n min-height: 24px;\n display: flex;\n background: light-dark(rgba(0, 0, 0, 0.08), rgba(0, 0, 0, 0.3));\n border-radius: 2px;\n overflow: hidden;\n}\n/* A lone page in a book that pairs elsewhere (a cover, a back page) keeps one page's width and\n centres, instead of stretching across both columns. */\n.zine-thumbs-row-lone .zine-thumbs-cell { flex: 0 0 calc(50% - 1px); }\n.zine-thumbs-img { display: block; width: 100%; height: auto; }\n.zine-thumbs-caption { flex: 1 0 100%; text-align: center; opacity: 0.7; }\n\n/* Share dialog. Centred over the page rather than in the side rail: a QR and six destinations\n need more room than the rail gives, and sharing is a brief interruption, not a browsing mode. */\n.zine-share-backdrop {\n position: fixed;\n inset: 0;\n z-index: 2147483000;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 16px;\n background: rgba(0, 0, 0, 0.55);\n font: 500 13px/1.4 system-ui, sans-serif;\n color: var(--zine-controls-fg, light-dark(#18181b, #f4f4f5));\n}\n.zine-share {\n width: min(320px, 100%);\n max-height: 100%;\n overflow-y: auto;\n display: flex;\n flex-direction: column;\n gap: 12px;\n padding: 14px;\n border-radius: 12px;\n background: var(--zine-share-bg, light-dark(#ffffff, #1c1c20));\n box-shadow: 0 18px 50px rgba(0, 0, 0, 0.45);\n box-sizing: border-box;\n}\n.zine-share-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }\n.zine-share-head strong { font-size: 15px; }\n.zine-share-close {\n display: inline-flex;\n width: 26px;\n height: 26px;\n align-items: center;\n justify-content: center;\n padding: 0;\n border: 0;\n border-radius: 6px;\n background: transparent;\n color: inherit;\n cursor: pointer;\n}\n.zine-share-close svg { width: 16px; height: 16px; }\n.zine-share-close:hover { background: var(--zine-controls-hover, light-dark(rgba(0,0,0,0.08), rgba(255,255,255,0.14))); }\n\n.zine-share-qr {\n align-self: center;\n line-height: 0;\n padding: 8px;\n border-radius: 8px;\n background: #fff;\n}\n.zine-share-qr svg { display: block; }\n\n.zine-share-link { display: flex; gap: 6px; }\n.zine-share-link input {\n flex: 1 1 auto;\n min-width: 0;\n padding: 7px 9px;\n border: 1px solid var(--zine-controls-hover, light-dark(rgba(0,0,0,0.14), rgba(255,255,255,0.2)));\n border-radius: 7px;\n background: light-dark(rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.3));\n color: inherit;\n font: inherit;\n}\n.zine-share-copy {\n flex: 0 0 auto;\n min-width: 74px;\n padding: 7px 12px;\n border: 0;\n border-radius: 7px;\n background: var(--zine-controls-accent, light-dark(#0284c7, #7dd3fc));\n color: light-dark(#ffffff, #06202c);\n font: inherit;\n font-weight: 600;\n cursor: pointer;\n}\n\n.zine-share-socials { display: flex; flex-wrap: wrap; gap: 6px; justify-content: center; }\n.zine-share-social {\n display: inline-flex;\n width: 38px;\n height: 38px;\n align-items: center;\n justify-content: center;\n border-radius: 8px;\n background: light-dark(rgba(0, 0, 0, 0.06), rgba(255, 255, 255, 0.08));\n color: inherit;\n text-decoration: none;\n}\n.zine-share-social svg { width: 19px; height: 19px; }\n.zine-share-social:hover { background: var(--zine-controls-hover, light-dark(rgba(0,0,0,0.12), rgba(255,255,255,0.18))); }\n.zine-share-close:focus-visible,\n.zine-share-copy:focus-visible,\n.zine-share-social:focus-visible,\n.zine-share-link input:focus-visible {\n outline: 2px solid var(--zine-controls-accent, light-dark(#0284c7, #7dd3fc));\n outline-offset: 2px;\n}\n\n@media (prefers-reduced-motion: no-preference) {\n .zine-controls-btn { transition: background 120ms ease; }\n}\n";
|
|
31
|
+
function i(e) {
|
|
32
|
+
if (e.getElementById(n)) return;
|
|
33
|
+
let t = e.createElement("style");
|
|
34
|
+
t.id = n, t.textContent = r, (e.head ?? e.documentElement)?.appendChild(t);
|
|
35
|
+
}
|
|
36
|
+
function a(e, t) {
|
|
37
|
+
t !== "auto" && (e.style.colorScheme = t);
|
|
38
|
+
}
|
|
39
|
+
//#endregion
|
|
40
|
+
export { t as i, i as n, e as r, a as t };
|
|
41
|
+
|
|
42
|
+
//# sourceMappingURL=styles-C1Qp5pby.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"styles-C1Qp5pby.js","names":[],"sources":["../src/controls/icons.ts","../src/controls/styles.ts"],"sourcesContent":["/**\n * Toolbar glyphs, inlined so the bundle carries no icon-font or sprite dependency.\n *\n * Paths are from Lucide (https://lucide.dev), ISC licensed:\n * Copyright (c) for portions of Lucide are held by Cole Bemis 2013-2022 as part of Feather\n * (MIT). All other copyright (c) for Lucide are held by Lucide Contributors 2022.\n *\n * Each value is the inner markup of a 24x24 `viewBox` drawn with `stroke=\"currentColor\"`,\n * `fill=\"none\"` and round caps/joins — {@link createIcon} supplies those, so the strings stay to\n * the geometry.\n */\n// Not annotated `Record<string, string>`: that would make every ICONS.foo possibly-undefined\n// under noUncheckedIndexedAccess, even for names written right here. `satisfies` keeps the\n// literal's exact keys while still checking the values are markup strings.\nexport const ICONS = {\n /** chevron-left */\n prev: '<path d=\"m15 18-6-6 6-6\"/>',\n /** chevron-right */\n next: '<path d=\"m9 18 6-6-6-6\"/>',\n /** chevrons-left */\n first: '<path d=\"m11 17-5-5 5-5\"/><path d=\"m18 17-5-5 5-5\"/>',\n /** chevrons-right */\n last: '<path d=\"m6 17 5-5-5-5\"/><path d=\"m13 17 5-5-5-5\"/>',\n /** circle-plus: zoom in. A plus, not a magnifier, so it never reads as a third \"search\". */\n zoomIn: '<circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M8 12h8\"/><path d=\"M12 8v8\"/>',\n /** circle-minus: zoom out, paired with zoomIn. */\n zoomOut: '<circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M8 12h8\"/>',\n /** search */\n search: '<circle cx=\"11\" cy=\"11\" r=\"8\"/><path d=\"m21 21-4.3-4.3\"/>',\n /** share-2 */\n share:\n '<circle cx=\"18\" cy=\"5\" r=\"3\"/><circle cx=\"6\" cy=\"12\" r=\"3\"/><circle cx=\"18\" cy=\"19\" r=\"3\"/><path d=\"m8.6 13.5 6.8 4\"/><path d=\"m15.4 6.5-6.8 4\"/>',\n /** ellipsis-vertical */\n menu: '<circle cx=\"12\" cy=\"12\" r=\"1\"/><circle cx=\"12\" cy=\"5\" r=\"1\"/><circle cx=\"12\" cy=\"19\" r=\"1\"/>',\n /** maximize */\n fullscreen:\n '<path d=\"M8 3H5a2 2 0 0 0-2 2v3\"/><path d=\"M21 8V5a2 2 0 0 0-2-2h-3\"/><path d=\"M3 16v3a2 2 0 0 0 2 2h3\"/><path d=\"M16 21h3a2 2 0 0 0 2-2v-3\"/>',\n /** minimize */\n exitFullscreen:\n '<path d=\"M8 3v3a2 2 0 0 1-2 2H3\"/><path d=\"M21 8h-3a2 2 0 0 1-2-2V3\"/><path d=\"M3 16h3a2 2 0 0 1 2 2v3\"/><path d=\"M16 21v-3a2 2 0 0 1 2-2h3\"/>',\n /** gallery-vertical-end */\n thumbnails:\n '<path d=\"M7 2h10\"/><path d=\"M5 6h14\"/><rect width=\"18\" height=\"12\" x=\"3\" y=\"10\" rx=\"2\"/>',\n /** list-tree */\n outline:\n '<path d=\"M21 12h-8\"/><path d=\"M21 6h-8\"/><path d=\"M21 18h-8\"/><path d=\"M3 6v4c0 1.1.9 2 2 2h3\"/><path d=\"M3 10v6c0 1.1.9 2 2 2h3\"/>',\n /** book-open: two facing pages */\n twoPages:\n '<path d=\"M12 7v14\"/><path d=\"M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z\"/>',\n /** file: a single page */\n onePage: '<path d=\"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z\"/><path d=\"M14 2v5h6\"/>',\n /** printer */\n print:\n '<path d=\"M6 9V2h12v7\"/><path d=\"M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2\"/><rect width=\"12\" height=\"8\" x=\"6\" y=\"14\" rx=\"1\"/>',\n /** download */\n download: '<path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\"/><path d=\"M7 10l5 5 5-5\"/><path d=\"M12 15V3\"/>',\n /** x */\n close: '<path d=\"M18 6 6 18\"/><path d=\"m6 6 12 12\"/>',\n /** volume-2: sound on */\n soundOn:\n '<path d=\"M11 4.7a.7.7 0 0 0-1.2-.5L6 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h3l3.8 3.8a.7.7 0 0 0 1.2-.5z\"/><path d=\"M16 9a5 5 0 0 1 0 6\"/><path d=\"M19.4 5.6a10 10 0 0 1 0 12.8\"/>',\n /** volume-x: muted */\n soundOff:\n '<path d=\"M11 4.7a.7.7 0 0 0-1.2-.5L6 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h3l3.8 3.8a.7.7 0 0 0 1.2-.5z\"/><path d=\"m16 9 5 6\"/><path d=\"m21 9-5 6\"/>',\n} satisfies Record<string, string>;\n\n/** Build an `<svg>` for one of the {@link ICONS}, or null if the name is unknown. */\nexport function createIcon(doc: Document, markup: string): SVGSVGElement {\n const svg = doc.createElementNS('http://www.w3.org/2000/svg', 'svg');\n svg.setAttribute('viewBox', '0 0 24 24');\n svg.setAttribute('fill', 'none');\n svg.setAttribute('stroke', 'currentColor');\n // Slightly under Lucide's default 2: at the toolbar's icon size a full-weight stroke reads\n // heavy and closes up the tighter shapes.\n svg.setAttribute('stroke-width', '1.75');\n svg.setAttribute('stroke-linecap', 'round');\n svg.setAttribute('stroke-linejoin', 'round');\n svg.setAttribute('aria-hidden', 'true');\n svg.setAttribute('focusable', 'false');\n // Icon markup is library-authored, never consumer input.\n svg.innerHTML = markup;\n return svg;\n}\n","/**\n * The toolbar's stylesheet, injected once per document.\n *\n * The rest of the library styles elements inline, but a toolbar needs `:hover`, `:focus-visible`\n * and `:disabled` — states inline styles cannot express — so this is the one place a `<style>`\n * element is warranted. Everything is scoped under `.zine-controls`.\n *\n * Colours come from custom properties so a consumer can retheme without overriding rules. Left\n * unset, each falls back to `light-dark(light, dark)`, which resolves against the element's used\n * `color-scheme`: the toolbar follows the host page's scheme with no configuration, and a host\n * with a theme toggle only has to set `color-scheme` on its root for the controls to match. The\n * `colorScheme` control option can force `light` or `dark` regardless of the page, and a set\n * `--zine-controls-*` token still wins over both.\n */\nconst STYLE_ID = 'zine-controls-style';\n\nexport const CSS = `\n/* Docked mode wraps the book so the bar can sit beside it without shrinking the container,\n whose measured box the renderer and hit-testing both depend on.\n The wrapper deliberately does not stretch or grow its children: the container keeps whatever\n width, aspect-ratio and auto-margins the consumer's own CSS gave it. */\n.zine-controls-wrap { display: flex; }\n/* Stacked: children keep their own width (so a percentage or auto-margin still resolves against\n the wrapper) and take only the height they ask for. */\n.zine-controls-wrap-top,\n.zine-controls-wrap-bottom { flex-direction: column; align-items: stretch; }\n/* Side by side: the bar is only as wide as its buttons, and both are vertically centred. */\n.zine-controls-wrap-left,\n.zine-controls-wrap-right { flex-direction: row; align-items: center; }\n/* min-height:0 matters as much as min-width here. A flex item's automatic minimum size floors it\n at its content, so once the canvas has grown for a taller layout the book cannot shrink back —\n its aspect-ratio is restored but ignored, leaving the extra height behind. */\n.zine-controls-wrap > * { flex: 0 0 auto; min-width: 0; min-height: 0; }\n\n.zine-controls {\n position: relative;\n display: flex;\n gap: 4px;\n padding: 5px;\n z-index: 2;\n box-sizing: border-box;\n font: 500 12px/1.2 system-ui, sans-serif;\n color: var(--zine-controls-fg, light-dark(#18181b, #f4f4f5));\n touch-action: auto;\n -webkit-user-select: none;\n user-select: none;\n}\n.zine-controls-docked { flex: 0 0 auto; justify-content: center; align-items: center; }\n.zine-controls-floating {\n position: absolute;\n /* Transparent to the book except on the buttons themselves, so gaps stay draggable. */\n pointer-events: none;\n}\n.zine-controls-bar {\n display: flex;\n align-items: center;\n gap: 1px;\n padding: 3px;\n border-radius: 8px;\n pointer-events: auto;\n background: var(--zine-controls-bg, light-dark(rgba(255, 255, 255, 0.94), rgba(24, 24, 27, 0.82)));\n backdrop-filter: blur(8px);\n}\n/* Floating: lifted off the page it covers. Docked: flat, with an outline so it reads as a\n control strip rather than a shadow hanging in empty space. */\n.zine-controls-floating .zine-controls-bar { box-shadow: 0 2px 12px rgba(0, 0, 0, 0.28); }\n.zine-controls-docked .zine-controls-bar {\n border: 1px solid var(--zine-controls-hover, light-dark(rgba(0, 0, 0, 0.08), rgba(255, 255, 255, 0.14)));\n}\n.zine-controls-floating.zine-controls-bottom { inset: auto 0 0 0; justify-content: center; }\n.zine-controls-floating.zine-controls-top { inset: 0 0 auto 0; justify-content: center; }\n.zine-controls-floating.zine-controls-left { inset: 0 auto 0 0; align-items: center; }\n.zine-controls-floating.zine-controls-right { inset: 0 0 0 auto; align-items: center; }\n/* A bar on a vertical edge stacks its buttons, docked or floating. */\n.zine-controls-left .zine-controls-bar,\n.zine-controls-right .zine-controls-bar { flex-direction: column; }\n\n.zine-controls-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 26px;\n height: 26px;\n padding: 0;\n border: 0;\n border-radius: 6px;\n background: transparent;\n color: inherit;\n cursor: pointer;\n font: inherit;\n}\n.zine-controls-btn svg { width: 15px; height: 15px; }\n.zine-controls-btn:hover:not(:disabled) { background: var(--zine-controls-hover, light-dark(rgba(0,0,0,0.08), rgba(255,255,255,0.14))); }\n.zine-controls-btn:focus-visible {\n outline: 2px solid var(--zine-controls-accent, light-dark(#0284c7, #7dd3fc));\n outline-offset: 1px;\n}\n.zine-controls-btn:disabled { opacity: 0.38; cursor: default; }\n/* A button mid-slow-action (fetching a file to save or print): a spinner, kept full-strength\n rather than dimmed so it reads as \"working\", not \"disabled\". */\n.zine-controls-btn.zine-controls-busy { opacity: 1; cursor: default; position: relative; }\n.zine-controls-btn.zine-controls-busy::after {\n content: '';\n box-sizing: border-box;\n width: 14px;\n height: 14px;\n border: 2px solid currentColor;\n border-top-color: transparent;\n border-radius: 50%;\n opacity: 0.7;\n animation: zine-controls-spin 0.7s linear infinite;\n}\n/* Icon-only bar button: hide the glyph and center the spinner over its place. */\n.zine-controls-btn.zine-controls-busy > svg { visibility: hidden; }\n.zine-controls-btn.zine-controls-busy::after {\n position: absolute;\n top: 50%;\n left: 50%;\n margin: -7px 0 0 -7px;\n}\n/* Menu item (glyph + label): let the glyph and label stand, and trail the spinner after them. */\n.zine-controls-menu .zine-controls-btn.zine-controls-busy > svg { visibility: visible; }\n.zine-controls-menu .zine-controls-btn.zine-controls-busy::after { position: static; }\n@keyframes zine-controls-spin { to { transform: rotate(360deg); } }\n@media (prefers-reduced-motion: reduce) {\n .zine-controls-btn.zine-controls-busy::after { animation-duration: 1.6s; }\n}\n/* A custom widget cannot always be disabled itself, so it is marked instead and reads the same. */\n.zine-controls-off { opacity: 0.38; }\n.zine-controls-off[aria-disabled='true'] { pointer-events: none; }\n.zine-controls-btn[aria-pressed=\"true\"],\n.zine-controls-btn[aria-expanded=\"true\"] { background: var(--zine-controls-hover, light-dark(rgba(0,0,0,0.08), rgba(255,255,255,0.14))); }\n\n.zine-controls-sep {\n width: 1px;\n align-self: stretch;\n margin: 3px 3px;\n background: currentColor;\n opacity: 0.22;\n}\n.zine-controls-left .zine-controls-sep,\n.zine-controls-right .zine-controls-sep { width: auto; height: 1px; margin: 3px 3px; }\n\n.zine-controls-page {\n display: inline-flex;\n align-items: center;\n gap: 3px;\n padding: 0 3px;\n pointer-events: auto;\n white-space: nowrap;\n}\n.zine-controls-page input {\n width: 2.2em;\n padding: 2px 3px;\n border: 1px solid var(--zine-controls-hover, light-dark(rgba(0,0,0,0.14), rgba(255,255,255,0.2)));\n border-radius: 4px;\n background: light-dark(rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.25));\n color: inherit;\n font: inherit;\n text-align: center;\n -moz-appearance: textfield;\n}\n.zine-controls-page input::-webkit-outer-spin-button,\n.zine-controls-page input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }\n.zine-controls-page input:focus-visible {\n outline: 2px solid var(--zine-controls-accent, light-dark(#0284c7, #7dd3fc));\n outline-offset: 0;\n}\n\n.zine-controls-menu {\n position: absolute;\n display: flex;\n flex-direction: column;\n gap: 1px;\n min-width: 152px;\n padding: 4px;\n border-radius: 8px;\n pointer-events: auto;\n background: var(--zine-controls-bg, light-dark(rgba(255, 255, 255, 0.94), rgba(24, 24, 27, 0.94)));\n box-shadow: 0 6px 22px rgba(0, 0, 0, 0.36);\n backdrop-filter: blur(8px);\n}\n.zine-controls-menu .zine-controls-btn {\n width: 100%;\n height: auto;\n gap: 8px;\n padding: 6px 8px;\n justify-content: flex-start;\n}\n\n/* The right-click menu opens in a fixed, viewport-spanning layer so the menu's absolute left/top\n are plain client coordinates — it lands under the cursor without the container having to be a\n positioning context. The layer itself is click-through; only the menu inside it takes events. */\n.zine-context-layer {\n position: fixed;\n inset: 0;\n z-index: 5;\n pointer-events: none;\n font: 500 12px/1.2 system-ui, sans-serif;\n color: var(--zine-controls-fg, light-dark(#18181b, #f4f4f5));\n -webkit-user-select: none;\n user-select: none;\n}\n\n/* Search shares the rail: a query field pinned at the top over a scrolling list of hits. */\n.zine-search { gap: 0; overflow: hidden; }\n.zine-search-input {\n flex: 0 0 auto;\n width: 100%;\n padding: 6px 8px;\n border: 1px solid var(--zine-controls-hover, light-dark(rgba(0,0,0,0.14), rgba(255,255,255,0.2)));\n border-radius: 6px;\n background: light-dark(rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.25));\n color: inherit;\n font: inherit;\n box-sizing: border-box;\n}\n/* Replace the browser's default (white) focus ring with the accent ring the other controls use. */\n.zine-search-input:focus-visible {\n outline: none;\n border-color: var(--zine-controls-accent, light-dark(#0284c7, #7dd3fc));\n box-shadow: 0 0 0 1px var(--zine-controls-accent, light-dark(#0284c7, #7dd3fc));\n}\n.zine-search-hits {\n flex: 1 1 auto;\n overflow-y: auto;\n /* Chain overscroll up to the page: once the list is at its end (or too short to scroll at all),\n a further wheel scrolls the page behind, the same as scrolling over the book itself does. */\n overscroll-behavior: auto;\n margin-top: 5px;\n scrollbar-width: thin;\n}\n.zine-search-hit {\n display: block;\n width: 100%;\n padding: 6px 8px;\n border: 0;\n border-radius: 5px;\n background: transparent;\n color: inherit;\n font: inherit;\n text-align: left;\n cursor: pointer;\n box-sizing: border-box;\n}\n.zine-search-hit:hover { background: var(--zine-controls-hover, light-dark(rgba(0,0,0,0.08), rgba(255,255,255,0.14))); }\n.zine-search-hit:focus-visible {\n outline: 2px solid var(--zine-controls-accent, light-dark(#0284c7, #7dd3fc));\n outline-offset: -2px;\n}\n.zine-search-page { display: block; font-size: 0.9em; opacity: 0.85; }\n.zine-search-hit small {\n display: block;\n margin-top: 2px;\n opacity: 0.62;\n line-height: 1.35;\n overflow-wrap: anywhere;\n}\n\n/* Page-turn arrows flanking the book. Like the docked toolbar and the side panels, they sit\n outside the container so they never shrink the book or intercept its gestures. */\n.zine-arrows-wrap {\n display: flex;\n align-items: center;\n gap: 4px;\n min-width: 0;\n}\n.zine-arrows-wrap > *:not(.zine-arrow) { flex: 1 1 auto; min-width: 0; min-height: 0; }\n.zine-arrow {\n flex: 0 0 auto;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 44px;\n height: 44px;\n padding: 0;\n border: 0;\n background: none;\n /* Light on a light page, dark on a dark one. The shadow below carries the contrast. */\n color: var(--zine-controls-fg, light-dark(#f4f4f5, #18181b));\n cursor: pointer;\n -webkit-user-select: none;\n user-select: none;\n /* No grey tap box on touch; the :active tint below is the press feedback instead. */\n -webkit-tap-highlight-color: transparent;\n}\n.zine-arrow svg {\n width: 36px;\n height: 36px;\n stroke-width: 2.25;\n /* No backing disc, so a subtle offset shadow keeps the chevron from disappearing into a\n matching background. It contrasts the fill: a light chevron (light mode) casts a soft dark\n shadow, a dark chevron (dark mode) a soft light one, offset rather than a glowing halo. */\n filter: drop-shadow(0 1px 2px light-dark(rgba(0, 0, 0, 0.5), rgba(255, 255, 255, 0.35)));\n}\n/* Feedback intensifies the chevron toward its extreme (white in light mode, black in dark)\n rather than tinting it, so no new colour is introduced. Not tied to --zine-controls-fg: that\n sets the resting fill, and hover has to differ from it to read as feedback. :active clears on\n release, so tap and click feel the same; :hover is behind (hover: hover) so it never sticks on\n touch. */\n.zine-arrow:active:not(:disabled) { color: light-dark(#ffffff, #000000); }\n@media (hover: hover) {\n .zine-arrow:hover:not(:disabled) { color: light-dark(#ffffff, #000000); }\n}\n.zine-arrow:focus-visible {\n outline: 2px solid var(--zine-controls-accent, light-dark(#0284c7, #7dd3fc));\n outline-offset: 2px;\n border-radius: 8px;\n}\n/* Nothing to turn to: invisible, but still occupying its slot so the book does not slide across\n as the reader reaches a cover. */\n.zine-arrow-hidden { visibility: hidden; }\n/* Device-scoped arrows (controls.arrows: 'desktop' | 'mobile'). The split follows the same 640px\n breakpoint as the flank/overlay layouts below, so the arrows appear and disappear live as the\n window crosses it. */\n@media (max-width: 640px) {\n .zine-arrows-desktop > .zine-arrow { display: none; }\n}\n@media (min-width: 641px) {\n .zine-arrows-mobile > .zine-arrow { display: none; }\n}\n/* Too narrow to flank without squeezing the pages, so overlay the arrows on the book's edges\n instead. The wrap sits outside the container, so painting them over it leaves the container's\n measured box (which sizes the book and maps taps) untouched. */\n@media (max-width: 640px) {\n .zine-arrows-wrap { position: relative; }\n .zine-arrow {\n position: absolute;\n top: 50%;\n transform: translateY(-50%);\n z-index: 2;\n /* Sit back over the page so the reader can look at the book. */\n opacity: 0.7;\n }\n .zine-arrow:first-child { left: -2px; }\n .zine-arrow:last-child { right: -2px; }\n}\n@media (prefers-reduced-motion: no-preference) {\n .zine-arrow { transition: background 120ms ease; }\n}\n\n/* Side panels (thumbnails, outline, search). The rail wraps the book and its docked toolbar so it\n sits beside them as a flex sibling. On a wide screen the book shrinks to make room (below); on a\n narrow one the rail becomes a drawer over the book.\n align-items:flex-start, not stretch: the book sizes its height from its width through its own\n aspect-ratio, and that height is 'auto' from flex's point of view, so a stretch would override it\n and blow the book (and the rail matched to it) up to the flex line's cross size. Left as auto, the\n book keeps its aspect height and the holder alone stretches down to meet it (below). */\n.zine-panel-wrap { display: flex; align-items: flex-start; gap: 8px; position: relative; }\n\n/* The book-side slot (the bare container, or the docked-toolbar wrapper around it) yields space to\n the fixed-width rail instead of overflowing: flex-shrink lets it fall below its own width, and\n min-width:0 removes the automatic content floor that would otherwise stop it. The book only\n actually shrinks if its width is elastic (a %, max-width, or the demo's min(...) with a % term);\n a hard-coded pixel width has nothing for the % to resolve smaller against. */\n.zine-panel-wrap > *:not(.zine-panel-holder):not(.zine-panel-scrim) {\n flex: 0 1 auto;\n min-width: 0;\n}\n/* With a docked toolbar the book lives inside .zine-controls-wrap, whose children are pinned\n flex:0 0 auto (so the bar keeps its size). Inside a panel wrap the book child — everything but\n the bar itself — must instead be allowed to shrink, so left/right docking flanks like the rest.\n Top/bottom docking already shrinks through the container's own % width. */\n.zine-panel-wrap .zine-controls-wrap > *:not(.zine-controls) {\n flex: 0 1 auto;\n min-width: 0;\n min-height: 0;\n}\n@media (prefers-reduced-motion: no-preference) {\n .zine-panel-wrap > *:not(.zine-panel-holder):not(.zine-panel-scrim) {\n transition: flex-basis 160ms ease, width 160ms ease;\n }\n}\n\n/* The holder stretches to the book's height; the rail is absolutely positioned inside it so a\n long list scrolls rather than growing the row and running past the bottom of the book. */\n.zine-panel-holder {\n position: relative;\n flex: 0 0 auto;\n align-self: stretch;\n min-height: 0;\n}\n/* The scrim dims the book behind the narrow-screen drawer. It exists on every screen but only\n shows under the breakpoint below, so on a wide screen it takes no flex slot and never intercepts\n a click. */\n.zine-panel-scrim { display: none; }\n.zine-panel {\n position: absolute;\n inset: 0;\n display: flex;\n flex-direction: column;\n gap: 6px;\n overflow-y: auto;\n /* Chain overscroll up to the page: once the list is at its end (or too short to scroll at all),\n a further wheel scrolls the page behind, the same as scrolling over the book itself does. */\n overscroll-behavior: auto;\n /* Reserve the scrollbar's lane whether or not it is showing, so rows never sit under the bar\n and the close button (inset past this lane below) has a fixed edge to clear. */\n scrollbar-gutter: stable;\n padding: 6px;\n box-sizing: border-box;\n border-radius: 8px;\n background: var(--zine-controls-bg, light-dark(rgba(255, 255, 255, 0.94), rgba(24, 24, 27, 0.82)));\n color: var(--zine-controls-fg, light-dark(#18181b, #f4f4f5));\n font: 500 11px/1.2 system-ui, sans-serif;\n scrollbar-width: thin;\n touch-action: auto;\n -webkit-user-select: none;\n user-select: none;\n}\n/* The rail sits flush against the book's reading-start edge (left, or right in RTL), so its outer\n corners there would round away from that edge and leave a gap. Square them; keep the interior\n corners, which face the page, rounded. */\n.zine-panel { border-radius: 0 8px 8px 0; }\n.zine-panel-wrap-rtl .zine-panel { border-radius: 8px 0 0 8px; }\n/* Overlay fallback, used when the container has no parent to wrap. */\n.zine-panel-overlay { position: absolute; inset: 0 auto 0 0; z-index: 3; }\n.zine-panel-active { background: var(--zine-controls-hover, light-dark(rgba(0,0,0,0.08), rgba(255,255,255,0.18))); }\n.zine-panel-note { padding: 8px; opacity: 0.66; line-height: 1.4; }\n/* Discoverable dismiss, floated in the rail's trailing-top (interior) corner, away from the book.\n It sits in the holder above the scrolling list so the list scrolls under it. A translucent\n backdrop keeps the glyph legible over a thumbnail or a line of text. */\n.zine-panel-close {\n position: absolute;\n top: 6px;\n /* Inset past the reserved scrollbar gutter so the button never overlaps the bar. */\n right: 12px;\n z-index: 1;\n display: inline-flex;\n width: 24px;\n height: 24px;\n align-items: center;\n justify-content: center;\n padding: 0;\n border: 0;\n border-radius: 6px;\n /* Opaque so a light thumbnail underneath never shows through and dims the glyph. */\n background: light-dark(#f4f4f5, #27272a);\n color: inherit;\n cursor: pointer;\n}\n.zine-panel-close svg { width: 15px; height: 15px; }\n/* Opaque hover, not the translucent hover token: a see-through backdrop over a light thumbnail\n would wash the glyph out. Solid neutrals keep the X readable in both themes over anything. */\n.zine-panel-close:hover { background: light-dark(#e4e4e7, #3f3f46); }\n.zine-panel-close:focus-visible {\n outline: 2px solid var(--zine-controls-accent, light-dark(#0284c7, #7dd3fc));\n outline-offset: -2px;\n}\n.zine-panel-wrap-rtl .zine-panel-close { right: auto; left: 12px; }\n/* End the search field just before the close button rather than running under it, so the two sit\n side by side and the field's border reads cleanly. The button's inner edge is ~30px from the\n panel's content edge (24px wide, its right edge 6px in once the reserved scrollbar gutter is\n accounted for), so 30px leaves them flush with a hair of breathing room. width:auto lets the\n column's stretch fill the rest, so the margin shortens the field instead of overflowing 100%. */\n.zine-search .zine-search-input { width: auto; align-self: stretch; margin-right: 30px; }\n.zine-panel-wrap-rtl .zine-search .zine-search-input { margin-right: 0; margin-left: 30px; }\n\n/* Overlay panels (outline, search) on a wide screen: float over the book's start edge instead of\n flanking it, so the book never resizes. The wrap drops to a block, laying the book out exactly as\n it was before the panel opened; the holder sits absolute over it at full book height, with a\n shadow so it reads as lifted off the page. thumbnails is not overlaid — it stays a flex flank and\n shrinks the book only when the two will not otherwise both fit. */\n@media (min-width: 641px) {\n .zine-panel-wrap-overlay { display: block; }\n .zine-panel-wrap-overlay .zine-panel-holder {\n position: absolute;\n inset: 0 auto 0 0;\n z-index: 4;\n box-shadow: 0 0 24px light-dark(rgba(0, 0, 0, 0.22), rgba(0, 0, 0, 0.55));\n }\n .zine-panel-wrap-overlay.zine-panel-wrap-rtl .zine-panel-holder { inset: 0 0 0 auto; }\n /* Opaque over the page it floats on, unlike a flanking rail that sits against empty margin. */\n .zine-panel-wrap-overlay .zine-panel {\n background: var(--zine-controls-bg, light-dark(#ffffff, #18181b));\n }\n /* A click-away layer over the book, so pressing the page behind the floating rail closes it the\n way tapping the scrim does on a narrow screen. Transparent here — the wide-screen rail only\n covers an edge, so there is no need to dim the page the reader is still looking at. It sits\n under the holder (z-index) so the rail's own rows still take their clicks. */\n .zine-panel-wrap-overlay .zine-panel-scrim {\n display: block;\n position: absolute;\n inset: 0;\n z-index: 3;\n background: transparent;\n }\n}\n@media (min-width: 641px) and (prefers-reduced-motion: no-preference) {\n .zine-panel-wrap-overlay .zine-panel-holder { animation: zine-drawer-in 180ms ease; }\n .zine-panel-wrap-overlay.zine-panel-wrap-rtl .zine-panel-holder { animation-name: zine-drawer-in-rtl; }\n}\n\n/* Narrow screen: no room to flank, so the rail becomes a drawer over the book and the scrim dims\n the page behind it. The wrap is position:relative, so the absolute holder and scrim below are\n measured against it — i.e. against the book's own box. */\n@media (max-width: 640px) {\n /* No flanking here — the rail and scrim both overlay absolutely — so drop the flex row entirely.\n As a plain block wrap the book keeps the exact box it had before a panel opened (its own width,\n margin and aspect-ratio height); a flex row would instead re-resolve the book's width and, with\n it, its aspect-derived height, enlarging the canvas and pushing the page down. */\n .zine-panel-wrap {\n display: block;\n }\n .zine-panel-holder {\n position: absolute;\n inset: 0 auto 0 0;\n z-index: 4;\n /* Cap the drawer so it never eats the whole book; its inline width still applies under this. */\n max-width: 80%;\n box-shadow: 0 0 24px light-dark(rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.6));\n }\n /* RTL reads from the right, so the drawer enters from there. */\n .zine-panel-wrap-rtl .zine-panel-holder { inset: 0 0 0 auto; }\n /* Opaque over the page, unlike the wide-screen rail which can sit against empty margin. */\n .zine-panel {\n background: var(--zine-controls-bg, light-dark(#ffffff, #18181b));\n }\n .zine-panel-scrim {\n display: block;\n position: absolute;\n inset: 0;\n z-index: 3;\n background: rgba(0, 0, 0, 0.4);\n }\n}\n@media (max-width: 640px) and (prefers-reduced-motion: no-preference) {\n .zine-panel-holder { animation: zine-drawer-in 180ms ease; }\n .zine-panel-wrap-rtl .zine-panel-holder { animation-name: zine-drawer-in-rtl; }\n .zine-panel-scrim { animation: zine-scrim-in 180ms ease; }\n}\n@keyframes zine-drawer-in { from { transform: translateX(-100%); } to { transform: translateX(0); } }\n@keyframes zine-drawer-in-rtl { from { transform: translateX(100%); } to { transform: translateX(0); } }\n@keyframes zine-scrim-in { from { opacity: 0; } to { opacity: 1; } }\n\n.zine-thumbs { align-items: center; }\n\n.zine-thumbs-row {\n display: flex;\n flex-wrap: wrap;\n justify-content: center;\n gap: 2px;\n width: 100%;\n padding: 4px;\n border: 0;\n border-radius: 6px;\n background: transparent;\n color: inherit;\n font: inherit;\n cursor: pointer;\n box-sizing: border-box;\n}\n.zine-thumbs-row:hover { background: var(--zine-controls-hover, light-dark(rgba(0,0,0,0.08), rgba(255,255,255,0.14))); }\n.zine-thumbs-row:focus-visible {\n outline: 2px solid var(--zine-controls-accent, light-dark(#0284c7, #7dd3fc));\n outline-offset: -2px;\n}\n.zine-thumbs-row.zine-panel-active .zine-thumbs-cell {\n outline: 1px solid var(--zine-controls-accent, light-dark(#0284c7, #7dd3fc));\n}\n\n/* Outline: a single column of headings, indented by depth. */\n/* No gap: a hairline between rows is the separator instead, so a title that wraps to two lines\n still reads as one entry rather than blending into its neighbours. */\n.zine-outline { gap: 0; }\n.zine-outline-row {\n display: block;\n width: 100%;\n padding: 6px 8px;\n border: 0;\n border-bottom: 1px solid var(--zine-controls-divider, light-dark(rgba(0, 0, 0, 0.08), rgba(255, 255, 255, 0.1)));\n border-radius: 5px;\n background: transparent;\n color: inherit;\n font: inherit;\n text-align: left;\n line-height: 1.35;\n cursor: pointer;\n box-sizing: border-box;\n overflow-wrap: anywhere;\n}\n.zine-outline-row:last-child { border-bottom: 0; }\n.zine-outline-row:hover:not(:disabled) {\n background: var(--zine-controls-hover, light-dark(rgba(0,0,0,0.08), rgba(255,255,255,0.14)));\n}\n.zine-outline-row:focus-visible {\n outline: 2px solid var(--zine-controls-accent, light-dark(#0284c7, #7dd3fc));\n outline-offset: -2px;\n}\n/* A heading whose destination could not be resolved: shown, but nothing to click through to. */\n.zine-outline-row:disabled { opacity: 0.5; cursor: default; }\n\n.zine-thumbs-cell {\n flex: 1 1 0;\n min-width: 0;\n min-height: 24px;\n display: flex;\n background: light-dark(rgba(0, 0, 0, 0.08), rgba(0, 0, 0, 0.3));\n border-radius: 2px;\n overflow: hidden;\n}\n/* A lone page in a book that pairs elsewhere (a cover, a back page) keeps one page's width and\n centres, instead of stretching across both columns. */\n.zine-thumbs-row-lone .zine-thumbs-cell { flex: 0 0 calc(50% - 1px); }\n.zine-thumbs-img { display: block; width: 100%; height: auto; }\n.zine-thumbs-caption { flex: 1 0 100%; text-align: center; opacity: 0.7; }\n\n/* Share dialog. Centred over the page rather than in the side rail: a QR and six destinations\n need more room than the rail gives, and sharing is a brief interruption, not a browsing mode. */\n.zine-share-backdrop {\n position: fixed;\n inset: 0;\n z-index: 2147483000;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 16px;\n background: rgba(0, 0, 0, 0.55);\n font: 500 13px/1.4 system-ui, sans-serif;\n color: var(--zine-controls-fg, light-dark(#18181b, #f4f4f5));\n}\n.zine-share {\n width: min(320px, 100%);\n max-height: 100%;\n overflow-y: auto;\n display: flex;\n flex-direction: column;\n gap: 12px;\n padding: 14px;\n border-radius: 12px;\n background: var(--zine-share-bg, light-dark(#ffffff, #1c1c20));\n box-shadow: 0 18px 50px rgba(0, 0, 0, 0.45);\n box-sizing: border-box;\n}\n.zine-share-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }\n.zine-share-head strong { font-size: 15px; }\n.zine-share-close {\n display: inline-flex;\n width: 26px;\n height: 26px;\n align-items: center;\n justify-content: center;\n padding: 0;\n border: 0;\n border-radius: 6px;\n background: transparent;\n color: inherit;\n cursor: pointer;\n}\n.zine-share-close svg { width: 16px; height: 16px; }\n.zine-share-close:hover { background: var(--zine-controls-hover, light-dark(rgba(0,0,0,0.08), rgba(255,255,255,0.14))); }\n\n.zine-share-qr {\n align-self: center;\n line-height: 0;\n padding: 8px;\n border-radius: 8px;\n background: #fff;\n}\n.zine-share-qr svg { display: block; }\n\n.zine-share-link { display: flex; gap: 6px; }\n.zine-share-link input {\n flex: 1 1 auto;\n min-width: 0;\n padding: 7px 9px;\n border: 1px solid var(--zine-controls-hover, light-dark(rgba(0,0,0,0.14), rgba(255,255,255,0.2)));\n border-radius: 7px;\n background: light-dark(rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.3));\n color: inherit;\n font: inherit;\n}\n.zine-share-copy {\n flex: 0 0 auto;\n min-width: 74px;\n padding: 7px 12px;\n border: 0;\n border-radius: 7px;\n background: var(--zine-controls-accent, light-dark(#0284c7, #7dd3fc));\n color: light-dark(#ffffff, #06202c);\n font: inherit;\n font-weight: 600;\n cursor: pointer;\n}\n\n.zine-share-socials { display: flex; flex-wrap: wrap; gap: 6px; justify-content: center; }\n.zine-share-social {\n display: inline-flex;\n width: 38px;\n height: 38px;\n align-items: center;\n justify-content: center;\n border-radius: 8px;\n background: light-dark(rgba(0, 0, 0, 0.06), rgba(255, 255, 255, 0.08));\n color: inherit;\n text-decoration: none;\n}\n.zine-share-social svg { width: 19px; height: 19px; }\n.zine-share-social:hover { background: var(--zine-controls-hover, light-dark(rgba(0,0,0,0.12), rgba(255,255,255,0.18))); }\n.zine-share-close:focus-visible,\n.zine-share-copy:focus-visible,\n.zine-share-social:focus-visible,\n.zine-share-link input:focus-visible {\n outline: 2px solid var(--zine-controls-accent, light-dark(#0284c7, #7dd3fc));\n outline-offset: 2px;\n}\n\n@media (prefers-reduced-motion: no-preference) {\n .zine-controls-btn { transition: background 120ms ease; }\n}\n`;\n\n/** Add the stylesheet to `doc` if it is not already there. */\nexport function ensureStyles(doc: Document): void {\n if (doc.getElementById(STYLE_ID)) return;\n const style = doc.createElement('style');\n style.id = STYLE_ID;\n style.textContent = CSS;\n (doc.head ?? doc.documentElement)?.appendChild(style);\n}\n\n/**\n * Pin an element's colour scheme so the `light-dark()` fallbacks resolve to one palette.\n *\n * `'auto'` does nothing: `color-scheme` inherits, so the element keeps the page's used scheme and\n * `light-dark()` follows it. `'light'` / `'dark'` set the property on the element itself, which its\n * themed descendants (buttons, menu, dialog) inherit in turn. An unset `--zine-controls-*` reads\n * the forced palette; a set one still overrides it, since the tokens sit outside `light-dark()`.\n */\nexport function applyColorScheme(el: HTMLElement, scheme: 'light' | 'dark' | 'auto'): void {\n if (scheme !== 'auto') el.style.colorScheme = scheme;\n}\n"],"mappings":";AAcA,IAAa,IAAQ;CAEnB,MAAM;CAEN,MAAM;CAEN,OAAO;CAEP,MAAM;CAEN,QAAQ;CAER,SAAS;CAET,QAAQ;CAER,OACE;CAEF,MAAM;CAEN,YACE;CAEF,gBACE;CAEF,YACE;CAEF,SACE;CAEF,UACE;CAEF,SAAS;CAET,OACE;CAEF,UAAU;CAEV,OAAO;CAEP,SACE;CAEF,UACE;AACJ;AAGA,SAAgB,EAAW,GAAe,GAA+B;CACvE,IAAM,IAAM,EAAI,gBAAgB,8BAA8B,KAAK;CAanE,OAZA,EAAI,aAAa,WAAW,WAAW,GACvC,EAAI,aAAa,QAAQ,MAAM,GAC/B,EAAI,aAAa,UAAU,cAAc,GAGzC,EAAI,aAAa,gBAAgB,MAAM,GACvC,EAAI,aAAa,kBAAkB,OAAO,GAC1C,EAAI,aAAa,mBAAmB,OAAO,GAC3C,EAAI,aAAa,eAAe,MAAM,GACtC,EAAI,aAAa,aAAa,OAAO,GAErC,EAAI,YAAY,GACT;AACT;;;ACpEA,IAAM,IAAW,uBAEJ,IAAM;AAyrBnB,SAAgB,EAAa,GAAqB;CAChD,IAAI,EAAI,eAAe,CAAQ,GAAG;CAClC,IAAM,IAAQ,EAAI,cAAc,OAAO;CAGvC,AAFA,EAAM,KAAK,GACX,EAAM,cAAc,IACnB,EAAI,QAAQ,EAAI,gBAAA,EAAkB,YAAY,CAAK;AACtD;AAUA,SAAgB,EAAiB,GAAiB,GAAyC;CACzF,AAAI,MAAW,WAAQ,EAAG,MAAM,cAAc;AAChD"}
|