@zinejs/core 0.9.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/controls/sidebar.d.ts +2 -0
- package/dist/controls/types.d.ts +3 -0
- package/dist/{controls-DzVdwPJB.js → controls-Dh1okIx6.js} +50 -43
- package/dist/controls-Dh1okIx6.js.map +1 -0
- package/dist/cssRenderer-DAW29lU2.js +156 -0
- package/dist/cssRenderer-DAW29lU2.js.map +1 -0
- package/dist/engine/gutter.d.ts +12 -0
- package/dist/gutter-C3H2CDT2.js +43 -0
- package/dist/gutter-C3H2CDT2.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +678 -564
- package/dist/index.js.map +1 -1
- package/dist/index.umd.js +14 -9
- package/dist/index.umd.js.map +1 -1
- package/dist/loading/loading.d.ts +2 -1
- package/dist/{loading-DF88K9yJ.js → loading-BgbqJz5k.js} +7 -8
- package/dist/loading-BgbqJz5k.js.map +1 -0
- package/dist/renderer/cssRenderer.d.ts +1 -1
- package/dist/renderer/types.d.ts +20 -0
- package/dist/renderer/webglRenderer.d.ts +1 -1
- package/dist/{share-Cq3RsjPr.js → share-CZorhnKI.js} +24 -21
- package/dist/share-CZorhnKI.js.map +1 -0
- package/dist/strings.d.ts +74 -0
- package/dist/types-DYqgosP1.js +9 -0
- package/dist/types-DYqgosP1.js.map +1 -0
- package/dist/webglRenderer-9RcNHAy-.js +430 -0
- package/dist/webglRenderer-9RcNHAy-.js.map +1 -0
- package/dist/zine.d.ts +18 -0
- package/options.schema.json +18 -0
- package/package.json +2 -2
- package/dist/controls-DzVdwPJB.js.map +0 -1
- package/dist/cssRenderer-Deskm2ND.js +0 -133
- package/dist/cssRenderer-Deskm2ND.js.map +0 -1
- package/dist/loading-DF88K9yJ.js.map +0 -1
- package/dist/share-Cq3RsjPr.js.map +0 -1
- package/dist/webglRenderer-CmQ_mUrX.js +0 -399
- package/dist/webglRenderer-CmQ_mUrX.js.map +0 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { LoadProgress } from '../source/types';
|
|
2
|
+
import type { ZineStrings } from '../strings';
|
|
2
3
|
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
4
|
/** Handle the engine uses to drive and tear down the overlay. */
|
|
4
5
|
export interface LoaderHandle {
|
|
@@ -15,4 +16,4 @@ export interface LoaderHandle {
|
|
|
15
16
|
* The overlay is hidden for the first {@link REVEAL_DELAY_MS} so a fast load never flashes it;
|
|
16
17
|
* `destroy()` before then cancels the reveal outright.
|
|
17
18
|
*/
|
|
18
|
-
export declare function mountLoading(container: HTMLElement): LoaderHandle;
|
|
19
|
+
export declare function mountLoading(container: HTMLElement, strings: ZineStrings): LoaderHandle;
|
|
@@ -5,8 +5,7 @@ function t(t) {
|
|
|
5
5
|
let n = t.createElement("style");
|
|
6
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
7
|
}
|
|
8
|
-
|
|
9
|
-
function r(e) {
|
|
8
|
+
function n(e, n) {
|
|
10
9
|
let r = e.ownerDocument;
|
|
11
10
|
t(r);
|
|
12
11
|
let i = r.createElement("div");
|
|
@@ -14,7 +13,7 @@ function r(e) {
|
|
|
14
13
|
let a = r.createElement("div");
|
|
15
14
|
a.className = "zine-loading-spinner", a.setAttribute("aria-hidden", "true");
|
|
16
15
|
let o = r.createElement("div");
|
|
17
|
-
o.className = "zine-loading-text", o.textContent =
|
|
16
|
+
o.className = "zine-loading-text", o.textContent = n.loadingOpening;
|
|
18
17
|
let s = r.createElement("div");
|
|
19
18
|
s.className = "zine-loading-bar", s.style.display = "none";
|
|
20
19
|
let c = r.createElement("div");
|
|
@@ -28,11 +27,11 @@ function r(e) {
|
|
|
28
27
|
update(e) {
|
|
29
28
|
if (e.total > 0) {
|
|
30
29
|
let t = Math.min(100, Math.round(e.loaded / e.total * 100));
|
|
31
|
-
o.textContent =
|
|
32
|
-
} else o.textContent =
|
|
30
|
+
o.textContent = n.downloadingPercent(t), s.style.display = "", c.style.width = `${t}%`;
|
|
31
|
+
} else o.textContent = n.downloadingSize(e.loaded / 1024 / 1024), s.style.display = "none";
|
|
33
32
|
},
|
|
34
33
|
preparing() {
|
|
35
|
-
o.textContent =
|
|
34
|
+
o.textContent = n.loadingPreparing, s.style.display = "none";
|
|
36
35
|
},
|
|
37
36
|
destroy() {
|
|
38
37
|
d || clearTimeout(f), i.remove(), l !== null && (e.style.position = l);
|
|
@@ -40,6 +39,6 @@ function r(e) {
|
|
|
40
39
|
};
|
|
41
40
|
}
|
|
42
41
|
//#endregion
|
|
43
|
-
export {
|
|
42
|
+
export { n as mountLoading };
|
|
44
43
|
|
|
45
|
-
//# sourceMappingURL=loading-
|
|
44
|
+
//# sourceMappingURL=loading-BgbqJz5k.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loading-BgbqJz5k.js","names":[],"sources":["../src/loading/loading.ts"],"sourcesContent":["import type { LoadProgress } from '../source/types';\nimport type { ZineStrings } from '../strings';\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\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, strings: ZineStrings): 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 = strings.loadingOpening;\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 = strings.downloadingPercent(pct);\n bar.style.display = '';\n fill.style.width = `${pct}%`;\n } else {\n text.textContent = strings.downloadingSize(progress.loaded / 1024 / 1024);\n bar.style.display = 'none';\n }\n },\n preparing(): void {\n text.textContent = strings.loadingPreparing;\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":";AAmBA,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;AAkBA,SAAgB,EAAa,GAAwB,GAAoC;CACvF,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,EAAQ;CAE3B,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,EAAQ,mBAAmB,CAAG,GACjD,EAAI,MAAM,UAAU,IACpB,EAAK,MAAM,QAAQ,GAAG,EAAI;GAC5B,OAEE,AADA,EAAK,cAAc,EAAQ,gBAAgB,EAAS,SAAS,OAAO,IAAI,GACxE,EAAI,MAAM,UAAU;EAExB;EACA,YAAkB;GAEhB,AADA,EAAK,cAAc,EAAQ,kBAC3B,EAAI,MAAM,UAAU;EACtB;EACA,UAAgB;GAGd,AAFK,KAAU,aAAa,CAAW,GACvC,EAAQ,OAAO,GACX,MAAoB,SAAM,EAAU,MAAM,WAAW;EAC3D;CACF;AACF"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Spread } from '../engine/spread';
|
|
2
|
-
import type
|
|
2
|
+
import { type FlipDirection, type LayoutMetrics, type Renderer, type RenderOptions, type SpreadContent } from './types';
|
|
3
3
|
/**
|
|
4
4
|
* Baseline renderer: a 2-panel CSS-3D fold. Runs on every browser, no GPU. Pages
|
|
5
5
|
* are <canvas> elements; the turning leaf (front + back faces, backface-hidden)
|
package/dist/renderer/types.d.ts
CHANGED
|
@@ -6,7 +6,20 @@ export type PageContent = ImageBitmap | HTMLCanvasElement;
|
|
|
6
6
|
export interface SpreadContent {
|
|
7
7
|
left: PageContent | null;
|
|
8
8
|
right: PageContent | null;
|
|
9
|
+
/** Fraction of each page's width hidden at its spine edge, removing a print-gutter duplicate
|
|
10
|
+
* (see `gutterOverlap`). Honoured only on a full two-page spread; 0 or omitted = none. */
|
|
11
|
+
spineTrim?: number;
|
|
9
12
|
}
|
|
13
|
+
/** The spine trim actually applied: only a full two-page spread is trimmed, never a lone page or
|
|
14
|
+
* single-page (fill) mode, where there is no facing page to duplicate. */
|
|
15
|
+
export declare function spineTrimOf(content: SpreadContent, fill: boolean): number;
|
|
16
|
+
/** The visible horizontal part of a page's raster, as [start, end] fractions of its width. */
|
|
17
|
+
export type PageSpan = readonly [number, number];
|
|
18
|
+
export declare const FULL_SPAN: PageSpan;
|
|
19
|
+
/** A left-hand page's spine is its right edge, so a spine trim narrows it from the right. */
|
|
20
|
+
export declare const leftSpan: (trim: number) => PageSpan;
|
|
21
|
+
/** A right-hand page's spine is its left edge. */
|
|
22
|
+
export declare const rightSpan: (trim: number) => PageSpan;
|
|
10
23
|
export type FlipDirection = 'forward' | 'backward';
|
|
11
24
|
export interface RenderOptions {
|
|
12
25
|
/** Render a lone page filling the container (single-page mode), not a half panel. */
|
|
@@ -15,6 +28,9 @@ export interface RenderOptions {
|
|
|
15
28
|
curl?: CurlSpec;
|
|
16
29
|
/** Corner the fold originates from for anchored curls; y in [0,1] (where the reader tapped). */
|
|
17
30
|
anchor?: CurlAnchor;
|
|
31
|
+
/** How a page whose size differs from the book's fits: `contain` (default) keeps its aspect and
|
|
32
|
+
* centers it (a margin appears); `fill` stretches it to the book's shape. Book-global. */
|
|
33
|
+
fit?: 'contain' | 'fill';
|
|
18
34
|
}
|
|
19
35
|
/** Layout metrics the engine needs for geometry and mode decisions. */
|
|
20
36
|
export interface LayoutMetrics {
|
|
@@ -39,6 +55,10 @@ export interface LayoutMetrics {
|
|
|
39
55
|
width: number;
|
|
40
56
|
height: number;
|
|
41
57
|
};
|
|
58
|
+
/** The book's stable aspect (width / height) for the whole document, from the first page rather
|
|
59
|
+
* than the current spread. The container is sized to this so a document with off-size pages does
|
|
60
|
+
* not resize (layout shift) as you navigate. Omit before the first paint. */
|
|
61
|
+
containerAspect?: number;
|
|
42
62
|
/**
|
|
43
63
|
* Where `content` would land at zoom `scale` with no pan, in container px.
|
|
44
64
|
*
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Spread } from '../engine/spread';
|
|
2
|
-
import type
|
|
2
|
+
import { type FlipDirection, type LayoutMetrics, type Renderer, type RenderOptions, type SpreadContent } from './types';
|
|
3
3
|
/**
|
|
4
4
|
* GPU renderer (WebGL2, hand-written). The turning leaf is a dense grid deformed on
|
|
5
5
|
* the CPU by the selected curl model (see geometry/curls) and
|
|
@@ -208,63 +208,66 @@ var y = 132, b = 1600, x = class {
|
|
|
208
208
|
#r;
|
|
209
209
|
#i = null;
|
|
210
210
|
#a;
|
|
211
|
+
#o;
|
|
211
212
|
constructor(r, i, a = "auto") {
|
|
212
213
|
let o = i.ownerDocument;
|
|
213
|
-
this.#e = o, this.#r = o.activeElement;
|
|
214
|
-
let s = r.pageLink(), c = o.title ||
|
|
214
|
+
this.#e = o, this.#r = o.activeElement, this.#o = r.strings;
|
|
215
|
+
let s = r.pageLink(), c = o.title || this.#o.shareFallbackTitle;
|
|
215
216
|
this.#t = o.createElement("div"), this.#t.className = "zine-share-backdrop", n(this.#t, a), this.#t.addEventListener("pointerdown", (e) => {
|
|
216
217
|
e.target === this.#t && this.close();
|
|
217
|
-
}), this.#n = o.createElement("div"), this.#n.className = "zine-share", this.#n.setAttribute("role", "dialog"), this.#n.setAttribute("aria-modal", "true"), this.#n.setAttribute("aria-label",
|
|
218
|
+
}), this.#n = o.createElement("div"), this.#n.className = "zine-share", this.#n.setAttribute("role", "dialog"), this.#n.setAttribute("aria-modal", "true"), this.#n.setAttribute("aria-label", this.#o.share), this.#t.appendChild(this.#n);
|
|
218
219
|
let l = o.createElement("div");
|
|
219
220
|
l.className = "zine-share-head";
|
|
220
221
|
let u = o.createElement("strong");
|
|
221
|
-
u.textContent =
|
|
222
|
+
u.textContent = this.#o.share;
|
|
222
223
|
let d = o.createElement("button");
|
|
223
|
-
d.type = "button", d.className = "zine-share-close", d.setAttribute("aria-label",
|
|
224
|
-
let f = this.#
|
|
224
|
+
d.type = "button", d.className = "zine-share-close", d.setAttribute("aria-label", this.#o.close), d.appendChild(e(o, t.close)), d.addEventListener("click", () => this.close()), l.append(u, d), this.#n.append(l, this.#c(o, s), this.#u(o, s, c));
|
|
225
|
+
let f = this.#s(o, s);
|
|
225
226
|
f && this.#n.insertBefore(f, this.#n.children[1]), i.ownerDocument.body.appendChild(this.#t), this.#a = (e) => {
|
|
226
|
-
e.key === "Escape" && this.close(), e.key === "Tab" && this.#
|
|
227
|
+
e.key === "Escape" && this.close(), e.key === "Tab" && this.#d(e);
|
|
227
228
|
}, this.#t.addEventListener("keydown", this.#a), d.focus();
|
|
228
229
|
}
|
|
229
|
-
#
|
|
230
|
+
#s(e, t) {
|
|
230
231
|
if (!t) return null;
|
|
231
232
|
try {
|
|
232
233
|
let n = e.createElement("div");
|
|
233
|
-
return n.className = "zine-share-qr", n.innerHTML = v(t, y), n.setAttribute("aria-label",
|
|
234
|
+
return n.className = "zine-share-qr", n.innerHTML = v(t, y), n.setAttribute("aria-label", this.#o.qrLabel), n.setAttribute("role", "img"), n;
|
|
234
235
|
} catch {
|
|
235
236
|
return null;
|
|
236
237
|
}
|
|
237
238
|
}
|
|
238
|
-
#
|
|
239
|
+
#c(e, t) {
|
|
239
240
|
let n = e.createElement("div");
|
|
240
241
|
n.className = "zine-share-link";
|
|
241
242
|
let r = e.createElement("input");
|
|
242
|
-
r.type = "text", r.readOnly = !0, r.value = t, r.setAttribute("aria-label",
|
|
243
|
+
r.type = "text", r.readOnly = !0, r.value = t, r.setAttribute("aria-label", this.#o.linkLabel), r.addEventListener("focus", () => r.select());
|
|
243
244
|
let i = e.createElement("button");
|
|
244
|
-
return i.type = "button", i.className = "zine-share-copy", i.textContent =
|
|
245
|
-
this.#
|
|
245
|
+
return i.type = "button", i.className = "zine-share-copy", i.textContent = this.#o.copy, i.addEventListener("click", () => {
|
|
246
|
+
this.#l(t, i);
|
|
246
247
|
}), n.append(r, i), n;
|
|
247
248
|
}
|
|
248
|
-
async #
|
|
249
|
+
async #l(e, t) {
|
|
249
250
|
try {
|
|
250
|
-
await navigator.clipboard?.writeText(e), t.textContent =
|
|
251
|
+
await navigator.clipboard?.writeText(e), t.textContent = this.#o.copied;
|
|
251
252
|
} catch {
|
|
252
|
-
t.textContent =
|
|
253
|
+
t.textContent = this.#o.copyManual;
|
|
253
254
|
}
|
|
254
255
|
this.#i && clearTimeout(this.#i), this.#i = setTimeout(() => {
|
|
255
|
-
t.textContent =
|
|
256
|
+
t.textContent = this.#o.copy;
|
|
256
257
|
}, b);
|
|
257
258
|
}
|
|
258
|
-
#
|
|
259
|
+
#u(t, n, r) {
|
|
259
260
|
let a = t.createElement("div");
|
|
260
261
|
a.className = "zine-share-socials";
|
|
261
262
|
for (let o of i) {
|
|
262
263
|
let i = t.createElement("a");
|
|
263
|
-
i.className = "zine-share-social", i.href = o.href(n, r)
|
|
264
|
+
i.className = "zine-share-social", i.href = o.href(n, r);
|
|
265
|
+
let s = o.id === "email" ? this.#o.email : o.label;
|
|
266
|
+
i.title = s, i.setAttribute("aria-label", this.#o.shareOn(s)), o.id !== "email" && (i.target = "_blank", i.rel = "noopener noreferrer"), i.appendChild(e(t, o.icon)), a.appendChild(i);
|
|
264
267
|
}
|
|
265
268
|
return a;
|
|
266
269
|
}
|
|
267
|
-
#
|
|
270
|
+
#d(e) {
|
|
268
271
|
let t = [...this.#n.querySelectorAll("button, a[href], input")].filter((e) => !e.hasAttribute("disabled"));
|
|
269
272
|
if (t.length === 0) return;
|
|
270
273
|
let n = t[0], r = t.at(-1), i = this.#e.activeElement;
|
|
@@ -280,4 +283,4 @@ var y = 132, b = 1600, x = class {
|
|
|
280
283
|
//#endregion
|
|
281
284
|
export { x as ShareDialog };
|
|
282
285
|
|
|
283
|
-
//# sourceMappingURL=share-
|
|
286
|
+
//# sourceMappingURL=share-CZorhnKI.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"share-CZorhnKI.js","names":["#doc","#lastFocus","#strings","#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';\nimport type { ZineStrings } from '../strings';\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 #strings: ZineStrings;\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 this.#strings = zine.strings;\n\n const url = zine.pageLink();\n const title = doc.title || this.#strings.shareFallbackTitle;\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', this.#strings.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 = this.#strings.share;\n const close = doc.createElement('button');\n close.type = 'button';\n close.className = 'zine-share-close';\n close.setAttribute('aria-label', this.#strings.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', this.#strings.qrLabel);\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', this.#strings.linkLabel);\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 = this.#strings.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 = this.#strings.copied;\n } catch {\n button.textContent = this.#strings.copyManual;\n }\n if (this.#timer) clearTimeout(this.#timer);\n this.#timer = setTimeout(() => {\n button.textContent = this.#strings.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 // Brand names stay as-is; \"Email\" is the one social label that translates.\n const label = social.id === 'email' ? this.#strings.email : social.label;\n link.title = label;\n link.setAttribute('aria-label', this.#strings.shareOn(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,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User-facing text for the reader UI (control labels, screen-reader announcements, panel and
|
|
3
|
+
* loading text). English by default; a consumer overrides any subset via the `strings` option.
|
|
4
|
+
*
|
|
5
|
+
* Static entries are plain strings. Entries that mix in a number or piece of text are functions,
|
|
6
|
+
* so a translation can handle pluralization and word order that a `{placeholder}` string cannot.
|
|
7
|
+
* Page numbers passed to these functions are already 1-based (display) values.
|
|
8
|
+
*/
|
|
9
|
+
export interface ZineStrings {
|
|
10
|
+
prevPage: string;
|
|
11
|
+
nextPage: string;
|
|
12
|
+
firstPage: string;
|
|
13
|
+
lastPage: string;
|
|
14
|
+
zoomIn: string;
|
|
15
|
+
zoomOut: string;
|
|
16
|
+
fullscreen: string;
|
|
17
|
+
mute: string;
|
|
18
|
+
unmute: string;
|
|
19
|
+
share: string;
|
|
20
|
+
downloadPdf: string;
|
|
21
|
+
showOnePage: string;
|
|
22
|
+
showTwoPages: string;
|
|
23
|
+
print: string;
|
|
24
|
+
more: string;
|
|
25
|
+
controlsLabel: string;
|
|
26
|
+
showThumbnails: string;
|
|
27
|
+
hideThumbnails: string;
|
|
28
|
+
showOutline: string;
|
|
29
|
+
hideOutline: string;
|
|
30
|
+
searchOpen: string;
|
|
31
|
+
searchClose: string;
|
|
32
|
+
pageWidgetLabel: string;
|
|
33
|
+
pageNumberLabel: string;
|
|
34
|
+
roledescription: string;
|
|
35
|
+
thumbnailsLabel: string;
|
|
36
|
+
outlineLabel: string;
|
|
37
|
+
outlineLoading: string;
|
|
38
|
+
outlineEmpty: string;
|
|
39
|
+
untitled: string;
|
|
40
|
+
searchResultsLabel: string;
|
|
41
|
+
searchPlaceholder: string;
|
|
42
|
+
searchInputLabel: string;
|
|
43
|
+
searching: string;
|
|
44
|
+
loadingOpening: string;
|
|
45
|
+
loadingPreparing: string;
|
|
46
|
+
shareFallbackTitle: string;
|
|
47
|
+
close: string;
|
|
48
|
+
copy: string;
|
|
49
|
+
copied: string;
|
|
50
|
+
copyManual: string;
|
|
51
|
+
qrLabel: string;
|
|
52
|
+
linkLabel: string;
|
|
53
|
+
email: string;
|
|
54
|
+
panHint: string;
|
|
55
|
+
pageAnnounce: (current: number, total: number) => string;
|
|
56
|
+
pageTotal: (total: number) => string;
|
|
57
|
+
downloadingPercent: (percent: number) => string;
|
|
58
|
+
downloadingSize: (megabytes: number) => string;
|
|
59
|
+
zoomHint: (opts: {
|
|
60
|
+
doubleClick: boolean;
|
|
61
|
+
wheel: boolean;
|
|
62
|
+
mac: boolean;
|
|
63
|
+
}) => string;
|
|
64
|
+
noMatches: (query: string) => string;
|
|
65
|
+
searchHitLabel: (page: number) => string;
|
|
66
|
+
searchHitAria: (page: number, excerpt: string) => string;
|
|
67
|
+
shareOn: (network: string) => string;
|
|
68
|
+
thumbnailAria: (pageLabels: string[]) => string;
|
|
69
|
+
outlineEntryAria: (title: string, page: number) => string;
|
|
70
|
+
}
|
|
71
|
+
/** Built-in English text. Overridden per key by the `strings` option. */
|
|
72
|
+
export declare const defaultStrings: ZineStrings;
|
|
73
|
+
/** Merge consumer overrides over the English defaults. A missing key keeps the default. */
|
|
74
|
+
export declare function resolveStrings(overrides?: Partial<ZineStrings>): ZineStrings;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
//#region src/renderer/types.ts
|
|
2
|
+
function e(e, t) {
|
|
3
|
+
return !t && e.left !== null && e.right !== null ? e.spineTrim ?? 0 : 0;
|
|
4
|
+
}
|
|
5
|
+
var t = [0, 1], n = (e) => [0, 1 - e], r = (e) => [e, 1];
|
|
6
|
+
//#endregion
|
|
7
|
+
export { e as i, n, r, t };
|
|
8
|
+
|
|
9
|
+
//# sourceMappingURL=types-DYqgosP1.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types-DYqgosP1.js","names":[],"sources":["../src/renderer/types.ts"],"sourcesContent":["import type { Spread } from '../engine/spread';\nimport type { CurlSpec, CurlAnchor } from '../geometry/curls/types';\n\n/** A decoded, ready-to-paint raster for one page (produced by a content source). */\nexport type PageContent = ImageBitmap | HTMLCanvasElement;\n\n/** Resolved content for the two halves of a spread; null = blank side. */\nexport interface SpreadContent {\n left: PageContent | null;\n right: PageContent | null;\n /** Fraction of each page's width hidden at its spine edge, removing a print-gutter duplicate\n * (see `gutterOverlap`). Honoured only on a full two-page spread; 0 or omitted = none. */\n spineTrim?: number;\n}\n\n/** The spine trim actually applied: only a full two-page spread is trimmed, never a lone page or\n * single-page (fill) mode, where there is no facing page to duplicate. */\nexport function spineTrimOf(content: SpreadContent, fill: boolean): number {\n return !fill && content.left !== null && content.right !== null ? (content.spineTrim ?? 0) : 0;\n}\n\n/** The visible horizontal part of a page's raster, as [start, end] fractions of its width. */\nexport type PageSpan = readonly [number, number];\nexport const FULL_SPAN: PageSpan = [0, 1];\n/** A left-hand page's spine is its right edge, so a spine trim narrows it from the right. */\nexport const leftSpan = (trim: number): PageSpan => [0, 1 - trim];\n/** A right-hand page's spine is its left edge. */\nexport const rightSpan = (trim: number): PageSpan => [trim, 1];\n\nexport type FlipDirection = 'forward' | 'backward';\n\nexport interface RenderOptions {\n /** Render a lone page filling the container (single-page mode), not a half panel. */\n fill?: boolean;\n /** Which page-curl model the WebGL2 renderer bends the leaf with (ignored by CSS). */\n curl?: CurlSpec;\n /** Corner the fold originates from for anchored curls; y in [0,1] (where the reader tapped). */\n anchor?: CurlAnchor;\n /** How a page whose size differs from the book's fits: `contain` (default) keeps its aspect and\n * centers it (a margin appears); `fill` stretches it to the book's shape. Book-global. */\n fit?: 'contain' | 'fill';\n}\n\n/** Layout metrics the engine needs for geometry and mode decisions. */\nexport interface LayoutMetrics {\n containerWidth: number;\n containerHeight: number;\n pageWidth: number;\n pageHeight: number;\n /** The letterboxed book area (aspect-fitted, centered) in container px — a stable 2-page\n * (or 1-page in fill) region used for aspect sizing. Omit to fall back to the full container. */\n book?: { x: number; y: number; width: number; height: number };\n /** Where the current spread is actually painted, in container px — equal to `book` for a\n * full spread, but the centered half for a lone page (cover / book front-back). This is the\n * region the engine hit-tests against so taps track the visible page. Omit to fall back. */\n content?: { x: number; y: number; width: number; height: number };\n /** The book's stable aspect (width / height) for the whole document, from the first page rather\n * than the current spread. The container is sized to this so a document with off-size pages does\n * not resize (layout shift) as you navigate. Omit before the first paint. */\n containerAspect?: number;\n /**\n * Where `content` would land at zoom `scale` with no pan, in container px.\n *\n * Reported by the renderer rather than derived by the engine because the two place a lone page\n * differently: WebGL2 offsets it outside the view scale, so the offset is fixed in screen space,\n * while CSS applies it inside and it magnifies. The engine adds the translate to this to bound\n * panning; without it a lone page can be dragged until its content is off screen. Omit to fall\n * back to the container.\n */\n screenAt?(scale: number): { x: number; y: number; width: number; height: number };\n}\n\n/**\n * The contract every renderer implements (CSS baseline, WebGL2/GPU). The engine\n * drives it and never reaches past it, so the renderers are interchangeable and\n * behaviorally identical (PRD §8). One is selected at init; a device fetches one.\n */\nexport interface Renderer {\n /** Attach to a container. Async to accommodate GPU context init (WebGL2). */\n mount(container: HTMLElement): Promise<void>;\n\n /** Tear down: remove DOM/listeners, release any GPU context. */\n destroy(): void;\n\n /** Paint a spread from already-resolved page rasters (static, no flip). */\n renderSpread(spread: Spread, content: SpreadContent, options?: RenderOptions): void;\n\n /**\n * Set up a flip: the turning leaf's front is `from`'s leading page and its back\n * is `to`'s facing page, with the destination revealed underneath. Call once at\n * flip start, then drive `setFlipProgress`.\n */\n beginFlip(\n from: SpreadContent,\n to: SpreadContent,\n direction: FlipDirection,\n options?: RenderOptions,\n ): void;\n\n /**\n * Drive the turning page to progress `t` (0..1) in `direction`. Also the stable\n * seek API used by visual-regression tests (call `beginFlip` first to stage content).\n */\n setFlipProgress(t: number, direction: FlipDirection): void;\n\n /** Apply zoom/pan: uniform `scale` about the viewport, translated by (x, y). */\n setViewTransform(scale: number, x: number, y: number): void;\n\n /** Current layout metrics (container + page dimensions). */\n measure(): LayoutMetrics;\n\n /**\n * Largest texture dimension (px) the renderer can accept, when it has a hard cap (WebGL2's\n * MAX_TEXTURE_SIZE). Sources read it to avoid rasterizing a zoom tile the GPU would reject and\n * paint black. Omit when unbounded (the CSS renderer).\n */\n readonly maxTextureSize?: number;\n\n /**\n * Optional: register a handler the renderer calls when it hits an unrecoverable\n * state (e.g. a GPU context that cannot be restored), so the engine can swap to\n * the CSS fallback (§8.4). Renderers that cannot fail this way omit it.\n */\n onFatal?(handler: () => void): void;\n}\n"],"mappings":";AAiBA,SAAgB,EAAY,GAAwB,GAAuB;CACzE,OAAO,CAAC,KAAQ,EAAQ,SAAS,QAAQ,EAAQ,UAAU,OAAQ,EAAQ,aAAa,IAAK;AAC/F;AAIA,IAAa,IAAsB,CAAC,GAAG,CAAC,GAE3B,KAAY,MAA2B,CAAC,GAAG,IAAI,CAAI,GAEnD,KAAa,MAA2B,CAAC,GAAM,CAAC"}
|