@0k/pdf-viewer 0.0.0-stage → 0.2.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Valentin Lab
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,283 @@
1
- # Temporary Holding Version
1
+ # @0k/pdf-viewer
2
+
3
+ ## Purpose
4
+
5
+ `@0k/pdf-viewer` is a plain-TypeScript in-page PDF viewer. It owns the
6
+ toolbar, the page canvas, paging, zoom, pinch and pan gestures, the fit
7
+ policy, error and retry, accessible controls, scoped styles and
8
+ single-owner teardown. Hosts own the PDF URL and its access policy,
9
+ where the viewer mounts, the affordance around it, and translated
10
+ labels.
11
+
12
+ It renders on `@0k/pdfjs-legacy-runtime`, which owns the PDF.js
13
+ compatibility policy (exact `pdfjs-dist` 3.11.174 pin, legacy build,
14
+ worker wiring, single-owner teardown type). This package never imports
15
+ `pdfjs-dist`.
16
+
17
+ MIT licensed.
18
+
19
+ ## Install
20
+
21
+ ``` bash
22
+ npm install @0k/pdf-viewer
23
+ ```
24
+
25
+ npm ≥7 installs the exact peer `@0k/pdfjs-legacy-runtime` automatically.
26
+ The host's bundler sees one copy of the runtime and emits one PDF.js
27
+ worker asset.
28
+
29
+ ## Usage
30
+
31
+ ``` typescript
32
+ import { mountPdfViewer } from '@0k/pdf-viewer';
33
+ import '@0k/pdf-viewer/style.css';
34
+
35
+ const owner = mountPdfViewer(host, {
36
+ url: 'https://example.com/manual.pdf', // PDF.js range-loads it
37
+ fit: 'width', // or 'page'
38
+ interaction: 'scroll', // in-flow; 'gestures' (default) for full screen
39
+ labels: { previous: 'Page précédente' },
40
+ });
41
+ owner.next();
42
+ await owner.destroy(); // releases the worker; idempotent
43
+ ```
44
+
45
+ The host decides when to mount (lazily, on an "open" click, so a
46
+ collapsed widget costs nothing) and where; the viewer fills its host
47
+ element and is in-flow. A host wanting a full-screen reader wraps it in
48
+ its own overlay.
49
+
50
+ ## Options
51
+
52
+ `mountPdfViewer(host, options)` mounts into an `HTMLElement` and starts
53
+ loading. The required `url` is passed verbatim to PDF.js; hosts own
54
+ access policy, including any cross-origin permissions. Optional fields:
55
+
56
+ - `fit`: `"width"` (default) fills the surface width; `"page"` fits the
57
+ whole page within the surface box using the smaller width/height
58
+ ratio. Both policies follow surface size changes automatically; resize
59
+ bursts are debounced before the page is re-rendered.
60
+ - `zoom`: an object with `min` (0.5), `max` (2), `step` (0.25) and
61
+ `initial` (1). Values are fractions of the fitted size; the initial
62
+ value and subsequent zoom changes are clamped to the bounds.
63
+ - `maxDpr`: backing-canvas device pixel ratio cap, default 2.
64
+ - `interaction`: `"gestures"` (default) owns drag-to-pan, pinch and any
65
+ wheel zoom, and supports double-click reset. `"scroll"` leaves
66
+ one-finger swipes and bare wheel scrolling to the browser while pinch
67
+ and Ctrl/⌘+wheel zoom. `"none"` leaves only the toolbar controls
68
+ active.
69
+ - `labels`: partial overrides of the English defaults below.
70
+ - `onStateChange`: callback receiving a `PdfViewerState` snapshot when
71
+ loading starts, a raster commits, or an error occurs. The snapshot has
72
+ `status` (`"loading"`, `"ready"` or `"error"`), `page`, `pageCount`,
73
+ `zoom` and `error` (a string or `null`). A ready document can still be
74
+ awaiting its first raster; the canvas stays hidden until that commit.
75
+
76
+ The zoom live region changes when a gesture settles (last pointer lifted
77
+ or 250 ms of wheel quiet), not during movement. Toolbar zoom changes the
78
+ readout immediately.
79
+
80
+ ### Labels
81
+
82
+ `labels` accepts any subset of `PdfViewerLabels`:
83
+
84
+ | Key | English default |
85
+ |-------------|-------------------------------|
86
+ | `controls` | PDF controls |
87
+ | `previous` | Previous page |
88
+ | `next` | Next page |
89
+ | `zoomOut` | Zoom out |
90
+ | `zoomIn` | Zoom in |
91
+ | `zoomReset` | Reset zoom |
92
+ | `reset` | Reset zoom and pan |
93
+ | `retry` | Retry |
94
+ | `loading` | Loading… |
95
+ | `failed` | Failed to load PDF: %{reason} |
96
+
97
+ The `%{reason}` placeholder in `failed` is replaced by the literal error
98
+ message. Labels are rendered as text, not HTML; dollar sequences in an
99
+ error reason are preserved.
100
+
101
+ ### Owner handle
102
+
103
+ `mountPdfViewer` returns a `PdfViewerOwner` with a readonly `host` and:
104
+
105
+ - `state()`: return the current state snapshot, including live gesture
106
+ zoom.
107
+ - `goTo(page)`: floor and clamp the page number to the document range.
108
+ - `next()` and `previous()`: move one page within that range.
109
+ - `setZoom(zoom)`: clamp zoom, update the readout and re-render.
110
+ - `resetView()`: restore zoom 1 (clamped to the bounds), not `initial`,
111
+ and clear pan.
112
+ - `reload()`: return a promise for releasing the current document and
113
+ loading the same URL again. Superseded loads cannot overwrite a newer
114
+ load; startup waits for preceding task teardowns to settle.
115
+ - `destroy()`: remove the DOM and return a promise for worker teardown.
116
+ Idempotent, including before loading completes.
117
+
118
+ Paging and zoom methods do nothing unless the document is ready.
119
+ Mounting again on the same host replaces its viewer. Across all hosts
120
+ using this module, every worker startup waits for all pending
121
+ loading-task teardowns to settle, whether resolved or rejected. A host
122
+ can call an old owner's `destroy()` without awaiting it and mount on a
123
+ new element: the new worker will not overlap that pending teardown. This
124
+ also applies to `reload()`. Only pending teardowns are retained; settled
125
+ ones are removed. A failed teardown is logged and does not block
126
+ startup, but may leave its worker running. A slow teardown delays
127
+ startup of every later viewer in the module (across all hosts); a
128
+ teardown that never settles holds them back indefinitely. An old owner
129
+ cannot destroy a replacement or a viewer on another host.
130
+
131
+ ### Exported constants
132
+
133
+ - `PDF_VIEWER_CLASS`: `"pdf-viewer"`, the viewer root class.
134
+ - `PDF_VIEWER_MIN_ZOOM`: 0.5.
135
+ - `PDF_VIEWER_MAX_ZOOM`: 2.
136
+ - `PDF_VIEWER_ZOOM_STEP`: 0.25.
137
+ - `PDF_VIEWER_DEFAULT_ZOOM`: 1.
138
+ - `PDF_VIEWER_MAX_DPR`: 2.
139
+ - `PDF_VIEWER_WHEEL_FACTOR`: 0.9 per 100 CSS pixels of wheel-down
140
+ movement; wheel-up reverses the factor. Line deltas use 16 CSS pixels
141
+ per line, and page deltas use the surface's client height.
142
+ - `PDF_VIEWER_WHEEL_SETTLE_MS`: 250.
143
+ - `DEFAULT_PDF_VIEWER_LABELS`: the English label object above.
144
+
145
+ ## Theming
146
+
147
+ Import `@0k/pdf-viewer/style.css` separately; importing the JavaScript
148
+ does not inject styles. Override these properties on the `.pdf-viewer`
149
+ root with a selector loaded after the package stylesheet:
150
+
151
+ | Property | Default |
152
+ |----------------------------------------|---------------------------|
153
+ | `--pdf-viewer-surface` | transparent |
154
+ | `--pdf-viewer-page-background` | \#fff |
155
+ | `--pdf-viewer-page-border` | rgba(128, 128, 128, 0.2) |
156
+ | `--pdf-viewer-page-radius` | 4px |
157
+ | `--pdf-viewer-surface-max-height` | none |
158
+ | `--pdf-viewer-button-size` | 2.25rem |
159
+ | `--pdf-viewer-button-radius` | 6px |
160
+ | `--pdf-viewer-button-background` | rgba(128, 128, 128, 0.12) |
161
+ | `--pdf-viewer-button-background-hover` | rgba(128, 128, 128, 0.24) |
162
+ | `--pdf-viewer-button-border` | rgba(128, 128, 128, 0.25) |
163
+ | `--pdf-viewer-button-color` | inherit |
164
+ | `--pdf-viewer-focus` | rgba(118, 227, 234, 0.6) |
165
+ | `--pdf-viewer-info` | inherit |
166
+ | `--pdf-viewer-error` | rgb(220, 90, 90) |
167
+ | `--pdf-viewer-error-background` | rgba(239, 68, 68, 0.15) |
168
+ | `--pdf-viewer-error-border` | rgba(239, 68, 68, 0.35) |
169
+ | `--pdf-viewer-reset-background` | rgba(0, 0, 0, 0.55) |
170
+ | `--pdf-viewer-reset-background-hover` | rgba(0, 0, 0, 0.75) |
171
+ | `--pdf-viewer-reset-color` | \#fff |
172
+ | `--pdf-viewer-gap` | 6px |
173
+
174
+ Toolbar button hit areas retain a 44px minimum regardless of button
175
+ size. The viewer fills its host's width and grows with the page unless
176
+ bounded. Set `--pdf-viewer-surface-max-height` (for example `60vh`) to
177
+ cap the surface, or make the root a flex child with a bounded height.
178
+ The surface can shrink (`min-height: 0`); toolbar and status do not
179
+ flex. Scroll and toolbar-only modes use native overflow scrolling;
180
+ gesture mode hides overflow and pans with a transform. Fit-to-page uses
181
+ the resulting surface box, so a full-screen host must provide its own
182
+ height constraint.
183
+
184
+ ## Security
185
+
186
+ The pinned `pdfjs-dist` 3.11.174 is affected by CVE-2024-4367: a crafted
187
+ font can execute JavaScript when evaluation is enabled. The [runtime's
188
+ security note](https://github.com/0k/pdfjs-legacy-runtime#security)
189
+ explains its mitigation: every document is loaded with
190
+ `isEvalSupported: false`, applied after caller options so it cannot be
191
+ re-enabled. A Content-Security-Policy without `'unsafe-eval'` provides a
192
+ second layer. The version remains pinned for Chrome 91 compatibility;
193
+ the mitigation does not remove the version-based `npm audit` advisory.
194
+
195
+ PDF.js also has an optional `canvas` dependency for Node.js rendering.
196
+ Its install tooling pulls in `tar` advisories; it does not enter the
197
+ browser bundle. Browser-only hosts can skip optional dependencies with
198
+ `npm install --omit=optional`, or use an npm `overrides` entry to select
199
+ a patched transitive `tar` version compatible with their install
200
+ tooling. The latter changes installation tooling, not the browser PDF
201
+ runtime.
202
+
203
+ ## Development
204
+
205
+ ``` bash
206
+ npm install
207
+ npm run check # unit tests, strict typecheck, build, Chrome 91 audit
208
+ ```
209
+
210
+ With Python Playwright and system Chrome available,
211
+ `npm run test:browser` bundles the built package in a temporary host and
212
+ verifies real PDF.js worker loading, raster pixels, rapid
213
+ zoom/reload/replacement, CSS, teardown, scroll reachability, fitted
214
+ borders and gesture-settle positions.
215
+
216
+ To try an unreleased change in a host, link the checkout there:
217
+ `npm link <path-to-pdf-viewer>`.
218
+
219
+
220
+ ## Changelog
221
+
222
+
223
+ ### 0.2.1 (2026-11-06)
224
+
225
+ #### Fix
226
+
227
+ * Start a viewer's ``PDF.js`` worker only after all pending teardowns on any host settle. [Valentin Lab]
228
+
229
+ Hosts that mount each document on a new element can switch documents
230
+ without awaiting the old owner: the next worker now waits for every
231
+ loading-task teardown already under way in the module, whatever its
232
+ host. Switching documents no longer runs the old and the new worker
233
+ side by side, which bounds worker memory on phones.
234
+
235
+ A failed teardown is logged and does not block startup, but ``PDF.js`` may
236
+ leave its worker running. A slow teardown delays every later viewer of
237
+ the module; one that never settles holds them back. Settled teardowns,
238
+ resolved or rejected, are dropped from the module-wide tracking, and
239
+ reload ordering, same-host replacement and owner isolation are
240
+ preserved.
241
+
242
+
243
+ ### 0.2.0 (2026-10-28)
244
+
245
+ #### New
246
+
247
+ * Add the PDF page viewer with paging, zoom, gestures, fit, retry and teardown. [Valentin Lab]
248
+
249
+ ``mountPdfViewer(host, options)`` mounts a toolbar and a page canvas in the
250
+ host element and returns an owner handle: the ``host`` it mounted in, and
251
+ ``state``, ``goTo``, ``next``, ``previous``, ``setZoom``, ``resetView``,
252
+ ``reload`` and ``destroy``. The host passes a URL; the PDF reader
253
+ range-loads it through ``@0k/pdfjs-legacy-runtime``. An ``interaction``
254
+ option chooses full gestures (pinch, pan, wheel zoom), in-flow scrolling with
255
+ pinch zoom only, or the toolbar only (``none``). Load and render errors show
256
+ a retry control. Labels default to English and are configurable. Styles
257
+ are scoped under the viewer root, themed only through ``--pdf-viewer-*``
258
+ custom properties, and imported by the host from
259
+ ``@0k/pdf-viewer/style.css``.
260
+
261
+ Contract:
262
+ - The loading task is the only owner of worker teardown. A reload, or a
263
+ second mount on the same host, starts its worker only after every
264
+ previous teardown settled; a superseded load never overwrites a newer
265
+ one; destroy releases the canvas storage.
266
+ - The owner handle is a frozen facade, safe to keep in a host's reactive
267
+ state.
268
+ - The page on screen is never cleaned up, and pages render off-screen and
269
+ are committed in one step, so Android WebView 91 never shows a blank
270
+ canvas.
271
+ - Fitting accounts for the surface padding and borders and follows surface
272
+ resizes and the appearance of scrollbars, and settles when a scrollbar
273
+ toggles; a zoomed page stays reachable by scrolling in scroll and
274
+ toolbar-only modes.
275
+ - Wheel zoom scales with the scrolled distance and re-renders once, even
276
+ when a pointer press interrupts it. A settling gesture keeps the page in
277
+ place, and the zoom readout changes once the gesture settles (at once for
278
+ toolbar zoom).
279
+ - Error reasons are shown verbatim, an undefined label keeps its English
280
+ default, and a throwing ``onStateChange`` callback is logged without
281
+ affecting the viewer.
282
+
2
283
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
@@ -0,0 +1,109 @@
1
+ /** Class of the viewer root; every style is scoped under it. */
2
+ export declare const PDF_VIEWER_CLASS = "pdf-viewer";
3
+ /** Default zoom bounds and toolbar step (a fraction of the fitted size). */
4
+ export declare const PDF_VIEWER_MIN_ZOOM = 0.5;
5
+ export declare const PDF_VIEWER_MAX_ZOOM = 2;
6
+ export declare const PDF_VIEWER_ZOOM_STEP = 0.25;
7
+ export declare const PDF_VIEWER_DEFAULT_ZOOM = 1;
8
+ /**
9
+ * Cap on the effective device pixel ratio used to size the backing
10
+ * canvas. 2 is sharp enough while bounding memory on a 3× phone.
11
+ */
12
+ export declare const PDF_VIEWER_MAX_DPR = 2;
13
+ /** Zoom factor for 100 CSS pixels of wheel-down movement. */
14
+ export declare const PDF_VIEWER_WHEEL_FACTOR = 0.9;
15
+ /** Milliseconds of quiet that end a wheel burst — a wheel has no "up". */
16
+ export declare const PDF_VIEWER_WHEEL_SETTLE_MS = 250;
17
+ export interface PdfViewerLabels {
18
+ /** `aria-label` of the toolbar. */
19
+ readonly controls: string;
20
+ readonly previous: string;
21
+ readonly next: string;
22
+ readonly zoomOut: string;
23
+ readonly zoomIn: string;
24
+ /** Back to the fitted size (toolbar). */
25
+ readonly zoomReset: string;
26
+ /** Back to the resting view after a gesture (floating control). */
27
+ readonly reset: string;
28
+ readonly retry: string;
29
+ readonly loading: string;
30
+ /** Failure sentence; `%{reason}` is replaced by the error message. */
31
+ readonly failed: string;
32
+ }
33
+ export declare const DEFAULT_PDF_VIEWER_LABELS: PdfViewerLabels;
34
+ /**
35
+ * How the resting (100 %) zoom relates to the surface:
36
+ * - `width`: the page fills the surface width; taller pages scroll.
37
+ * Right for an in-flow reader in a prose column.
38
+ * - `page`: the whole page fits the surface box (smaller ratio wins).
39
+ * Right for a full-screen reader, where cropping a scanned page would
40
+ * hide the margin a stamp or signature sits in.
41
+ */
42
+ export type PdfViewerFit = "width" | "page";
43
+ export type PdfViewerInteraction = "gestures" | "scroll" | "none";
44
+ export type PdfViewerStatus = "loading" | "ready" | "error";
45
+ export interface PdfViewerState {
46
+ readonly status: PdfViewerStatus;
47
+ readonly page: number;
48
+ readonly pageCount: number;
49
+ readonly zoom: number;
50
+ readonly error: string | null;
51
+ }
52
+ export interface PdfViewerOptions {
53
+ /** The document URL, handed verbatim to PDF.js (range-loaded). */
54
+ readonly url: string;
55
+ readonly fit?: PdfViewerFit;
56
+ readonly labels?: {
57
+ readonly [K in keyof PdfViewerLabels]?: string | undefined;
58
+ };
59
+ readonly zoom?: {
60
+ readonly min?: number;
61
+ readonly max?: number;
62
+ readonly step?: number;
63
+ readonly initial?: number;
64
+ };
65
+ readonly maxDpr?: number;
66
+ /**
67
+ * How the page surface reacts to pointers and the wheel:
68
+ * - `gestures` (default): the surface owns every gesture — drag pans,
69
+ * pinch and any wheel zoom. Right for a full-screen reader, where
70
+ * there is nothing else to scroll.
71
+ * - `scroll`: the BROWSER keeps scrolling (one-finger swipe, bare
72
+ * wheel), so a reader embedded in a scrolling page never traps the
73
+ * reader's finger; pinch and Ctrl/⌘+wheel (what a touchpad pinch
74
+ * emits) still zoom the page.
75
+ * - `none`: toolbar only.
76
+ */
77
+ readonly interaction?: PdfViewerInteraction;
78
+ readonly onStateChange?: (state: PdfViewerState) => void;
79
+ }
80
+ /** The handle a host keeps for the viewer it mounted. */
81
+ export interface PdfViewerOwner {
82
+ readonly host: HTMLElement;
83
+ state(): PdfViewerState;
84
+ goTo(page: number): void;
85
+ next(): void;
86
+ previous(): void;
87
+ setZoom(zoom: number): void;
88
+ /** Restore the resting view: zoom 1, no pan. */
89
+ resetView(): void;
90
+ /** Release the current document (if any) and load the URL again. */
91
+ reload(): Promise<void>;
92
+ /**
93
+ * Release the loading task (the worker), remove the viewer DOM and
94
+ * stop listening. Idempotent; safe on a viewer that never loaded.
95
+ */
96
+ destroy(): Promise<void>;
97
+ }
98
+ /**
99
+ * Mount a viewer into `host` and start loading `options.url`.
100
+ *
101
+ * Mounting twice on one host replaces the previous viewer. On any host,
102
+ * worker startup waits for every pending loading-task teardown in this
103
+ * module to settle, including rejected teardowns. Hosts need not await
104
+ * an old owner's destroy() before mounting on a new element. The returned
105
+ * owner acts only on the viewer it mounted. A failed teardown is logged
106
+ * and allows startup, but may leave its worker running. A slow or never
107
+ * settling teardown delays or blocks startup across all hosts.
108
+ */
109
+ export declare function mountPdfViewer(host: HTMLElement, options: PdfViewerOptions): PdfViewerOwner;