Skip to content

Commit 0bf5bfa

Browse files
serpentbladeclaude
andcommitted
docs(pdf): @rozie-ui/pdf showcase + comparison + live-demo (3rd-page standard)
Showcase (quick starts x6, validated Props table [10 rows], Events, 12-verb handle, recipes, gotchas — worker/standardFontDataUrl/no-CSS-import), libraries comparison (react-pdf deep / ng2-pdf-viewer + vue-pdf-embed moderate / Svelte thin / Solid+Lit nothing), and a live-demo page running the real @rozie-ui/pdf-vue package: a 3-page PDF with selectable text layer, page nav + zoom + rotate + continuous-scroll toggle via the handle, bundled worker via new URL(). Sidebar + rozie-codegen product + docs deps (@rozie-ui/pdf-vue, pdfjs-dist). Verified: vitepress build clean (143 anchors, 0 broken); headless render confirms canvas + text layer (selectable spans) render, readout 'page 1/3', Next->2/3. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 23c59d5 commit 0bf5bfa

7 files changed

Lines changed: 612 additions & 1 deletion

File tree

docs/.vitepress/config.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,14 @@ export default defineConfig({
167167
{ text: 'Cropper — live demo', link: '/guide/cropper-demo' },
168168
],
169169
},
170+
{
171+
text: '@rozie-ui/pdf',
172+
items: [
173+
{ text: 'PdfViewer — showcase & API', link: '/guide/pdf' },
174+
{ text: 'PDF libraries comparison', link: '/guide/pdf-comparison' },
175+
{ text: 'PdfViewer — live demo', link: '/guide/pdf-demo' },
176+
],
177+
},
170178
],
171179
'/compatibility': [
172180
{

docs/.vitepress/rozie-codegen.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export function rozieCodegen(
6161
// `SortableList` moved there in Phase 20-01 and `Flatpickr` in the
6262
// flatpickr port, so docs resolve their canonical producer from the
6363
// package source rather than the (now-removed) `examples/<Name>.rozie`.
64-
for (const product of ['sortable-list', 'flatpickr', 'fullcalendar', 'codemirror', 'chartjs', 'tiptap', 'maplibre', 'cropper']) {
64+
for (const product of ['sortable-list', 'flatpickr', 'fullcalendar', 'codemirror', 'chartjs', 'tiptap', 'maplibre', 'cropper', 'pdf']) {
6565
const pkgSrc = resolve(
6666
opts.examplesDir,
6767
'..',

docs/guide/pdf-comparison.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# PDF libraries comparison
2+
3+
How `@rozie-ui/pdf` compares to the existing per-framework [PDF.js](https://mozilla.github.io/pdf.js/) wrappers. PDF.js (mozilla/pdf.js, shipped as `pdfjs-dist`) is the de-facto vanilla-JS PDF rendering engine, and it is framework-agnostic: every wrapper exists only to glue reactive state to PDF.js's imperative `getDocument()` / `page.render()` flow, configure the Web Worker, render the page canvas (and, for the good ones, the selectable text layer), and forward the page / load events. The result is a **lopsided ecosystem**: a deep, maintained React wrapper; decent Vue and Angular options; a **thin Svelte story; and effectively nothing for Solid or Lit**. Rozie ships one source to all six.
4+
5+
> Research snapshot: 2026-06-08. Versions and the wrapper landscape move; treat them as of that date. Note `@react-pdf/renderer` is a different library — it *generates* PDFs from React components; it is **not** a viewer, so it's out of scope here.
6+
7+
## The wrappers at a glance
8+
9+
| Framework | PDF.js wrapper | Engine | Depth | Notes |
10+
| --- | --- | --- | :---: | --- |
11+
| **React** | `react-pdf` (wojtekmaj) | `pdfjs-dist` | **deep** | Mature, actively maintained, the obvious React pick. |
12+
| **Vue** | `vue-pdf-embed` (+ `@tato30/vue-pdf`) | `pdfjs-dist` | **moderate** | Maintained, reasonable surface; less deep than `react-pdf`. |
13+
| **Angular** | `ng2-pdf-viewer` | `pdfjs-dist` | **moderate** | Popular, widely used; maintenance has slowed (last publish 2024). |
14+
| **Svelte** | `svelte-pdf` / community wrappers | `pdfjs-dist` | **thin** | Sparse surface, lower adoption, no text-layer story. |
15+
| **Solid** | *(none)* ||| No dedicated PDF.js (or comparable) viewer wrapper. |
16+
| **Lit** | *(none)* ||| No web-component viewer; PDF.js's own prebuilt viewer is an iframe-embedded *app*, not a component. |
17+
| **Rozie** | `@rozie-ui/pdf-*` | `pdfjs-dist` v6 | **deep** | One source → all six, same props / two-way `page` / events / handle / text layer. |
18+
19+
`react-pdf` is an **excellent, mature library** — for a single-React app it's the obvious pick, and Rozie does not claim to out-feature it on React. `vue-pdf-embed` and `ng2-pdf-viewer` are likewise **solid choices on their home framework**. The wedge is the underserved targets: Svelte's options are **thin** (`svelte-pdf` is sparse, low-adoption, with no selectable-text-layer story), and **Solid and Lit have nothing at all** — a Lit dev's only "option" is embedding PDF.js's prebuilt viewer-app in an iframe, which is a whole application, not a component. Rozie gives all three underserved targets a real, *consistent* embeddable PDF viewer — the same one it produces for React — from a single definition, with one uniform API across all six.
20+
21+
## Feature matrix
22+
23+
Cell legend: **** = documented out-of-the-box · **** = not supported / not present · **⚠️** = partial / consumer-glue-required / thin.
24+
25+
| Capability | `react-pdf` | `vue-pdf-embed` | `ng2-pdf-viewer` | `svelte-pdf` | Solid (none) | Lit (none) | **`@rozie-ui/pdf`** |
26+
| --- | :---: | :---: | :---: | :---: | :---: | :---: | :---: |
27+
| Mount from URL ||||| hand-roll | hand-roll |`:src` |
28+
| Mount from data (`Uint8Array` / `ArrayBuffer`) |||| ⚠️ | hand-roll | hand-roll |`:src` (+ `data:` URL decode) |
29+
| **Two-way current page** | ⚠️ via state | ⚠️ via prop | ⚠️ `[page]` + `(pageChange)` | ⚠️ |||`r-model:page` (echo-guarded) |
30+
| Continuous scroll (all pages) |||| ⚠️ |||`render-all-pages` |
31+
| Single-page mode ||||||| ✅ (default) |
32+
| Zoom |||| ⚠️ |||`:scale` + zoom verbs |
33+
| Rotation |||| ⚠️ |||`:rotation` + rotate verbs |
34+
| **Selectable text layer** |||||||`text-layer` (default on, CSS shipped) |
35+
| Imperative handle | ⚠️ via refs | ⚠️ partial | ⚠️ via methods || hand-roll | hand-roll | ✅ uniform 12-verb `$expose` |
36+
| Password-protected PDFs |||| ⚠️ |||`:password` + `passwordrequest` event |
37+
| TypeScript |||| ⚠️ ||||
38+
| One source → all 6 frameworks ||||||||
39+
40+
## Where Rozie wins today
41+
42+
- **One definition, six idiomatic packages** — including the three frameworks the ecosystem underserves: **Svelte (thin `svelte-pdf`, no text layer), Solid (nothing), and Lit (nothing but an iframe-embedded viewer-app)**. A Svelte dev today fights a sparse, low-adoption wrapper; a Solid or Lit dev hand-rolls the whole worker / `getDocument` / canvas / text-layer flow. Rozie hands all three a first-class PDF viewer with selectable text, page nav, zoom and rotation.
43+
- **A real two-way current page on all six**`r-model:page` (1-based) reads *and* drives which page renders in single mode, and in `render-all-pages` mode it reflects the scrolled-to page back via an `IntersectionObserver` (and the `pagechange` event), with an echo-guard so a consumer write and the scroll-spy don't fight. The incumbents surface the page via a one-way prop plus a separate change event; you wire the round-trip yourself.
44+
- **A selectable text layer that just works**`text-layer` is on by default and renders PDF.js's text spans over each page canvas so text is copyable / searchable. The required `.textLayer` CSS and the `--scale-factor` var ship *with the component* (via the `:root {}` engine-DOM escape hatch), so there's **no extra CSS import**. This is exactly what `svelte-pdf` lacks and what a hand-rolled Solid / Lit viewer rarely gets right.
45+
- **A uniform 12-verb imperative handle** (`getDocument` / `getPageCount` / `goToPage` / `nextPage` / `prevPage` / `setScale` / `zoomIn` / `zoomOut` / `fitWidth` / `fitPage` / `rotateCW` / `rotateCCW`) grabbed with each framework's native ref — identical on every target, versus "however this wrapper happens to expose things." The verbs drive the internal render state, so they work whether or not the consumer two-way-binds `page`.
46+
- **Zero-config worker** — the #1 PDF.js integration friction is `GlobalWorkerOptions.workerSrc`. The `worker-src` prop defaults to the version-matched CDN copy, so the component renders with no setup; override it (`:worker-src`) for offline / CSP / bundled-worker builds. Standard-font data is wired the same way (`:standard-font-data-url`).
47+
- **`getDocument()` is always one hop from the raw engine**, so the full `pdfjs-dist` API (annotation extraction, outline, metadata, custom render flows) is reachable on any target when the curated surface doesn't cover something.
48+
49+
## What Rozie defers {#what-rozie-defers}
50+
51+
This page concedes where the standalone wrappers and PDF.js's own viewer are genuinely ahead — that's what keeps the comparison credible, and it doubles as Rozie's own roadmap.
52+
53+
- **Annotation layer / form fields (AcroForm).** `react-pdf` and PDF.js's full prebuilt viewer render the **annotation layer** — links, widget annotations, and interactive AcroForm form fields. Rozie v1 renders the **page canvas + the selectable text layer**, not the annotation layer, so links aren't clickable and form fields aren't fillable inside the component. This is a meaningful piece of PDF.js (`AnnotationLayer` + the annotation storage / form value plumbing), deliberately deferred rather than half-shipped. Until then, `getDocument()` hands you the raw `PDFDocumentProxy` so you can drive the annotation layer yourself.
54+
- **Search UI, thumbnails sidebar, print / download chrome.** PDF.js's full prebuilt **viewer application** ships find-in-document, a thumbnail sidebar, print and download toolbars, and presentation mode. Rozie ships the **embeddable viewer component**, not the full viewer-app chrome. The underlying data is all reachable — `getDocument()` exposes the raw pdfjs document for custom search / thumbnail / print UI — but Rozie doesn't bundle that chrome.
55+
- **Big-framework depth on the home framework.** `react-pdf` is a mature, multi-year library with deep React-idiomatic ergonomics, broad edge-case handling, and a large user base; `vue-pdf-embed` and `ng2-pdf-viewer` are likewise well-worn on their own frameworks. On their home framework each exposes more accumulated polish than Rozie's curated prop set. Rozie's value is **not** "more than `react-pdf` on React" — it's the **same idiomatic component on all six frameworks from one source**, with the underserved **Svelte / Solid / Lit** getting a viewer they otherwise lack. For anything outside the curated surface, `getDocument()` hands you the raw engine on every target.
56+
- **`@rozie-ui/pdf` is `0.1.0`.** The surface (10 props / 5 events / 12-verb handle / two-way `page` model / selectable text layer / single + continuous render modes) is stable and gate-verified, but it is younger than the multi-year incumbents.
57+
58+
## Try it
59+
60+
The [`@rozie-ui/pdf` showcase + API reference](/guide/pdf) documents the `@rozie-ui/pdf-*` packages — one pre-compiled, per-framework install (`npm i @rozie-ui/pdf-react pdfjs-dist`, etc.). The PDF.js Web Worker **auto-configures** from the version-matched CDN, so there's nothing extra to import to render a PDF (override `:worker-src` for offline / CSP / bundled-worker builds). The showcase walks the two-way `page` binding, single vs `render-all-pages` modes, the selectable text layer, zoom / rotation, password-protected PDFs, and the 12-verb imperative handle. The [live demo](/guide/pdf-demo) runs the component across all six targets.
61+
62+
## Cross-references
63+
64+
- [PDF — showcase & API](/guide/pdf) — the full `@rozie-ui/pdf` surface, quick starts, and recipes.
65+
- [PDF — live demo](/guide/pdf-demo) — the viewer running across all six targets.
66+
- [`PdfViewer.rozie` source on GitHub](https://github.com/One-Learning-Community/rozie.js/blob/main/packages/ui/pdf/src/PdfViewer.rozie)
67+
- [Cropper libraries comparison](/guide/cropper-comparison) — the sibling engine-wrapper port.

docs/guide/pdf-demo.md

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
---
2+
title: PdfViewer — live demo
3+
---
4+
5+
<script setup lang="ts">
6+
import { ref } from 'vue';
7+
import PdfViewer from '@rozie-ui/pdf-vue';
8+
9+
// Bundle the PDF.js worker locally via Vite's `new URL(...)` so the demo needs no
10+
// CDN for the worker (the recommended override for bundler setups — the prop
11+
// otherwise defaults to a CDN copy).
12+
const workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
13+
14+
// A self-contained 3-page PDF (base64 data URL) — network-free, so the demo works
15+
// offline / in CI.
16+
const SAMPLE =
17+
'data:application/pdf;base64,JVBERi0xLjQKMSAwIG9iago8PC9UeXBlL0NhdGFsb2cvUGFnZXMgMiAwIFI+PgplbmRvYmoKMiAwIG9iago8PC9UeXBlL1BhZ2VzL0tpZHNbNCAwIFIgNSAwIFIgNiAwIFJdL0NvdW50IDM+PgplbmRvYmoKMyAwIG9iago8PC9UeXBlL0ZvbnQvU3VidHlwZS9UeXBlMS9CYXNlRm9udC9IZWx2ZXRpY2E+PgplbmRvYmoKNCAwIG9iago8PC9UeXBlL1BhZ2UvUGFyZW50IDIgMCBSL01lZGlhQm94WzAgMCA2MTIgNzkyXS9SZXNvdXJjZXM8PC9Gb250PDwvRjEgMyAwIFI+Pj4+L0NvbnRlbnRzIDcgMCBSPj4KZW5kb2JqCjUgMCBvYmoKPDwvVHlwZS9QYWdlL1BhcmVudCAyIDAgUi9NZWRpYUJveFswIDAgNjEyIDc5Ml0vUmVzb3VyY2VzPDwvRm9udDw8L0YxIDMgMCBSPj4+Pi9Db250ZW50cyA4IDAgUj4+CmVuZG9iago2IDAgb2JqCjw8L1R5cGUvUGFnZS9QYXJlbnQgMiAwIFIvTWVkaWFCb3hbMCAwIDYxMiA3OTJdL1Jlc291cmNlczw8L0ZvbnQ8PC9GMSAzIDAgUj4+Pj4vQ29udGVudHMgOSAwIFI+PgplbmRvYmoKNyAwIG9iago8PC9MZW5ndGggMTI3Pj4Kc3RyZWFtCkJUIC9GMSAyOCBUZiA2MCA3MDAgVGQgKFJvemllIFBERiB2aWV3ZXIgIC0gIHBhZ2UgMSkgVGogMCAtNDAgVGQgL0YxIDE0IFRmIChUaGUgcXVpY2sgYnJvd24gZm94IGp1bXBzIG92ZXIgdGhlIGxhenkgZG9nLikgVGogRVQKZW5kc3RyZWFtCmVuZG9iago4IDAgb2JqCjw8L0xlbmd0aCAxMzc+PgpzdHJlYW0KQlQgL0YxIDI4IFRmIDYwIDcwMCBUZCAoT25lIHNvdXJjZSwgc2l4IGZyYW1ld29ya3MgIC0gIHBhZ2UgMikgVGogMCAtNDAgVGQgL0YxIDE0IFRmIChUaGUgcXVpY2sgYnJvd24gZm94IGp1bXBzIG92ZXIgdGhlIGxhenkgZG9nLikgVGogRVQKZW5kc3RyZWFtCmVuZG9iago5IDAgb2JqCjw8L0xlbmd0aCAxNDU+PgpzdHJlYW0KQlQgL0YxIDI4IFRmIDYwIDcwMCBUZCAoU2VsZWN0YWJsZSB0ZXh0IHZpYSB0aGUgdGV4dCBsYXllciAgLSAgcGFnZSAzKSBUaiAwIC00MCBUZCAvRjEgMTQgVGYgKFRoZSBxdWljayBicm93biBmb3gganVtcHMgb3ZlciB0aGUgbGF6eSBkb2cuKSBUaiBFVAplbmRzdHJlYW0KZW5kb2JqCnhyZWYKMCAxMAowMDAwMDAwMDAwIDY1NTM1IGYgCjAwMDAwMDAwMDkgMDAwMDAgbiAKMDAwMDAwMDA1NCAwMDAwMCBuIAowMDAwMDAwMTE3IDAwMDAwIG4gCjAwMDAwMDAxODAgMDAwMDAgbiAKMDAwMDAwMDI5MiAwMDAwMCBuIAowMDAwMDAwNDA0IDAwMDAwIG4gCjAwMDAwMDA1MTYgMDAwMDAgbiAKMDAwMDAwMDY5MiAwMDAwMCBuIAowMDAwMDAwODc4IDAwMDAwIG4gCnRyYWlsZXIKPDwvU2l6ZSAxMC9Sb290IDEgMCBSPj4Kc3RhcnR4cmVmCjEwNzIKJSVFT0Y=';
18+
19+
const viewer = ref();
20+
const page = ref(1);
21+
const total = ref(0);
22+
const allPages = ref(false);
23+
</script>
24+
25+
# PdfViewer — live demo
26+
27+
This is the **real `@rozie-ui/pdf-vue` package** running on this page (VitePress is itself a Vue app), rendering a 3-page PDF via PDF.js. Page through it, zoom, rotate, toggle continuous scroll — and **select the text** (it's a real text layer, not an image). All of it is driven by the one `PdfViewer.rozie` source that compiles to six frameworks.
28+
29+
<ClientOnly>
30+
<div class="pdf-live">
31+
<div class="pdf-live__controls">
32+
<button @click="viewer?.prevPage()" :disabled="page <= 1">‹ Prev</button>
33+
<span class="pdf-live__readout">page {{ page }} / {{ total || '' }}</span>
34+
<button @click="viewer?.nextPage()" :disabled="page >= total">Next ›</button>
35+
<span class="pdf-live__sep" />
36+
<button @click="viewer?.zoomOut()">Zoom −</button>
37+
<button @click="viewer?.zoomIn()">Zoom +</button>
38+
<button @click="viewer?.fitWidth()">Fit width</button>
39+
<button @click="viewer?.rotateCW()">Rotate ⟳</button>
40+
<span class="pdf-live__sep" />
41+
<button :class="{ 'pdf-live__primary': allPages }" @click="allPages = !allPages">
42+
{{ allPages ? 'Continuous ✓' : 'Continuous' }}
43+
</button>
44+
</div>
45+
46+
<div class="pdf-live__stage">
47+
<PdfViewer
48+
ref="viewer"
49+
:src="SAMPLE"
50+
:worker-src="workerSrc"
51+
v-model:page="page"
52+
:render-all-pages="allPages"
53+
@load="(e) => (total = e.numPages)"
54+
/>
55+
</div>
56+
57+
<p class="pdf-live__hint">Tip: drag-select the text on a page — it's copyable.</p>
58+
</div>
59+
</ClientOnly>
60+
61+
The current page is two-way bound with `v-model:page` (the readout tracks it as you scroll in continuous mode), and the buttons drive the imperative handle (`prevPage` / `nextPage` / `zoomIn` / `zoomOut` / `fitWidth` / `rotateCW`). The worker is bundled locally here via `new URL(...)`; left unset it defaults to a CDN copy. See the [full API](/guide/pdf) for every prop, event, and handle verb.
62+
63+
## One source, six outputs
64+
65+
You author the component **once** as a `.rozie` file:
66+
67+
<<< ../../packages/ui/pdf/src/PdfViewer.rozie{html}[PdfViewer.rozie — the single source]
68+
69+
…and Rozie compiles it to six idiomatic, framework-native components. Switch the tabs to see the **actual generated output** for each target (this is exactly what ships in `@rozie-ui/pdf-{react,vue,svelte,angular,solid,lit}`):
70+
71+
::: code-group
72+
73+
<<< ../../packages/ui/pdf/packages/react/src/PdfViewer.tsx[React]
74+
<<< ../../packages/ui/pdf/packages/vue/src/PdfViewer.vue[Vue]
75+
<<< ../../packages/ui/pdf/packages/svelte/src/PdfViewer.svelte[Svelte]
76+
<<< ../../packages/ui/pdf/packages/angular/src/PdfViewer.ts[Angular]
77+
<<< ../../packages/ui/pdf/packages/solid/src/PdfViewer.tsx[Solid]
78+
<<< ../../packages/ui/pdf/packages/lit/src/PdfViewer.ts[Lit]
79+
80+
:::
81+
82+
Each is a real, idiomatic component for its framework — React `forwardRef` + hooks, Vue `<script setup>` + `defineModel`, Svelte 5 runes, an Angular standalone component, a Solid component, and a Lit custom element — with the same props, events, and 12-verb imperative handle, all from the one source above.
83+
84+
## See also
85+
86+
- [PdfViewer — showcase & API](/guide/pdf) — install, quick starts for all six frameworks, the worker setup, and the full reference.
87+
- [PDF libraries comparison](/guide/pdf-comparison) — how `@rozie-ui/pdf` stacks up against react-pdf, vue-pdf-embed, ng2-pdf-viewer, and the underserved frameworks.
88+
89+
<style scoped>
90+
.pdf-live {
91+
margin: 1.5rem 0;
92+
padding: 1rem;
93+
border: 1px solid var(--vp-c-divider);
94+
border-radius: 12px;
95+
background: var(--vp-c-bg-soft);
96+
}
97+
.pdf-live__controls {
98+
display: flex;
99+
flex-wrap: wrap;
100+
align-items: center;
101+
gap: 0.4rem;
102+
margin-bottom: 0.85rem;
103+
}
104+
.pdf-live__controls button {
105+
font: inherit;
106+
font-size: 0.82rem;
107+
padding: 0.3rem 0.7rem;
108+
border: 1px solid var(--vp-c-divider);
109+
border-radius: 7px;
110+
background: var(--vp-c-bg);
111+
color: var(--vp-c-text-1);
112+
cursor: pointer;
113+
transition: border-color 0.15s, background 0.15s;
114+
}
115+
.pdf-live__controls button:hover:not(:disabled) {
116+
border-color: var(--vp-c-brand-1);
117+
color: var(--vp-c-brand-1);
118+
}
119+
.pdf-live__controls button:disabled {
120+
opacity: 0.45;
121+
cursor: default;
122+
}
123+
.pdf-live__controls button.pdf-live__primary {
124+
background: var(--vp-c-brand-1);
125+
border-color: var(--vp-c-brand-1);
126+
color: #fff;
127+
font-weight: 600;
128+
}
129+
.pdf-live__sep {
130+
width: 1px;
131+
align-self: stretch;
132+
margin: 0 0.3rem;
133+
background: var(--vp-c-divider);
134+
}
135+
.pdf-live__readout {
136+
font-size: 0.82rem;
137+
font-variant-numeric: tabular-nums;
138+
color: var(--vp-c-text-2);
139+
}
140+
.pdf-live__stage {
141+
height: 480px;
142+
border-radius: 8px;
143+
overflow: hidden;
144+
}
145+
.pdf-live__hint {
146+
margin: 0.6rem 0 0;
147+
font-size: 0.8rem;
148+
color: var(--vp-c-text-3);
149+
}
150+
</style>

0 commit comments

Comments
 (0)