reflex-pdfslick 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,268 @@
1
+ /**
2
+ * PdfSlickWrapper — local React wrapper around `@pdfslick/react`.
3
+ *
4
+ * PDFSlick's public surface is the hook `usePDFSlick(url, options)`, not a
5
+ * drop-in component, and `<PDFSlickThumbnails>` uses a render-prop child that
6
+ * cannot be expressed from Python. This thin wrapper calls the hook internally,
7
+ * renders `<PDFSlickViewer>` (and optional thumbnails), subscribes to the
8
+ * Zustand store via `usePDFSlickStore`, and exposes a clean prop/event API for
9
+ * the Reflex `PdfSlick` component to bind to.
10
+ *
11
+ * It is browser-only (PDF.js), so the Reflex side wraps it as a NoSSRComponent.
12
+ *
13
+ * Responsibilities by phase:
14
+ * - Phase 1: call the hook, render the viewer, import CSS.
15
+ * - Phase 2: subscribe to store slices, forward serializable scalars to Reflex
16
+ * events (page, scale, load, error, metadata). Only serializable values
17
+ * cross the boundary; Dates become ISO strings, Maps become arrays.
18
+ * - Phase 3: apply declarative `command` objects (goto/zoom/rotate/modes/
19
+ * print/download) against the imperative `pdfSlick` instance, apply-once.
20
+ * - Phase 4: render an optional thumbnails sidebar (vertical or horizontal).
21
+ */
22
+ import * as React from "react";
23
+ import { GlobalWorkerOptions } from "pdfjs-dist";
24
+ // Vite resolves `?url` to a served/hashed asset URL for the worker file. This
25
+ // uses the SAME hoisted `pdfjs-dist` instance that `@pdfslick/core` imports, so
26
+ // the worker version always matches the PDF.js API version.
27
+ import pdfWorkerUrl from "pdfjs-dist/build/pdf.worker.min.mjs?url";
28
+ import { usePDFSlick, PDFSlickViewer, PDFSlickThumbnails } from "@pdfslick/react";
29
+ import "@pdfslick/react/dist/pdf_viewer.css";
30
+
31
+ // PDF.js worker fix. `@pdfslick/core` sets
32
+ // GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url)
33
+ // at module load, which resolves RELATIVE to the core module directory
34
+ // (.../@pdfslick/core/dist/esm/pdfjs-dist/build/...) — a path that does not
35
+ // exist under Reflex's Vite server, so the worker fails to load ("Setting up
36
+ // fake worker failed"). All ES imports above are evaluated before this module
37
+ // body runs, so core's (broken) assignment has already happened; we override it
38
+ // here with the correctly-resolved worker URL on the same GlobalWorkerOptions.
39
+ if (pdfWorkerUrl) {
40
+ GlobalWorkerOptions.workerSrc = pdfWorkerUrl as string;
41
+ }
42
+
43
+ type PdfCommand = {
44
+ action?: string;
45
+ page?: number;
46
+ value?: number | string;
47
+ mode?: number;
48
+ degrees?: number;
49
+ };
50
+
51
+ export type PdfSlickWrapperProps = {
52
+ url: string;
53
+ scaleValue?: string;
54
+ singlePageViewer?: boolean;
55
+ removePageBorders?: boolean;
56
+ thumbnailWidth?: number;
57
+ showThumbnails?: boolean;
58
+ horizontalThumbnails?: boolean;
59
+ command?: PdfCommand | null;
60
+ onDocumentLoad?: (numPages: number) => void;
61
+ onPageChange?: (page: number) => void;
62
+ onScaleChange?: (scale: number) => void;
63
+ onError?: (err: { name: string; message: string }) => void;
64
+ onMetadata?: (meta: Record<string, unknown>) => void;
65
+ };
66
+
67
+ /** Convert a possible Date to an ISO string; pass through null/undefined. */
68
+ function toIso(value: unknown): string | null {
69
+ if (value instanceof Date && !isNaN(value.getTime())) return value.toISOString();
70
+ if (typeof value === "string" && value) return value;
71
+ return null;
72
+ }
73
+
74
+ /** Normalize an unknown error into a serializable {name, message} object. */
75
+ function normalizeError(error: unknown): { name: string; message: string } {
76
+ if (error && typeof error === "object") {
77
+ const e = error as { name?: string; message?: string };
78
+ return { name: e.name ?? "Error", message: e.message ?? String(error) };
79
+ }
80
+ return { name: "Error", message: String(error) };
81
+ }
82
+
83
+ export function PdfSlickWrapper({
84
+ url,
85
+ scaleValue = "page-fit",
86
+ singlePageViewer = false,
87
+ removePageBorders = false,
88
+ thumbnailWidth = 212,
89
+ showThumbnails = false,
90
+ horizontalThumbnails = false,
91
+ command = null,
92
+ onDocumentLoad,
93
+ onPageChange,
94
+ onScaleChange,
95
+ onError,
96
+ onMetadata,
97
+ }: PdfSlickWrapperProps) {
98
+ const { viewerRef, thumbsRef, usePDFSlickStore, error } = usePDFSlick(url, {
99
+ scaleValue,
100
+ singlePageViewer,
101
+ removePageBorders,
102
+ thumbnailWidth,
103
+ });
104
+
105
+ const pageNumber = usePDFSlickStore((s) => s.pageNumber);
106
+ const numPages = usePDFSlickStore((s) => s.numPages);
107
+ const scale = usePDFSlickStore((s) => s.scale);
108
+ const isLoaded = usePDFSlickStore((s) => s.isDocumentLoaded);
109
+ const pdfSlick = usePDFSlickStore((s) => s.pdfSlick);
110
+ const store = usePDFSlickStore((s) => s);
111
+
112
+ // --- Phase 2: forward store changes to Reflex events --------------------
113
+ React.useEffect(() => {
114
+ if (!isLoaded) return;
115
+ onDocumentLoad?.(numPages);
116
+ if (onMetadata) {
117
+ onMetadata({
118
+ numPages,
119
+ title: store.title ?? "",
120
+ author: store.author ?? "",
121
+ subject: store.subject ?? "",
122
+ creator: store.creator ?? "",
123
+ producer: store.producer ?? "",
124
+ version: store.version ?? "",
125
+ filename: store.filename ?? "",
126
+ filesize: store.filesize ?? 0,
127
+ isLinearized: !!store.isLinearized,
128
+ creationDate: toIso(store.creationDate),
129
+ modificationDate: toIso(store.modificationDate),
130
+ });
131
+ }
132
+ // eslint-disable-next-line react-hooks/exhaustive-deps
133
+ }, [isLoaded, numPages]);
134
+
135
+ React.useEffect(() => {
136
+ onPageChange?.(pageNumber);
137
+ // eslint-disable-next-line react-hooks/exhaustive-deps
138
+ }, [pageNumber]);
139
+
140
+ React.useEffect(() => {
141
+ onScaleChange?.(scale);
142
+ // eslint-disable-next-line react-hooks/exhaustive-deps
143
+ }, [scale]);
144
+
145
+ React.useEffect(() => {
146
+ if (error) onError?.(normalizeError(error));
147
+ // eslint-disable-next-line react-hooks/exhaustive-deps
148
+ }, [error]);
149
+
150
+ // --- Phase 3: apply declarative commands once ---------------------------
151
+ const lastCommandKey = React.useRef<string>("");
152
+ React.useEffect(() => {
153
+ if (!command || !command.action || !pdfSlick) return;
154
+ const key = JSON.stringify(command);
155
+ if (key === lastCommandKey.current) return;
156
+ lastCommandKey.current = key;
157
+
158
+ const ps = pdfSlick as any;
159
+ const current = store.pageNumber ?? 1;
160
+ const total = store.numPages ?? 1;
161
+ switch (command.action) {
162
+ case "goto":
163
+ if (typeof command.page === "number") ps.gotoPage(command.page);
164
+ break;
165
+ case "next":
166
+ ps.gotoPage(Math.min(total, current + 1));
167
+ break;
168
+ case "prev":
169
+ ps.gotoPage(Math.max(1, current - 1));
170
+ break;
171
+ case "zoomIn":
172
+ (ps.viewer ?? ps).increaseScale?.();
173
+ break;
174
+ case "zoomOut":
175
+ (ps.viewer ?? ps).decreaseScale?.();
176
+ break;
177
+ case "zoomPreset":
178
+ if (command.value != null) ps.currentScaleValue = String(command.value);
179
+ break;
180
+ case "zoomValue":
181
+ if (command.value != null) ps.currentScale = Number(command.value);
182
+ break;
183
+ case "rotate":
184
+ if (typeof command.degrees === "number") {
185
+ const base = store.pagesRotation ?? 0;
186
+ ps.setRotation(base + command.degrees);
187
+ }
188
+ break;
189
+ case "scrollMode":
190
+ if (typeof command.mode === "number") ps.setScrollMode(command.mode);
191
+ break;
192
+ case "spreadMode":
193
+ if (typeof command.mode === "number") ps.setSpreadMode(command.mode);
194
+ break;
195
+ case "print":
196
+ ps.triggerPrinting?.();
197
+ break;
198
+ case "download":
199
+ ps.downloadOrSave?.();
200
+ break;
201
+ default:
202
+ break;
203
+ }
204
+ // eslint-disable-next-line react-hooks/exhaustive-deps
205
+ }, [command, pdfSlick]);
206
+
207
+ if (error) {
208
+ return (
209
+ <div className="pdfSlick" style={{ padding: "1rem", color: "#b91c1c" }}>
210
+ Failed to load PDF: {normalizeError(error).message}
211
+ </div>
212
+ );
213
+ }
214
+
215
+ const horizontal = showThumbnails && horizontalThumbnails;
216
+ const thumbsPanel = showThumbnails ? (
217
+ <div
218
+ ref={thumbsRef as any}
219
+ className="pdfSlickThumbs"
220
+ style={
221
+ horizontal
222
+ ? { height: 160, width: "100%", overflowX: "auto", display: "flex", gap: 8, padding: 8 }
223
+ : { width: 240, height: "100%", overflowY: "auto", padding: 8 }
224
+ }
225
+ >
226
+ <PDFSlickThumbnails {...{ thumbsRef, usePDFSlickStore }}>
227
+ {({ pageNumber: pn, src, pageLabel, loaded }) => (
228
+ <div
229
+ key={pn}
230
+ onClick={() => pdfSlick?.gotoPage(pn)}
231
+ style={{
232
+ cursor: "pointer",
233
+ padding: 4,
234
+ outline: pn === pageNumber ? "2px solid #2563eb" : "1px solid #e5e7eb",
235
+ borderRadius: 4,
236
+ }}
237
+ >
238
+ {loaded && src ? (
239
+ <img src={src} alt={`Page ${pageLabel ?? pn}`} style={{ display: "block", width: "100%" }} />
240
+ ) : (
241
+ <div style={{ height: horizontal ? 120 : 260, width: 160, background: "#e5e7eb" }} />
242
+ )}
243
+ </div>
244
+ )}
245
+ </PDFSlickThumbnails>
246
+ </div>
247
+ ) : null;
248
+
249
+ return (
250
+ <div
251
+ className="pdfSlick"
252
+ style={{
253
+ position: "absolute",
254
+ inset: 0,
255
+ display: "flex",
256
+ flexDirection: horizontal ? "column" : "row",
257
+ }}
258
+ >
259
+ {showThumbnails && !horizontal && thumbsPanel}
260
+ {showThumbnails && horizontal && thumbsPanel}
261
+ <div style={{ flex: 1, position: "relative" }}>
262
+ <PDFSlickViewer {...{ viewerRef, usePDFSlickStore }} />
263
+ </div>
264
+ </div>
265
+ );
266
+ }
267
+
268
+ export default PdfSlickWrapper;
@@ -0,0 +1,31 @@
1
+ """reflex-pdfslick — a native Reflex component wrapping PDFSlick (PDF.js + Zustand).
2
+
3
+ Public API (stable surface):
4
+
5
+ from reflex_pdfslick import pdf_slick, PdfSlick
6
+ from reflex_pdfslick import ScrollMode, SpreadMode, ScaleValue
7
+ from reflex_pdfslick import PdfSlickOptions
8
+ from reflex_pdfslick import commands
9
+
10
+ ``pdf_slick(url=...)`` renders a reactive PDF viewer; viewer state flows into
11
+ Reflex via events (``on_page_change``, ``on_document_load``, ``on_scale_change``,
12
+ ``on_error``, ``on_metadata``) and Python drives the viewer through the
13
+ declarative ``command`` prop built with the ``commands`` helpers.
14
+ """
15
+
16
+ from . import commands
17
+ from .models import ScaleValue, ScrollMode, SpreadMode
18
+ from .options import PdfSlickOptions
19
+ from .pdfslick import PdfSlick, pdf_slick
20
+
21
+ __all__ = [
22
+ "PdfSlick",
23
+ "pdf_slick",
24
+ "PdfSlickOptions",
25
+ "ScaleValue",
26
+ "ScrollMode",
27
+ "SpreadMode",
28
+ "commands",
29
+ ]
30
+
31
+ __version__ = "0.0.1"
@@ -0,0 +1,98 @@
1
+ """Declarative imperative-control commands for ``PdfSlick``.
2
+
3
+ PDFSlick's imperative actions live on the ``pdfSlick`` store instance, which
4
+ stays in JS. To drive them from Python we send a small JSON ``command`` object
5
+ into the component's ``command`` prop; the JS wrapper watches it and applies the
6
+ matching action once (apply-then-clear by command identity).
7
+
8
+ These helpers build those objects so app code never hand-writes the action
9
+ strings. Bind the result to ``PdfSlick(..., command=State.command)`` and set
10
+ ``State.command`` from an event handler::
11
+
12
+ @rx.event
13
+ def go_next(self):
14
+ self.command = commands.next_page()
15
+
16
+ The action vocabulary mirrors ``specs/api/component-api.md`` §4 and the switch in
17
+ ``js/PdfSlickWrapper.tsx``. ``None`` is the idle/cleared value (no-op).
18
+
19
+ References:
20
+ - ``specs/api/component-api.md`` §4
21
+ - ``specs/technical/architecture.md`` §4.5
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ from typing import Any
27
+
28
+ Command = dict[str, Any]
29
+
30
+
31
+ def goto_page(page: int) -> Command:
32
+ """Jump to a 1-based page number (``pdfSlick.gotoPage(page)``)."""
33
+ return {"action": "goto", "page": int(page)}
34
+
35
+
36
+ def next_page() -> Command:
37
+ """Go to the next page (clamped to the last page)."""
38
+ return {"action": "next"}
39
+
40
+
41
+ def prev_page() -> Command:
42
+ """Go to the previous page (clamped to the first page)."""
43
+ return {"action": "prev"}
44
+
45
+
46
+ def zoom_in() -> Command:
47
+ """Step zoom in (``viewer.increaseScale()``)."""
48
+ return {"action": "zoomIn"}
49
+
50
+
51
+ def zoom_out() -> Command:
52
+ """Step zoom out (``viewer.decreaseScale()``)."""
53
+ return {"action": "zoomOut"}
54
+
55
+
56
+ def set_zoom_preset(preset: str) -> Command:
57
+ """Set a named zoom preset (``pdfSlick.currentScaleValue = preset``).
58
+
59
+ ``preset`` is one of ``"auto" | "page-actual" | "page-fit" | "page-width"``
60
+ (see :class:`~reflex_pdfslick.ScaleValue`).
61
+ """
62
+ return {"action": "zoomPreset", "value": str(preset)}
63
+
64
+
65
+ def set_zoom_value(scale: float) -> Command:
66
+ """Set an absolute numeric zoom (``pdfSlick.currentScale = scale``)."""
67
+ return {"action": "zoomValue", "value": float(scale)}
68
+
69
+
70
+ def rotate(degrees: int) -> Command:
71
+ """Rotate pages by a relative delta in degrees (e.g. ``90`` / ``-90``)."""
72
+ return {"action": "rotate", "degrees": int(degrees)}
73
+
74
+
75
+ def set_scroll_mode(mode: int) -> Command:
76
+ """Set the scroll mode (``pdfSlick.setScrollMode``).
77
+
78
+ Accepts a :class:`~reflex_pdfslick.ScrollMode` or a plain int.
79
+ """
80
+ return {"action": "scrollMode", "mode": int(mode)}
81
+
82
+
83
+ def set_spread_mode(mode: int) -> Command:
84
+ """Set the spread mode (``pdfSlick.setSpreadMode``).
85
+
86
+ Accepts a :class:`~reflex_pdfslick.SpreadMode` or a plain int.
87
+ """
88
+ return {"action": "spreadMode", "mode": int(mode)}
89
+
90
+
91
+ def print_document() -> Command:
92
+ """Open the print dialog (``pdfSlick.triggerPrinting()``)."""
93
+ return {"action": "print"}
94
+
95
+
96
+ def download() -> Command:
97
+ """Download / save the document (``pdfSlick.downloadOrSave()``)."""
98
+ return {"action": "download"}
@@ -0,0 +1,47 @@
1
+ """Enums and value types mirroring PDFSlick / PDF.js constants.
2
+
3
+ These mirror the numeric enums re-exported by ``@pdfslick/react`` (originally
4
+ from PDF.js ``ui_utils``) and the accepted ``scaleValue`` strings. Keeping them
5
+ in Python lets the demo app and downstream users avoid magic numbers.
6
+
7
+ References:
8
+ - ``specs/data-model/store-schema.md``
9
+ - ``specs/api/component-api.md``
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from enum import Enum, IntEnum
15
+
16
+
17
+ class ScrollMode(IntEnum):
18
+ """PDF.js scroll modes (values match ``pdfjs-dist`` ``ScrollMode``)."""
19
+
20
+ VERTICAL = 0
21
+ HORIZONTAL = 1
22
+ WRAPPED = 2
23
+ PAGE = 3
24
+
25
+
26
+ class SpreadMode(IntEnum):
27
+ """PDF.js spread modes (values match ``pdfjs-dist`` ``SpreadMode``)."""
28
+
29
+ NONE = 0
30
+ ODD = 1
31
+ EVEN = 2
32
+
33
+
34
+ class ScaleValue(str, Enum):
35
+ """Named zoom presets accepted by PDFSlick's ``scaleValue`` option.
36
+
37
+ A numeric string (e.g. ``"1.5"``) is also valid and maps to an absolute
38
+ scale; use it directly as a plain ``str`` where a preset is not enough.
39
+ """
40
+
41
+ AUTO = "auto"
42
+ PAGE_ACTUAL = "page-actual"
43
+ PAGE_FIT = "page-fit"
44
+ PAGE_WIDTH = "page-width"
45
+
46
+ def __str__(self) -> str: # pragma: no cover - convenience
47
+ return self.value
@@ -0,0 +1,41 @@
1
+ """Typed PDFSlick options object.
2
+
3
+ Mirrors the ``PDFSlickOptions`` type from ``@pdfslick/core@4.x``. Subclassing
4
+ ``rx.PropsBase`` means snake_case Python fields are auto-converted to the
5
+ camelCase keys PDFSlick expects when serialized to a JS object prop.
6
+
7
+ Only the most commonly used options are surfaced as first-class fields; the full
8
+ upstream option set is documented in ``specs/api/component-api.md``. Unknown or
9
+ advanced keys can still be passed through ``extra``.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from typing import Any, Optional
15
+
16
+ import reflex as rx
17
+
18
+
19
+ class PdfSlickOptions(rx.PropsBase):
20
+ """Options forwarded to ``usePDFSlick(url, options)``.
21
+
22
+ Field names are snake_case in Python and emitted as camelCase to JS, e.g.
23
+ ``single_page_viewer`` -> ``singlePageViewer``.
24
+ """
25
+
26
+ scale_value: Optional[str] = None # "auto" | "page-fit" | "page-width" | "page-actual" | numeric
27
+ single_page_viewer: Optional[bool] = None
28
+ remove_page_borders: Optional[bool] = None
29
+ enable_print_auto_rotate: Optional[bool] = None
30
+ use_only_css_zoom: Optional[bool] = None
31
+ text_layer_mode: Optional[int] = None
32
+ annotation_mode: Optional[int] = None
33
+ annotation_editor_mode: Optional[int] = None
34
+ thumbnail_width: Optional[int] = None
35
+ print_resolution: Optional[int] = None
36
+ max_canvas_pixels: Optional[int] = None
37
+ filename: Optional[str] = None
38
+
39
+ # Escape hatch for advanced/less-common upstream options (e.g.
40
+ # ``getDocumentParams``). Merged verbatim into the JS options object.
41
+ extra: dict[str, Any] = {}
@@ -0,0 +1,80 @@
1
+ """The ``PdfSlick`` Reflex component.
2
+
3
+ PDFSlick's public entry point is the **hook** ``usePDFSlick(url, options)``, not
4
+ a drop-in component, and its ``<PDFSlickThumbnails>`` uses a render-prop child
5
+ that cannot be expressed from Python. So we ship a small local React wrapper
6
+ (``PdfSlickWrapper.tsx``, beside this module) that calls the hook internally,
7
+ renders
8
+ ``<PDFSlickViewer>`` (+ optional thumbnails), subscribes to ``usePDFSlickStore``
9
+ selectors, and exposes a clean prop/event API. See
10
+ ``specs/technical/architecture.md`` for the full rationale.
11
+
12
+ Wiring (Reflex 0.9.x):
13
+
14
+ - ``library`` points at the local wrapper via ``rx.asset(..., shared=True)``'s
15
+ ``$/public`` importable path; the npm package is declared in
16
+ ``lib_dependencies`` so Reflex installs it.
17
+ - PDF.js is browser-only (touches ``window``/``document``/canvas), so the
18
+ component is a ``rx.NoSSRComponent`` (emits ``next/dynamic(..., {ssr:false})``).
19
+ - The required stylesheet ``@pdfslick/react/dist/pdf_viewer.css`` is imported
20
+ once via ``add_imports``.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ from typing import Any
26
+
27
+ import reflex as rx
28
+
29
+ # Pin the upstream npm package for reproducible builds.
30
+ PDFSLICK_REACT = "@pdfslick/react@4.0.0"
31
+ PDFSLICK_CSS = "@pdfslick/react/dist/pdf_viewer.css"
32
+ # Declared so the wrapper can import the PDF.js worker as a Vite asset and
33
+ # override PDFSlick's broken default ``workerSrc``. The caret range matches
34
+ # ``@pdfslick/core``'s own dependency so the installer resolves a single shared
35
+ # copy — guaranteeing the worker version matches the PDF.js API version.
36
+ PDFJS_DIST = "pdfjs-dist@^6.0.227"
37
+
38
+ # Resolve the local JS wrapper into a build-time importable module path
39
+ # ("$/public/...") so Reflex bundles our source instead of trying to import a
40
+ # named export from the npm package.
41
+ _WRAPPER_ASSET = rx.asset("PdfSlickWrapper.tsx", shared=True)
42
+
43
+
44
+ class PdfSlick(rx.NoSSRComponent):
45
+ """Reactive PDF viewer wrapping ``@pdfslick/react``."""
46
+
47
+ # --- Library wiring -----------------------------------------------------
48
+ library = _WRAPPER_ASSET.importable_path
49
+ tag = "PdfSlickWrapper"
50
+ is_default = True
51
+ lib_dependencies = [PDFSLICK_REACT, PDFJS_DIST]
52
+
53
+ # --- Props (camelCased to JS automatically) -----------------------------
54
+ url: rx.Var[str]
55
+ scale_value: rx.Var[str]
56
+ single_page_viewer: rx.Var[bool]
57
+ remove_page_borders: rx.Var[bool]
58
+ thumbnail_width: rx.Var[int]
59
+ show_thumbnails: rx.Var[bool]
60
+ horizontal_thumbnails: rx.Var[bool]
61
+
62
+ # Declarative imperative-control channel (Phase 3): a command object the
63
+ # wrapper applies then clears. Built with helpers in ``commands.py``.
64
+ command: rx.Var[dict[str, Any]]
65
+
66
+ # --- Events -------------------------------------------------------------
67
+ # Each forwards the JS callback's first argument to the Python handler.
68
+ on_document_load: rx.EventHandler[lambda num_pages: [num_pages]]
69
+ on_page_change: rx.EventHandler[lambda page: [page]]
70
+ on_scale_change: rx.EventHandler[lambda scale: [scale]]
71
+ on_error: rx.EventHandler[lambda err: [err]]
72
+ on_metadata: rx.EventHandler[lambda meta: [meta]]
73
+
74
+ def add_imports(self) -> dict[str, Any]:
75
+ """Import PDFSlick's required stylesheet exactly once."""
76
+ return {"": [PDFSLICK_CSS]}
77
+
78
+
79
+ # Snake_case factory following Reflex convention.
80
+ pdf_slick = PdfSlick.create
@@ -0,0 +1,113 @@
1
+ """Stub file for custom_components/reflex_pdfslick/pdfslick.py"""
2
+
3
+ # ------------------- DO NOT EDIT ----------------------
4
+ # This file was generated by `reflex/utils/pyi_generator.py`!
5
+ # ------------------------------------------------------
6
+ from collections.abc import Mapping, Sequence
7
+ from typing import Any, Optional
8
+ from reflex_components_core.core.breakpoints import Breakpoints
9
+ from reflex_base.event import (
10
+ EventType,
11
+ PointerEventInfo,
12
+ )
13
+ from reflex_base.vars.base import Var
14
+ import reflex as rx
15
+
16
+ PDFSLICK_REACT = "@pdfslick/react@4.0.0"
17
+ PDFSLICK_CSS = "@pdfslick/react/dist/pdf_viewer.css"
18
+ PDFJS_DIST = "pdfjs-dist@^6.0.227"
19
+ _WRAPPER_ASSET = rx.asset("PdfSlickWrapper.tsx", shared=True)
20
+
21
+ class PdfSlick(rx.NoSSRComponent):
22
+ def add_imports(self) -> dict[str, Any]: ...
23
+ @classmethod
24
+ def create(
25
+ cls,
26
+ *children,
27
+ url: Var[str] | str | None = None,
28
+ scale_value: Var[str] | str | None = None,
29
+ single_page_viewer: Var[bool] | bool | None = None,
30
+ remove_page_borders: Var[bool] | bool | None = None,
31
+ thumbnail_width: Var[int] | int | None = None,
32
+ show_thumbnails: Var[bool] | bool | None = None,
33
+ horizontal_thumbnails: Var[bool] | bool | None = None,
34
+ command: Var[dict[str, Any]] | dict[str, Any] | None = None,
35
+ style: Sequence[Mapping[str, Any]]
36
+ | Mapping[str, Any]
37
+ | Var[Mapping[str, Any]]
38
+ | Breakpoints
39
+ | None = None,
40
+ key: Any | None = None,
41
+ id: Any | None = None,
42
+ ref: Var | None = None,
43
+ class_name: Any | None = None,
44
+ custom_attrs: dict[str, Any | Var] | None = None,
45
+ on_blur: Optional[EventType[()]] = None,
46
+ on_click: Optional[EventType[()] | EventType[PointerEventInfo]] = None,
47
+ on_context_menu: Optional[EventType[()] | EventType[PointerEventInfo]] = None,
48
+ on_document_load: Optional[EventType[Any]] = None,
49
+ on_double_click: Optional[EventType[()] | EventType[PointerEventInfo]] = None,
50
+ on_error: Optional[EventType[Any]] = None,
51
+ on_focus: Optional[EventType[()]] = None,
52
+ on_metadata: Optional[EventType[Any]] = None,
53
+ on_mount: Optional[EventType[()]] = None,
54
+ on_mouse_down: Optional[EventType[()]] = None,
55
+ on_mouse_enter: Optional[EventType[()]] = None,
56
+ on_mouse_leave: Optional[EventType[()]] = None,
57
+ on_mouse_move: Optional[EventType[()]] = None,
58
+ on_mouse_out: Optional[EventType[()]] = None,
59
+ on_mouse_over: Optional[EventType[()]] = None,
60
+ on_mouse_up: Optional[EventType[()]] = None,
61
+ on_page_change: Optional[EventType[Any]] = None,
62
+ on_scale_change: Optional[EventType[Any]] = None,
63
+ on_scroll: Optional[EventType[()]] = None,
64
+ on_scroll_end: Optional[EventType[()]] = None,
65
+ on_unmount: Optional[EventType[()]] = None,
66
+ **props,
67
+ ) -> "PdfSlick":
68
+ """Create the component.
69
+
70
+ Args:
71
+ *children: The children of the component.
72
+ url: --- Props (camelCased to JS automatically) -----------------------------
73
+ scale_value: no description
74
+ single_page_viewer: no description
75
+ remove_page_borders: no description
76
+ thumbnail_width: no description
77
+ show_thumbnails: no description
78
+ horizontal_thumbnails: no description
79
+ command: Declarative imperative-control channel (Phase 3): a command object the wrapper applies then clears. Built with helpers in ``commands.py``.
80
+ style: The style of the component.
81
+ key: A unique key for the component.
82
+ id: The id for the component.
83
+ ref: The Var to pass as the ref to the component.
84
+ class_name: The class name for the component.
85
+ custom_attrs: Attributes passed directly to the component.
86
+ on_focus: Fired when the element (or some element inside of it) receives focus. For example, it is called when the user clicks on a text input.
87
+ on_blur: Fired when focus has left the element (or left some element inside of it). For example, it is called when the user clicks outside of a focused text input.
88
+ on_click: Fired when the user clicks on an element. For example, it's called when the user clicks on a button.
89
+ on_context_menu: Fired when the user right-clicks on an element.
90
+ on_double_click: Fired when the user double-clicks on an element.
91
+ on_mouse_down: Fired when the user presses a mouse button on an element.
92
+ on_mouse_enter: Fired when the mouse pointer enters the element.
93
+ on_mouse_leave: Fired when the mouse pointer leaves the element.
94
+ on_mouse_move: Fired when the mouse pointer moves over the element.
95
+ on_mouse_out: Fired when the mouse pointer moves out of the element.
96
+ on_mouse_over: Fired when the mouse pointer moves onto the element.
97
+ on_mouse_up: Fired when the user releases a mouse button on an element.
98
+ on_scroll: Fired when the user scrolls the element.
99
+ on_scroll_end: Fired when scrolling ends on the element.
100
+ on_mount: Fired when the component is mounted to the page.
101
+ on_unmount: Fired when the component is removed from the page. Only called during navigation, not on page refresh.
102
+ on_document_load: --- Events ------------------------------------------------------------- Each forwards the JS callback's first argument to the Python handler.
103
+ on_page_change: no description
104
+ on_scale_change: no description
105
+ on_error: no description
106
+ on_metadata: no description
107
+ **props: The props of the component.
108
+
109
+ Returns:
110
+ The component."""
111
+ ...
112
+
113
+ pdf_slick = PdfSlick.create
@@ -0,0 +1,262 @@
1
+ Metadata-Version: 2.4
2
+ Name: reflex-pdfslick
3
+ Version: 0.0.1
4
+ Summary: A native Reflex component wrapping PDFSlick (PDF.js + Zustand) to view and interact with PDF documents in pure Python.
5
+ Author-email: Ernesto Crespo <ecrespo@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/ecrespo/reflex-pdfslick
8
+ Project-URL: Repository, https://github.com/ecrespo/reflex-pdfslick
9
+ Project-URL: Issues, https://github.com/ecrespo/reflex-pdfslick/issues
10
+ Project-URL: Documentation, https://github.com/ecrespo/reflex-pdfslick#readme
11
+ Project-URL: Changelog, https://github.com/ecrespo/reflex-pdfslick/releases
12
+ Project-URL: PyPI, https://pypi.org/project/reflex-pdfslick/
13
+ Project-URL: PDFSlick, https://pdfslick.dev/
14
+ Keywords: reflex,reflex-custom-components,pdf,pdfslick,pdfjs,pdf-viewer,component,viewer,thumbnails
15
+ Classifier: Development Status :: 3 - Alpha
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: License :: OSI Approved :: MIT License
18
+ Classifier: Operating System :: OS Independent
19
+ Classifier: Framework :: AsyncIO
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: reflex>=0.9.0
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=8.0; extra == "dev"
27
+ Requires-Dist: build; extra == "dev"
28
+ Requires-Dist: twine; extra == "dev"
29
+ Dynamic: license-file
30
+
31
+ # reflex-pdfslick
32
+
33
+ [![PyPI version](https://img.shields.io/pypi/v/reflex-pdfslick.svg)](https://pypi.org/project/reflex-pdfslick/)
34
+ [![Python versions](https://img.shields.io/pypi/pyversions/reflex-pdfslick.svg)](https://pypi.org/project/reflex-pdfslick/)
35
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/ecrespo/reflex-pdfslick/blob/main/LICENSE)
36
+ [![GitHub](https://img.shields.io/badge/GitHub-ecrespo%2Freflex--pdfslick-181717?logo=github)](https://github.com/ecrespo/reflex-pdfslick)
37
+
38
+ - **Source:** https://github.com/ecrespo/reflex-pdfslick
39
+ - **Issues:** https://github.com/ecrespo/reflex-pdfslick/issues
40
+ - **PDFSlick (upstream):** https://github.com/pdfslick/pdfslick — https://pdfslick.dev
41
+
42
+ A native [Reflex](https://reflex.dev/) component that wraps
43
+ [**PDFSlick**](https://pdfslick.dev/) so you can view and interact with PDF
44
+ documents in **pure Python** — no HTML, no `iframe`, no hand-written
45
+ JavaScript.
46
+
47
+ PDFSlick is built on top of Mozilla's [PDF.js](https://github.com/mozilla/pdf.js)
48
+ and uses [Zustand](https://github.com/pmndrs/zustand) to expose a reactive store
49
+ for the loaded document. `reflex-pdfslick` bridges that store and PDFSlick's
50
+ imperative API into Reflex's declarative, state-driven model: document state
51
+ (page number, page count, zoom, rotation, metadata, thumbnails) flows back to
52
+ Python as events, and Python triggers actions (go to page, zoom, rotate, print,
53
+ download) on the viewer.
54
+
55
+ > **Project status — `ALPHA` / implemented, pre-release.**
56
+ > The component runtime, imperative controls, and the full 7-example demo
57
+ > gallery are built and covered by tests; the demo compiles to a Reflex
58
+ > production build in CI. The remaining step is the PyPI/GitHub release. See
59
+ > [Roadmap](#roadmap) for details.
60
+
61
+ ```python
62
+ import reflex as rx
63
+ from reflex_pdfslick import pdf_slick
64
+
65
+
66
+ class State(rx.State):
67
+ page_number: int = 1
68
+ num_pages: int = 0
69
+
70
+ @rx.event
71
+ def on_page_change(self, page: int):
72
+ self.page_number = page
73
+
74
+ @rx.event
75
+ def on_document_load(self, num_pages: int):
76
+ self.num_pages = num_pages
77
+
78
+
79
+ def index() -> rx.Component:
80
+ return pdf_slick(
81
+ url="/sample.pdf",
82
+ scale_value="page-fit",
83
+ single_page_viewer=True,
84
+ on_page_change=State.on_page_change,
85
+ on_document_load=State.on_document_load,
86
+ height="700px",
87
+ )
88
+
89
+
90
+ app = rx.App()
91
+ app.add_page(index)
92
+ ```
93
+
94
+ ## Why a native component instead of an `iframe`?
95
+
96
+ Embedding a PDF viewer through an `iframe` or a raw `<embed>` works, but it is
97
+ isolated from your app: you cannot react to page changes, drive the zoom from a
98
+ Python toolbar, or read the document's metadata into your state. Wrapping
99
+ PDFSlick as a first-class Reflex component means:
100
+
101
+ - **Reactive state** — `page_number`, `num_pages`, `scale`, `rotation`,
102
+ `is_document_loaded` and document metadata are pushed into Reflex state via
103
+ typed events.
104
+ - **Imperative control from Python** — go to page, zoom in/out, fit width/page,
105
+ rotate, switch scroll/spread modes, print and download are exposed as
106
+ component-level controls.
107
+ - **Reusable & versioned** — installable with `pip`, with the npm dependency
108
+ (`@pdfslick/react`) pinned for reproducible builds.
109
+ - **SSR-safe** — PDF.js is browser-only, so the component is wrapped as a
110
+ Reflex `NoSSRComponent` and only ever renders on the client.
111
+
112
+ ## Installation
113
+
114
+ ```shell
115
+ pip install reflex-pdfslick
116
+ # or, from source
117
+ git clone https://github.com/ecrespo/reflex-pdfslick.git
118
+ cd reflex-pdfslick
119
+ pip install -e .
120
+ ```
121
+
122
+ Reflex installs the underlying npm packages automatically on first run. The
123
+ component pins `@pdfslick/react` and `pdfjs-dist` (which pull in
124
+ `@pdfslick/core` and `zustand`) and imports the required stylesheet
125
+ `@pdfslick/react/dist/pdf_viewer.css` for you.
126
+
127
+ ## Custom-component packaging (build & publish)
128
+
129
+ This repository is a standard [Reflex custom
130
+ component](https://reflex.dev/docs/custom-components/overview/): the package
131
+ lives in `custom_components/reflex_pdfslick/` (shipping the `PdfSlickWrapper.tsx`
132
+ wrapper and a generated `pdfslick.pyi` stub), the demo app in `pdfslick_demo/`,
133
+ and `pyproject.toml` carries the `reflex-custom-components` keyword used by the
134
+ Reflex component gallery to discover it on PyPI.
135
+
136
+ ```shell
137
+ # Build the wheel + sdist into dist/ (also regenerates the .pyi type stubs).
138
+ # PYTHONPATH=. lets the stub generator import custom_components/.
139
+ PYTHONPATH=. uv run reflex component build
140
+
141
+ # Inspect, then publish to PyPI (bump the version in pyproject.toml first):
142
+ uv run twine upload dist/* # or: uv publish
143
+
144
+ # Share on the Reflex community gallery:
145
+ uv run reflex component share
146
+ ```
147
+
148
+ The built wheel includes the React wrapper (`reflex_pdfslick/PdfSlickWrapper.tsx`)
149
+ and the type stub, so `rx.asset(...)` resolves the wrapper when the package is
150
+ installed from PyPI — not only in editable mode.
151
+
152
+ ## Planned API (summary)
153
+
154
+ The full contract lives in [specs/api/component-api.md](specs/api/component-api.md).
155
+
156
+ | Prop | Type | Description |
157
+ |---|---|---|
158
+ | `url` | `str` | Path/URL of the PDF to load. |
159
+ | `scale_value` | `str` | `"auto"`, `"page-fit"`, `"page-width"`, `"page-actual"` or a numeric string. |
160
+ | `single_page_viewer` | `bool` | Render a single page at a time. |
161
+ | `thumbnail_width` | `int` | Width (px) of generated thumbnails. |
162
+ | `remove_page_borders` | `bool` | Hide page borders. |
163
+
164
+ | Event | Payload | Fired when |
165
+ |---|---|---|
166
+ | `on_document_load` | `int` (num pages) | The document finishes loading. |
167
+ | `on_page_change` | `int` (page) | The current page changes. |
168
+ | `on_scale_change` | `float` (scale) | The zoom level changes. |
169
+ | `on_error` | `dict` | Loading/parsing fails. |
170
+
171
+ Imperative controls (next/previous page, zoom, rotate, print, download) are
172
+ driven from Python state — see the API spec for the dispatch mechanism.
173
+
174
+ ## Examples (demo gallery)
175
+
176
+ The `pdfslick_demo/` Reflex app mirrors PDFSlick's own example set. Each example
177
+ is documented in the [PRD](specs/prd/pdfslick-component.md) and
178
+ [task breakdown](specs/tasks/task-breakdown.md):
179
+
180
+ 1. **Simple Viewer** — basic viewer with prev/next navigation and zoom.
181
+ 2. **Full Viewer App** — toolbar, zoom selector, rotation, scroll/spread modes,
182
+ document info, search and thumbnails sidebar.
183
+ 3. **Thumbnails Layout** — a grid of page thumbnails as the primary surface.
184
+ 4. **Horizontal Thumbnails** — a horizontal thumbnail strip.
185
+ 5. **Multiple Documents** — several independent viewers on one page.
186
+ 6. **Load from ArrayBuffer** — open a PDF from in-memory bytes.
187
+ 7. **Error Handling** — graceful handling of load/parse errors.
188
+
189
+ Run the demo:
190
+
191
+ ```shell
192
+ cd pdfslick_demo
193
+ pip install -r requirements.txt
194
+ reflex run
195
+ ```
196
+
197
+ ## Architecture (at a glance)
198
+
199
+ ```
200
+ Python (Reflex app)
201
+ pdf_slick(url=..., scale_value=..., on_page_change=...)
202
+ │ compiles to
203
+ ▼
204
+ React / Next.js (generated by Reflex)
205
+ <PdfSlickWrapper> ← local .tsx, NoSSR, calls usePDFSlick(url, options)
206
+ │ renders <PDFSlickViewer/> + (optional) thumbnails
207
+ │ subscribes to usePDFSlickStore selectors → emits Reflex events
208
+ ▼
209
+ @pdfslick/react → @pdfslick/core → PDF.js (pdfjs-dist) + Zustand store
210
+ ```
211
+
212
+ Because PDFSlick's public surface is the **hook** `usePDFSlick(...)` (not a
213
+ drop-in component) and its thumbnails component uses a render-prop child, the
214
+ wrapper is a small local `.tsx` component that calls the hook internally and
215
+ exposes a clean prop/event API to Python. Full rationale and trade-offs are in
216
+ [specs/technical/architecture.md](specs/technical/architecture.md).
217
+
218
+ ## Spec-Driven Design documentation
219
+
220
+ This project is documented before it is built. The `specs/` directory is the
221
+ source of truth:
222
+
223
+ | Document | Purpose |
224
+ |---|---|
225
+ | [`specs/prd/pdfslick-component.md`](specs/prd/pdfslick-component.md) | Product requirements, goals, scope, examples. |
226
+ | [`specs/technical/architecture.md`](specs/technical/architecture.md) | Technical design, SSR strategy, wrapper pattern. |
227
+ | [`specs/api/component-api.md`](specs/api/component-api.md) | Public Python API: props, events, controls. |
228
+ | [`specs/data-model/store-schema.md`](specs/data-model/store-schema.md) | PDFSlick store schema and Python ↔ JS mapping. |
229
+ | [`specs/plans/implementation-plan.md`](specs/plans/implementation-plan.md) | Phases, milestones, sequencing. |
230
+ | [`specs/tasks/task-breakdown.md`](specs/tasks/task-breakdown.md) | Granular, trackable task list per phase. |
231
+ | [`specs/research/pdfslick-and-reflex-research.md`](specs/research/pdfslick-and-reflex-research.md) | Primary-source research notes that ground the design. |
232
+
233
+ ## Roadmap
234
+
235
+ - [x] **Phase 0 — Foundation & SDD**: repository, packaging, SDD artifacts,
236
+ README, demo skeleton.
237
+ - [x] **Phase 1 — Core wrapper**: `PdfSlickWrapper.tsx`, `pdf_slick` component,
238
+ props, CSS import, NoSSR.
239
+ - [x] **Phase 2 — Events & state bridge**: page/scale/load/error/metadata events.
240
+ - [x] **Phase 3 — Imperative controls**: navigation, zoom, rotation, modes,
241
+ print, download (declarative `command` prop + `commands` helpers).
242
+ - [x] **Phase 4 — Thumbnails & multi-document**: vertical/horizontal thumbnails,
243
+ independent multi-instance viewers.
244
+ - [x] **Phase 5 — Demo gallery** (all 7 examples) **& docs polish**.
245
+ - [x] **Phase 6 — Tests & CI**: 35 unit tests, GitHub Actions (pytest 3.10–3.13,
246
+ TSX transpile, demo build smoke). Demo compiles to a Reflex production build.
247
+ - [ ] **Release** — PyPI publish + tagged GitHub release *(awaiting maintainer
248
+ credentials)*.
249
+
250
+ ## Credits
251
+
252
+ - [PDFSlick](https://pdfslick.dev/) by
253
+ [Vancho Stojkov](https://github.com/van100j) — the library being wrapped (MIT).
254
+ - [PDF.js](https://github.com/mozilla/pdf.js) by Mozilla.
255
+ - [Reflex](https://reflex.dev/) — the Python web framework.
256
+
257
+ This wrapper is an independent project and is not affiliated with or endorsed by
258
+ the PDFSlick authors.
259
+
260
+ ## License
261
+
262
+ [MIT](LICENSE) © Ernesto Crespo
@@ -0,0 +1,12 @@
1
+ reflex_pdfslick/PdfSlickWrapper.tsx,sha256=b2snBAZtNqun420O4-Wj_drk1EnZArw49R2-Oift3lk,9399
2
+ reflex_pdfslick/__init__.py,sha256=lBFbdhpNUcYZJgsAEROfObpwL4ANaFXFzJYnCfXLV8A,951
3
+ reflex_pdfslick/commands.py,sha256=TeSq9dv9dhQDs2BnJQvi8mzjBYfbmwIOjXr4oOhMG5A,3011
4
+ reflex_pdfslick/models.py,sha256=6dJn3Y1A33j18iQnv9wysV6dY3ZBchZp_EVhHq6Hysg,1227
5
+ reflex_pdfslick/options.py,sha256=4XdfjLzyNbcmViOwF0lXtVRAZ4bcSmTz3XzP4qF8EDI,1566
6
+ reflex_pdfslick/pdfslick.py,sha256=JcKcR36uYb9um42-r4DmCgHHw2r0Sl9RzIFJX5ytInU,3354
7
+ reflex_pdfslick/pdfslick.pyi,sha256=cWPslTVrGjMuGo-GdZm9QtFOnOSrluJhVxTBvhQOWug,5853
8
+ reflex_pdfslick-0.0.1.dist-info/licenses/LICENSE,sha256=ikDZVzB6LJXytfIXkllDZj9ningVU9BduOAcL8jLKYI,1071
9
+ reflex_pdfslick-0.0.1.dist-info/METADATA,sha256=alXTw9d7ZxZOVLPmsN3KMq2O1lyCTb16queIV5gkC2o,11332
10
+ reflex_pdfslick-0.0.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
11
+ reflex_pdfslick-0.0.1.dist-info/top_level.txt,sha256=LdjY4d1vmIUeKMRRQ_rJ8y7XuJ7qhsyKfHaHXpnn7zs,16
12
+ reflex_pdfslick-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ernesto Crespo
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.
@@ -0,0 +1 @@
1
+ reflex_pdfslick