From d88384bff24158f146fe4999a69de3d711872c62 Mon Sep 17 00:00:00 2001 From: Josh McLeod Date: Wed, 4 Feb 2026 10:40:56 -0500 Subject: [PATCH 01/31] Add example pdf --- data/example.pdf | Bin 0 -> 24607 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 data/example.pdf diff --git a/data/example.pdf b/data/example.pdf new file mode 100644 index 0000000000000000000000000000000000000000..d58f17623612cf39ed52a4542f5ceacfa32ca887 GIT binary patch literal 24607 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

5o2HW1`MbZnE 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 z Date: Wed, 4 Feb 2026 10:48:33 -0500 Subject: [PATCH 02/31] Add pdf js --- README.md | 11 +++- package.json | 3 +- yarn.lock | 142 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 153 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f6c1478..235de06 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,9 @@ -# spider -Shared web components library +# Spider web components +i By RoleModel Software + +# PDF Viewer + +[PDF.js](https://mozilla.github.io/pdf.js) is awesome, but you will notice this paragraph in their setup instructions: +> The viewer is built on the display layer and is the UI for PDF viewer in Firefox and the other browser extensions within the project. It can be a good starting point for building your own viewer. However, we do ask if you plan to embed the viewer in your own site, that it not just be an unmodified version. Please re-skin it or build upon it. + +This component aims to be that skin layer built upon PDF.js packaged in a lovely drop-in web component. diff --git a/package.json b/package.json index abd1506..d91599d 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,8 @@ "build": "tsc && rsync -a src/assets dist/" }, "dependencies": { - "lit": "^3.3.2" + "lit": "^3.3.2", + "pdfjs-dist": "^5.4.624" }, "devDependencies": { "vite": "^7.2.4" diff --git a/yarn.lock b/yarn.lock index 67afd91..93c8c9a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -228,6 +228,125 @@ __metadata: languageName: node linkType: hard +"@napi-rs/canvas-android-arm64@npm:0.1.89": + version: 0.1.89 + resolution: "@napi-rs/canvas-android-arm64@npm:0.1.89" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"@napi-rs/canvas-darwin-arm64@npm:0.1.89": + version: 0.1.89 + resolution: "@napi-rs/canvas-darwin-arm64@npm:0.1.89" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@napi-rs/canvas-darwin-x64@npm:0.1.89": + version: 0.1.89 + resolution: "@napi-rs/canvas-darwin-x64@npm:0.1.89" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@napi-rs/canvas-linux-arm-gnueabihf@npm:0.1.89": + version: 0.1.89 + resolution: "@napi-rs/canvas-linux-arm-gnueabihf@npm:0.1.89" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@napi-rs/canvas-linux-arm64-gnu@npm:0.1.89": + version: 0.1.89 + resolution: "@napi-rs/canvas-linux-arm64-gnu@npm:0.1.89" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@napi-rs/canvas-linux-arm64-musl@npm:0.1.89": + version: 0.1.89 + resolution: "@napi-rs/canvas-linux-arm64-musl@npm:0.1.89" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@napi-rs/canvas-linux-riscv64-gnu@npm:0.1.89": + version: 0.1.89 + resolution: "@napi-rs/canvas-linux-riscv64-gnu@npm:0.1.89" + conditions: os=linux & cpu=riscv64 & libc=glibc + languageName: node + linkType: hard + +"@napi-rs/canvas-linux-x64-gnu@npm:0.1.89": + version: 0.1.89 + resolution: "@napi-rs/canvas-linux-x64-gnu@npm:0.1.89" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@napi-rs/canvas-linux-x64-musl@npm:0.1.89": + version: 0.1.89 + resolution: "@napi-rs/canvas-linux-x64-musl@npm:0.1.89" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@napi-rs/canvas-win32-arm64-msvc@npm:0.1.89": + version: 0.1.89 + resolution: "@napi-rs/canvas-win32-arm64-msvc@npm:0.1.89" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@napi-rs/canvas-win32-x64-msvc@npm:0.1.89": + version: 0.1.89 + resolution: "@napi-rs/canvas-win32-x64-msvc@npm:0.1.89" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@napi-rs/canvas@npm:^0.1.88": + version: 0.1.89 + resolution: "@napi-rs/canvas@npm:0.1.89" + dependencies: + "@napi-rs/canvas-android-arm64": "npm:0.1.89" + "@napi-rs/canvas-darwin-arm64": "npm:0.1.89" + "@napi-rs/canvas-darwin-x64": "npm:0.1.89" + "@napi-rs/canvas-linux-arm-gnueabihf": "npm:0.1.89" + "@napi-rs/canvas-linux-arm64-gnu": "npm:0.1.89" + "@napi-rs/canvas-linux-arm64-musl": "npm:0.1.89" + "@napi-rs/canvas-linux-riscv64-gnu": "npm:0.1.89" + "@napi-rs/canvas-linux-x64-gnu": "npm:0.1.89" + "@napi-rs/canvas-linux-x64-musl": "npm:0.1.89" + "@napi-rs/canvas-win32-arm64-msvc": "npm:0.1.89" + "@napi-rs/canvas-win32-x64-msvc": "npm:0.1.89" + dependenciesMeta: + "@napi-rs/canvas-android-arm64": + optional: true + "@napi-rs/canvas-darwin-arm64": + optional: true + "@napi-rs/canvas-darwin-x64": + optional: true + "@napi-rs/canvas-linux-arm-gnueabihf": + optional: true + "@napi-rs/canvas-linux-arm64-gnu": + optional: true + "@napi-rs/canvas-linux-arm64-musl": + optional: true + "@napi-rs/canvas-linux-riscv64-gnu": + optional: true + "@napi-rs/canvas-linux-x64-gnu": + optional: true + "@napi-rs/canvas-linux-x64-musl": + optional: true + "@napi-rs/canvas-win32-arm64-msvc": + optional: true + "@napi-rs/canvas-win32-x64-msvc": + optional: true + checksum: 10c0/e1b678b4be10b0963eee20286a1b2ea01bfb9adc36c557a712574bb077f8310a76a1ec4d6105111a9525263920589950021a6678e3b249721e57ac7550e8078b + languageName: node + linkType: hard + "@npmcli/agent@npm:^4.0.0": version: 4.0.0 resolution: "@npmcli/agent@npm:4.0.0" @@ -910,6 +1029,13 @@ __metadata: languageName: node linkType: hard +"node-readable-to-web-readable-stream@npm:^0.4.2": + version: 0.4.2 + resolution: "node-readable-to-web-readable-stream@npm:0.4.2" + checksum: 10c0/8c3d09cac51c5f886e1636fa2a5404d664245c8bdc9a65e102552894963ed1b27207d5b94de59e37045d81cb9e8970cf79e561006df7ee8821cb761e728b3a80 + languageName: node + linkType: hard + "nopt@npm:^9.0.0": version: 9.0.0 resolution: "nopt@npm:9.0.0" @@ -938,6 +1064,21 @@ __metadata: languageName: node linkType: hard +"pdfjs-dist@npm:^5.4.624": + version: 5.4.624 + resolution: "pdfjs-dist@npm:5.4.624" + dependencies: + "@napi-rs/canvas": "npm:^0.1.88" + node-readable-to-web-readable-stream: "npm:^0.4.2" + dependenciesMeta: + "@napi-rs/canvas": + optional: true + node-readable-to-web-readable-stream: + optional: true + checksum: 10c0/cebfc264ff41f7bedb60bfd2c9900be82bc19f143a1f8dd426d1e08c2093116e41c3900451633733c9ee65b18e39972e4f8de3622d443dde7d01c7eeb018dcfe + languageName: node + linkType: hard + "picocolors@npm:^1.1.1": version: 1.1.1 resolution: "picocolors@npm:1.1.1" @@ -1133,6 +1274,7 @@ __metadata: resolution: "spider@workspace:." dependencies: lit: "npm:^3.3.2" + pdfjs-dist: "npm:^5.4.624" vite: "npm:^7.2.4" languageName: unknown linkType: soft From 2d3623013c8027b52d3b3d5dd234bfee20f9d731 Mon Sep 17 00:00:00 2001 From: Josh McLeod Date: Wed, 4 Feb 2026 10:59:33 -0500 Subject: [PATCH 03/31] Add pdf worker and some UI --- index.html | 2 +- src/components/pdf-viewer/pdf-viewer.js | 121 +++++++++++++++++- .../pdf-viewer/pdf-viewer.styles.js | 72 ++++++++++- vite.config.js | 13 ++ 4 files changed, 204 insertions(+), 4 deletions(-) create mode 100644 vite.config.js diff --git a/index.html b/index.html index 61c5f56..87691b1 100644 --- a/index.html +++ b/index.html @@ -8,6 +8,6 @@ - + diff --git a/src/components/pdf-viewer/pdf-viewer.js b/src/components/pdf-viewer/pdf-viewer.js index 72d2410..c3eb688 100644 --- a/src/components/pdf-viewer/pdf-viewer.js +++ b/src/components/pdf-viewer/pdf-viewer.js @@ -1,14 +1,133 @@ import { LitElement, html } from 'lit' +import * as pdfjsLib from 'pdfjs-dist' +import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.mjs?url' import styles from './pdf-viewer.styles.js' +// Configure the worker path for PDF.js +pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker + export default class PDFViewer extends LitElement { + static get properties() { + return { + src: { type: String }, + currentPage: { type: Number }, + totalPages: { type: Number }, + scale: { type: Number } + } + } + static get styles() { return styles; } + constructor() { + super() + this.src = '' + this.currentPage = 1 + this.totalPages = 0 + this.scale = 1.5 + this.pdfDoc = null + } + + async firstUpdated() { + if (this.src) { + await this.loadPDF() + } + } + + updated(changedProperties) { + if (changedProperties.has('src') && this.src) { + this.loadPDF() + } + } + + async loadPDF() { + if (!this.src) return + + try { + const loadingTask = pdfjsLib.getDocument(this.src) + this.pdfDoc = await loadingTask.promise + this.totalPages = this.pdfDoc.numPages + this.currentPage = 1 + await this.renderPage(this.currentPage) + } catch (error) { + console.error('Error loading PDF:', error) + } + } + + async renderPage(pageNumber) { + if (!this.pdfDoc) return + + try { + const page = await this.pdfDoc.getPage(pageNumber) + const canvas = this.shadowRoot.querySelector('#pdf-canvas') + const context = canvas.getContext('2d') + + const viewport = page.getViewport({ scale: this.scale }) + canvas.height = viewport.height + canvas.width = viewport.width + + const renderContext = { + canvasContext: context, + viewport: viewport + } + + await page.render(renderContext).promise + } catch (error) { + console.error('Error rendering page:', error) + } + } + + previousPage() { + if (this.currentPage <= 1) return + this.currentPage-- + this.renderPage(this.currentPage) + } + + nextPage() { + if (this.currentPage >= this.totalPages) return + this.currentPage++ + this.renderPage(this.currentPage) + } + + zoomIn() { + this.scale += 0.25 + this.renderPage(this.currentPage) + } + + zoomOut() { + if (this.scale <= 0.5) return + this.scale -= 0.25 + this.renderPage(this.currentPage) + } + render() { return html` -

We are ago
+
+
+ + + Page ${this.currentPage} of ${this.totalPages} + + +
+ + ${Math.round(this.scale * 100)}% + +
+
+
+ +
+
` } } diff --git a/src/components/pdf-viewer/pdf-viewer.styles.js b/src/components/pdf-viewer/pdf-viewer.styles.js index 9bbf3ec..a182b5e 100644 --- a/src/components/pdf-viewer/pdf-viewer.styles.js +++ b/src/components/pdf-viewer/pdf-viewer.styles.js @@ -1,7 +1,75 @@ import { css } from 'lit'; export default css` - .test { - color: red; + :host { + display: block; + width: 100%; + height: 100vh; + font-family: system-ui, -apple-system, sans-serif; + } + + .pdf-viewer-container { + display: flex; + flex-direction: column; + height: 100%; + background-color: #525659; + } + + .toolbar { + display: flex; + align-items: center; + gap: 1rem; + padding: 1rem; + background-color: #323639; + color: white; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + } + + .zoom-controls { + display: flex; + align-items: center; + gap: 0.5rem; + margin-left: auto; + } + + button { + padding: 0.5rem 1rem; + background-color: #0d6efd; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + transition: background-color 0.2s; + } + + button:hover:not(:disabled) { + background-color: #0b5ed7; + } + + button:disabled { + background-color: #6c757d; + cursor: not-allowed; + opacity: 0.6; + } + + .page-info, + .zoom-level { + padding: 0.5rem; + color: white; + font-size: 14px; + } + + .canvas-container { + flex: 1; + overflow: auto; + display: flex; + justify-content: center; + padding: 2rem; + } + + #pdf-canvas { + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); + background: white; } `; diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..b4a81aa --- /dev/null +++ b/vite.config.js @@ -0,0 +1,13 @@ +export default { + optimizeDeps: { + exclude: ['pdfjs-dist'] + }, + server: { + fs: { + strict: false + } + }, + worker: { + format: 'es' + } +} From d68391446784d4b7694d95132d16de910f4a49d9 Mon Sep 17 00:00:00 2001 From: Josh McLeod Date: Wed, 4 Feb 2026 11:13:05 -0500 Subject: [PATCH 04/31] Add copilot instructions --- .github/instructions/css.instructions.md | 16 ++++++++++++++++ .github/instructions/js.instructions.md | 11 +++++++++++ .github/instructions/project.instructions.md | 18 ++++++++++++++++++ 3 files changed, 45 insertions(+) create mode 100644 .github/instructions/css.instructions.md create mode 100644 .github/instructions/js.instructions.md create mode 100644 .github/instructions/project.instructions.md diff --git a/.github/instructions/css.instructions.md b/.github/instructions/css.instructions.md new file mode 100644 index 0000000..6162384 --- /dev/null +++ b/.github/instructions/css.instructions.md @@ -0,0 +1,16 @@ +--- +applyTo: '**/*.css,**/*.scss' +--- + +SCSS is used for styling, but avoid using mixins. Use BEM methodology for class naming. + +## CSS Library +We use Optics for our CSS library. It provides a set of components and tokens that should be used +to maintain consistency across the application. Consider if the component you are creating should be a modifier of an existing component before creating a new one. + +## CSS Variables +Avoid adding in fallbacks for CSS variables. See the list of available Optics tokens in the +`.github/docs/optics/tokens.json` file. + +## Specificity +Never use `!important`. Always make the selector more specific to solve those issues. diff --git a/.github/instructions/js.instructions.md b/.github/instructions/js.instructions.md new file mode 100644 index 0000000..52c829e --- /dev/null +++ b/.github/instructions/js.instructions.md @@ -0,0 +1,11 @@ +--- +applyTo: "**/*.js,**/*.mjs,**/*.cjs" +--- + +- Prefer ESM syntax for JavaScript files, and avoid using CommonJS syntax unless the file is already CommonJS. +- Important: Never use semicolons at the end of statements. +- Use single quotes for strings, except when the string contains a single quote, in which case use double quotes. +- Use `const` for variables that are not reassigned, and `let` for variables that are reassigned. + +## Frameworks and Libraries +- Use [Lit](https://lit.dev/) for building web components. diff --git a/.github/instructions/project.instructions.md b/.github/instructions/project.instructions.md new file mode 100644 index 0000000..4c1206d --- /dev/null +++ b/.github/instructions/project.instructions.md @@ -0,0 +1,18 @@ +--- +applyTo: '**/*' +--- +## Project Context +This project is a collection of web components developed by RoleModel Software, designed to enhance web applications with reusable UI elements. The PDF Viewer component leverages PDF.js to provide a customizable and embeddable PDF viewing experience. + +## General +- Do not create summary markdown files after making changes + +## Tech Stack +- Lit for web components +- PDF.js for PDF rendering + +## Response +- Provide evidence-based responses to feedback, focusing on technical accuracy and clarity. +- Maintain a professional and constructive tone in all communications. +- Avoid unnecessary embellishments or emotional language; focus on the task at hand. +— Avoid unnecessary comments; the code should be self-explanatory. From bea8b8a75c6fb5eb5db7eaa58fae394fb35a3816 Mon Sep 17 00:00:00 2001 From: Josh McLeod Date: Wed, 4 Feb 2026 11:28:18 -0500 Subject: [PATCH 05/31] Add component hierarchy --- .yarnrc.yml | 1 + package-lock.json | 1432 +++++++++++++++++ src/components/pdf-viewer/README.md | 146 ++ .../pdf-viewer/canvas/pdf-canvas.js | 117 ++ .../pdf-viewer/canvas/pdf-canvas.styles.js | 24 + src/components/pdf-viewer/index.js | 2 + src/components/pdf-viewer/pdf-viewer.js | 172 +- .../pdf-viewer/pdf-viewer.styles.js | 65 +- .../pdf-viewer/sidebar/pdf-sidebar.js | 97 ++ .../pdf-viewer/sidebar/pdf-sidebar.styles.js | 22 + .../pdf-viewer/thumbnail/pdf-thumbnail.js | 82 + .../thumbnail/pdf-thumbnail.styles.js | 41 + .../pdf-viewer/toolbar/pdf-toolbar.js | 109 ++ .../pdf-viewer/toolbar/pdf-toolbar.styles.js | 54 + 14 files changed, 2226 insertions(+), 138 deletions(-) create mode 100644 .yarnrc.yml create mode 100644 package-lock.json create mode 100644 src/components/pdf-viewer/README.md create mode 100644 src/components/pdf-viewer/canvas/pdf-canvas.js create mode 100644 src/components/pdf-viewer/canvas/pdf-canvas.styles.js create mode 100644 src/components/pdf-viewer/index.js create mode 100644 src/components/pdf-viewer/sidebar/pdf-sidebar.js create mode 100644 src/components/pdf-viewer/sidebar/pdf-sidebar.styles.js create mode 100644 src/components/pdf-viewer/thumbnail/pdf-thumbnail.js create mode 100644 src/components/pdf-viewer/thumbnail/pdf-thumbnail.styles.js create mode 100644 src/components/pdf-viewer/toolbar/pdf-toolbar.js create mode 100644 src/components/pdf-viewer/toolbar/pdf-toolbar.styles.js diff --git a/.yarnrc.yml b/.yarnrc.yml new file mode 100644 index 0000000..3186f3f --- /dev/null +++ b/.yarnrc.yml @@ -0,0 +1 @@ +nodeLinker: node-modules diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..0113e08 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1432 @@ +{ + "name": "spider", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "spider", + "version": "0.0.1", + "license": "MIT", + "dependencies": { + "lit": "^3.3.2", + "pdfjs-dist": "^5.4.624" + }, + "devDependencies": { + "vite": "^7.2.4" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@lit-labs/ssr-dom-shim": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.5.1.tgz", + "integrity": "sha512-Aou5UdlSpr5whQe8AA/bZG0jMj96CoJIWbGfZ91qieWu5AWUMKw8VR/pAkQkJYvBNhmCcWnZlyyk5oze8JIqYA==", + "license": "BSD-3-Clause" + }, + "node_modules/@lit/reactive-element": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.1.2.tgz", + "integrity": "sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.5.0" + } + }, + "node_modules/@napi-rs/canvas": { + "version": "0.1.89", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.89.tgz", + "integrity": "sha512-7GjmkMirJHejeALCqUnZY3QwID7bbumOiLrqq2LKgxrdjdmxWQBTc6rcASa2u8wuWrH7qo4/4n/VNrOwCoKlKg==", + "license": "MIT", + "optional": true, + "workspaces": [ + "e2e/*" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/canvas-android-arm64": "0.1.89", + "@napi-rs/canvas-darwin-arm64": "0.1.89", + "@napi-rs/canvas-darwin-x64": "0.1.89", + "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.89", + "@napi-rs/canvas-linux-arm64-gnu": "0.1.89", + "@napi-rs/canvas-linux-arm64-musl": "0.1.89", + "@napi-rs/canvas-linux-riscv64-gnu": "0.1.89", + "@napi-rs/canvas-linux-x64-gnu": "0.1.89", + "@napi-rs/canvas-linux-x64-musl": "0.1.89", + "@napi-rs/canvas-win32-arm64-msvc": "0.1.89", + "@napi-rs/canvas-win32-x64-msvc": "0.1.89" + } + }, + "node_modules/@napi-rs/canvas-android-arm64": { + "version": "0.1.89", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.89.tgz", + "integrity": "sha512-CXxQTXsjtQqKGENS8Ejv9pZOFJhOPIl2goenS+aU8dY4DygvkyagDhy/I07D1YLqrDtPvLEX5zZHt8qUdnuIpQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-darwin-arm64": { + "version": "0.1.89", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.89.tgz", + "integrity": "sha512-k29cR/Zl20WLYM7M8YePevRu2VQRaKcRedYr1V/8FFHkyIQ8kShEV+MPoPGi+znvmd17Eqjy2Pk2F2kpM2umVg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-darwin-x64": { + "version": "0.1.89", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.89.tgz", + "integrity": "sha512-iUragqhBrA5FqU13pkhYBDbUD1WEAIlT8R2+fj6xHICY2nemzwMUI8OENDhRh7zuL06YDcRwENbjAVxOmaX9jg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { + "version": "0.1.89", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.89.tgz", + "integrity": "sha512-y3SM9sfDWasY58ftoaI09YBFm35Ig8tosZqgahLJ2WGqawCusGNPV9P0/4PsrLOCZqGg629WxexQMY25n7zcvA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-gnu": { + "version": "0.1.89", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.89.tgz", + "integrity": "sha512-NEoF9y8xq5fX8HG8aZunBom1ILdTwt7ayBzSBIwrmitk7snj4W6Fz/yN/ZOmlM1iyzHDNX5Xn0n+VgWCF8BEdA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-musl": { + "version": "0.1.89", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.89.tgz", + "integrity": "sha512-UQQkIEzV12/l60j1ziMjZ+mtodICNUbrd205uAhbyTw0t60CrC/EsKb5/aJWGq1wM0agvcgZV72JJCKfLS6+4w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-riscv64-gnu": { + "version": "0.1.89", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.89.tgz", + "integrity": "sha512-1/VmEoFaIO6ONeeEMGoWF17wOYZOl5hxDC1ios2Bkz/oQjbJJ8DY/X22vWTmvuUKWWhBVlo63pxLGZbjJU/heA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-gnu": { + "version": "0.1.89", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.89.tgz", + "integrity": "sha512-ebLuqkCuaPIkKgKH9q4+pqWi1tkPOfiTk5PM1LKR1tB9iO9sFNVSIgwEp+SJreTSbA2DK5rW8lQXiN78SjtcvA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-musl": { + "version": "0.1.89", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.89.tgz", + "integrity": "sha512-w+5qxHzplvA4BkHhCaizNMLLXiI+CfP84YhpHm/PqMub4u8J0uOAv+aaGv40rYEYra5hHRWr9LUd6cfW32o9/A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-win32-arm64-msvc": { + "version": "0.1.89", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-0.1.89.tgz", + "integrity": "sha512-DmyXa5lJHcjOsDC78BM3bnEECqbK3xASVMrKfvtT/7S7Z8NGQOugvu+L7b41V6cexCd34mBWgMOsjoEBceeB1Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-win32-x64-msvc": { + "version": "0.1.89", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.89.tgz", + "integrity": "sha512-WMej0LZrIqIncQcx0JHaMXlnAG7sncwJh7obs/GBgp0xF9qABjwoRwIooMWCZkSansapKGNUHhamY6qEnFN7gA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/lit": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/lit/-/lit-3.3.2.tgz", + "integrity": "sha512-NF9zbsP79l4ao2SNrH3NkfmFgN/hBYSQo90saIVI1o5GpjAdCPVstVzO1MrLOakHoEhYkrtRjPK6Ob521aoYWQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit/reactive-element": "^2.1.0", + "lit-element": "^4.2.0", + "lit-html": "^3.3.0" + } + }, + "node_modules/lit-element": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.2.2.tgz", + "integrity": "sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.5.0", + "@lit/reactive-element": "^2.1.0", + "lit-html": "^3.3.0" + } + }, + "node_modules/lit-html": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.2.tgz", + "integrity": "sha512-Qy9hU88zcmaxBXcc10ZpdK7cOLXvXpRoBxERdtqV9QOrfpMZZ6pSYP91LhpPtap3sFMUiL7Tw2RImbe0Al2/kw==", + "license": "BSD-3-Clause", + "dependencies": { + "@types/trusted-types": "^2.0.2" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-readable-to-web-readable-stream": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/node-readable-to-web-readable-stream/-/node-readable-to-web-readable-stream-0.4.2.tgz", + "integrity": "sha512-/cMZNI34v//jUTrI+UIo4ieHAB5EZRY/+7OmXZgBxaWBMcW2tGdceIw06RFxWxrKZ5Jp3sI2i5TsRo+CBhtVLQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pdfjs-dist": { + "version": "5.4.624", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.4.624.tgz", + "integrity": "sha512-sm6TxKTtWv1Oh6n3C6J6a8odejb5uO4A4zo/2dgkHuC0iu8ZMAXOezEODkVaoVp8nX1Xzr+0WxFJJmUr45hQzg==", + "license": "Apache-2.0", + "engines": { + "node": ">=20.16.0 || >=22.3.0" + }, + "optionalDependencies": { + "@napi-rs/canvas": "^0.1.88", + "node-readable-to-web-readable-stream": "^0.4.2" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/src/components/pdf-viewer/README.md b/src/components/pdf-viewer/README.md new file mode 100644 index 0000000..a275930 --- /dev/null +++ b/src/components/pdf-viewer/README.md @@ -0,0 +1,146 @@ +# PDF Viewer Web Component + +A composable PDF viewer web component built with Lit and PDF.js. + +## Features + +- 📄 Full PDF rendering with PDF.js +- 🖼️ Thumbnail navigation sidebar +- 🔍 Zoom controls (in/out) +- ⏮️ Page navigation (previous/next) +- 📱 Responsive layout +- 🎨 Customizable through CSS +- 🔧 Composable architecture +- 🎯 Framework agnostic + +## Installation + +Simply copy the entire `pdf-viewer` directory into your project. + +## Usage + +### Basic Usage + +```html + + + + + + + + + +``` + +### Properties + +| Property | Type | Description | +|----------|--------|--------------------------| +| `src` | String | Path to the PDF file | + +### Events + +The component emits the following events: + +#### `pdf-loaded` +Fired when PDF is successfully loaded. +```javascript +viewer.addEventListener('pdf-loaded', (e) => { + console.log(`Total pages: ${e.detail.totalPages}`) +}) +``` + +#### `page-change` +Fired when the current page changes. +```javascript +viewer.addEventListener('page-change', (e) => { + console.log(`Current page: ${e.detail.pageNumber}`) +}) +``` + +#### `scale-change` +Fired when zoom scale changes. +```javascript +viewer.addEventListener('scale-change', (e) => { + console.log(`Zoom: ${Math.round(e.detail.scale * 100)}%`) +}) +``` + +#### `pdf-error` +Fired when PDF fails to load. +```javascript +viewer.addEventListener('pdf-error', (e) => { + console.error('Failed to load PDF:', e.detail.error) +}) +``` + +### JavaScript API + +```javascript +const viewer = document.querySelector('pdf-viewer') + +// Change PDF source dynamically +viewer.src = '/path/to/different.pdf' +``` + +## Architecture + +The component uses a composable architecture with a root component and child components: + +``` +pdf-viewer (root) +├── toolbar - Navigation and zoom controls +├── sidebar - Thumbnail navigation +│ └── thumbnail +├── canvas - Main PDF display +``` + +### Component Structure + +``` +pdf-viewer/ +├── pdf-viewer.js # Root component +├── pdf-viewer.styles.js # Root styles +├── index.js # Exports +├── toolbar/ # Toolbar component +│ ├── pdf-toolbar.js +│ └── pdf-toolbar.styles.js +├── sidebar/ # Sidebar component +│ ├── pdf-sidebar.js +│ └── pdf-sidebar.styles.js +├── canvas/ # Canvas component +│ ├── pdf-canvas.js +│ └── pdf-canvas.styles.js +└── thumbnail/ # Thumbnail component + ├── pdf-thumbnail.js + └── pdf-thumbnail.styles.js +``` + +## Dependencies + +- [Lit](https://lit.dev/) - Web component framework +- [PDF.js](https://mozilla.github.io/pdf.js/) - PDF rendering library + +## Browser Support + +Works in all modern browsers that support: +- Web Components (Custom Elements v1) +- Shadow DOM v1 +- ES Modules + +## Customization + +The component uses Shadow DOM with CSS custom properties for styling. Each sub-component has its own styles file that can be customized. + +## Context API + +Child components access shared state through a Symbol-based context pattern. See `COMPONENT-ARCHITECTURE.md` for technical details. + +## License + +See the main project LICENSE file. + +## Credits + +Built by RoleModel Software diff --git a/src/components/pdf-viewer/canvas/pdf-canvas.js b/src/components/pdf-viewer/canvas/pdf-canvas.js new file mode 100644 index 0000000..39cb8a1 --- /dev/null +++ b/src/components/pdf-viewer/canvas/pdf-canvas.js @@ -0,0 +1,117 @@ +import { LitElement, html } from 'lit' +import styles from './pdf-canvas.styles.js' + +const PDFContext = Symbol('pdf-context') + +export default class PDFCanvas extends LitElement { + static get properties() { + return { + _currentPage: { type: Number, state: true }, + _scale: { type: Number, state: true } + } + } + + static get styles() { + return styles + } + + constructor() { + super() + this._currentPage = 1 + this._scale = 1.5 + this._context = null + } + + connectedCallback() { + super.connectedCallback() + this._findContext() + this.addEventListener('pdf-loaded', this._handlePDFLoaded) + this.addEventListener('page-change', this._handlePageChange) + this.addEventListener('scale-change', this._handleScaleChange) + } + + disconnectedCallback() { + super.disconnectedCallback() + this.removeEventListener('pdf-loaded', this._handlePDFLoaded) + this.removeEventListener('page-change', this._handlePageChange) + this.removeEventListener('scale-change', this._handleScaleChange) + } + + _findContext() { + let node = this.parentElement + while (node) { + if (node.tagName === 'PDF-VIEWER' && node[PDFContext]) { + this._context = node[PDFContext] + this._updateFromContext() + break + } + node = node.parentElement + } + } + + _updateFromContext() { + if (!this._context) return + this._currentPage = this._context.getCurrentPage() + this._scale = this._context.getScale() + } + + _handlePDFLoaded = async () => { + this._updateFromContext() + await this.renderPage(this._currentPage) + } + + _handlePageChange = async () => { + this._updateFromContext() + await this.renderPage(this._currentPage) + } + + _handleScaleChange = async () => { + this._updateFromContext() + await this.renderPage(this._currentPage) + } + + async firstUpdated() { + if (this._context && this._context.getPdfDoc()) { + await this.renderPage(this._currentPage) + } + } + + async renderPage(pageNumber) { + if (!this._context) return + const pdfDoc = this._context.getPdfDoc() + if (!pdfDoc) return + + try { + const page = await pdfDoc.getPage(pageNumber) + const canvas = this.shadowRoot.querySelector('#pdf-canvas') + if (!canvas) return + + const context = canvas.getContext('2d') + const viewport = page.getViewport({ scale: this._scale }) + + canvas.height = viewport.height + canvas.width = viewport.width + + const renderContext = { + canvasContext: context, + viewport: viewport + } + + await page.render(renderContext).promise + } catch (error) { + console.error('Error rendering page:', error) + } + } + + render() { + return html` +
+ +
+ ` + } +} + +customElements.define('pdf-canvas', PDFCanvas) + +export { PDFContext } diff --git a/src/components/pdf-viewer/canvas/pdf-canvas.styles.js b/src/components/pdf-viewer/canvas/pdf-canvas.styles.js new file mode 100644 index 0000000..7d74b78 --- /dev/null +++ b/src/components/pdf-viewer/canvas/pdf-canvas.styles.js @@ -0,0 +1,24 @@ +import { css } from 'lit' + +export default css` + :host { + display: block; + width: 100%; + height: 100%; + } + + .canvas-container { + flex: 1; + overflow: auto; + display: flex; + justify-content: center; + align-items: flex-start; + padding: 2rem; + background: #e9e9e9; + } + + canvas { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + background: white; + } +` diff --git a/src/components/pdf-viewer/index.js b/src/components/pdf-viewer/index.js new file mode 100644 index 0000000..164dceb --- /dev/null +++ b/src/components/pdf-viewer/index.js @@ -0,0 +1,2 @@ +export { default as PDFViewer } from './pdf-viewer.js' +export { PDFContext } from './pdf-viewer.js' diff --git a/src/components/pdf-viewer/pdf-viewer.js b/src/components/pdf-viewer/pdf-viewer.js index c3eb688..f792113 100644 --- a/src/components/pdf-viewer/pdf-viewer.js +++ b/src/components/pdf-viewer/pdf-viewer.js @@ -2,31 +2,99 @@ import { LitElement, html } from 'lit' import * as pdfjsLib from 'pdfjs-dist' import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.mjs?url' import styles from './pdf-viewer.styles.js' +import './toolbar/pdf-toolbar.js' +import './sidebar/pdf-sidebar.js' +import './canvas/pdf-canvas.js' -// Configure the worker path for PDF.js pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker +const PDFContext = Symbol('pdf-context') + export default class PDFViewer extends LitElement { static get properties() { return { - src: { type: String }, - currentPage: { type: Number }, - totalPages: { type: Number }, - scale: { type: Number } + src: { type: String } } } static get styles() { - return styles; + return styles } constructor() { super() this.src = '' + this.pdfDoc = null this.currentPage = 1 this.totalPages = 0 this.scale = 1.5 - this.pdfDoc = null + + this[PDFContext] = { + getPdfDoc: () => this.pdfDoc, + getCurrentPage: () => this.currentPage, + getTotalPages: () => this.totalPages, + getScale: () => this.scale, + setCurrentPage: (page) => { + this.currentPage = page + this.requestUpdate() + this.dispatchEvent(new CustomEvent('page-change', { + detail: { pageNumber: page }, + bubbles: true, + composed: true + })) + }, + setScale: (scale) => { + this.scale = scale + this.requestUpdate() + this.dispatchEvent(new CustomEvent('scale-change', { + detail: { scale }, + bubbles: true, + composed: true + })) + }, + nextPage: () => { + if (this.currentPage < this.totalPages) { + this.currentPage++ + this.requestUpdate() + this.dispatchEvent(new CustomEvent('page-change', { + detail: { pageNumber: this.currentPage }, + bubbles: true, + composed: true + })) + } + }, + previousPage: () => { + if (this.currentPage > 1) { + this.currentPage-- + this.requestUpdate() + this.dispatchEvent(new CustomEvent('page-change', { + detail: { pageNumber: this.currentPage }, + bubbles: true, + composed: true + })) + } + }, + zoomIn: () => { + this.scale += 0.25 + this.requestUpdate() + this.dispatchEvent(new CustomEvent('scale-change', { + detail: { scale: this.scale }, + bubbles: true, + composed: true + })) + }, + zoomOut: () => { + if (this.scale > 0.5) { + this.scale -= 0.25 + this.requestUpdate() + this.dispatchEvent(new CustomEvent('scale-change', { + detail: { scale: this.scale }, + bubbles: true, + composed: true + })) + } + } + } } async firstUpdated() { @@ -49,88 +117,38 @@ export default class PDFViewer extends LitElement { this.pdfDoc = await loadingTask.promise this.totalPages = this.pdfDoc.numPages this.currentPage = 1 - await this.renderPage(this.currentPage) + this.requestUpdate() + this.dispatchEvent(new CustomEvent('pdf-loaded', { + detail: { + totalPages: this.totalPages, + pdfDoc: this.pdfDoc + }, + bubbles: true, + composed: true + })) } catch (error) { console.error('Error loading PDF:', error) + this.dispatchEvent(new CustomEvent('pdf-error', { + detail: { error }, + bubbles: true, + composed: true + })) } } - async renderPage(pageNumber) { - if (!this.pdfDoc) return - - try { - const page = await this.pdfDoc.getPage(pageNumber) - const canvas = this.shadowRoot.querySelector('#pdf-canvas') - const context = canvas.getContext('2d') - - const viewport = page.getViewport({ scale: this.scale }) - canvas.height = viewport.height - canvas.width = viewport.width - - const renderContext = { - canvasContext: context, - viewport: viewport - } - - await page.render(renderContext).promise - } catch (error) { - console.error('Error rendering page:', error) - } - } - - previousPage() { - if (this.currentPage <= 1) return - this.currentPage-- - this.renderPage(this.currentPage) - } - - nextPage() { - if (this.currentPage >= this.totalPages) return - this.currentPage++ - this.renderPage(this.currentPage) - } - - zoomIn() { - this.scale += 0.25 - this.renderPage(this.currentPage) - } - - zoomOut() { - if (this.scale <= 0.5) return - this.scale -= 0.25 - this.renderPage(this.currentPage) - } - render() { return html`
-
- - - Page ${this.currentPage} of ${this.totalPages} - - -
- - ${Math.round(this.scale * 100)}% - -
-
-
- + +
+ +
` } } -customElements.define('pdf-viewer', PDFViewer); +customElements.define('pdf-viewer', PDFViewer) +export { PDFContext } diff --git a/src/components/pdf-viewer/pdf-viewer.styles.js b/src/components/pdf-viewer/pdf-viewer.styles.js index a182b5e..336a14e 100644 --- a/src/components/pdf-viewer/pdf-viewer.styles.js +++ b/src/components/pdf-viewer/pdf-viewer.styles.js @@ -1,75 +1,18 @@ -import { css } from 'lit'; - +import { css } from 'lit' export default css` :host { display: block; width: 100%; height: 100vh; - font-family: system-ui, -apple-system, sans-serif; } - .pdf-viewer-container { display: flex; flex-direction: column; height: 100%; - background-color: #525659; } - - .toolbar { + .content-container { display: flex; - align-items: center; - gap: 1rem; - padding: 1rem; - background-color: #323639; - color: white; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); - } - - .zoom-controls { - display: flex; - align-items: center; - gap: 0.5rem; - margin-left: auto; - } - - button { - padding: 0.5rem 1rem; - background-color: #0d6efd; - color: white; - border: none; - border-radius: 4px; - cursor: pointer; - font-size: 14px; - transition: background-color 0.2s; - } - - button:hover:not(:disabled) { - background-color: #0b5ed7; - } - - button:disabled { - background-color: #6c757d; - cursor: not-allowed; - opacity: 0.6; - } - - .page-info, - .zoom-level { - padding: 0.5rem; - color: white; - font-size: 14px; - } - - .canvas-container { flex: 1; - overflow: auto; - display: flex; - justify-content: center; - padding: 2rem; - } - - #pdf-canvas { - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); - background: white; + overflow: hidden; } -`; +` diff --git a/src/components/pdf-viewer/sidebar/pdf-sidebar.js b/src/components/pdf-viewer/sidebar/pdf-sidebar.js new file mode 100644 index 0000000..501bdd9 --- /dev/null +++ b/src/components/pdf-viewer/sidebar/pdf-sidebar.js @@ -0,0 +1,97 @@ +import { LitElement, html } from 'lit' +import styles from './pdf-sidebar.styles.js' +import '../thumbnail/pdf-thumbnail.js' + +const PDFContext = Symbol('pdf-context') + +export default class PDFSidebar extends LitElement { + static get properties() { + return { + _currentPage: { type: Number, state: true }, + _totalPages: { type: Number, state: true } + } + } + + static get styles() { + return styles + } + + constructor() { + super() + this._currentPage = 1 + this._totalPages = 0 + this._context = null + } + + connectedCallback() { + super.connectedCallback() + this._findContext() + this.addEventListener('pdf-loaded', this._handleUpdate) + this.addEventListener('page-change', this._handleUpdate) + } + + disconnectedCallback() { + super.disconnectedCallback() + this.removeEventListener('pdf-loaded', this._handleUpdate) + this.removeEventListener('page-change', this._handleUpdate) + } + + _findContext() { + let node = this.parentElement + while (node) { + if (node.tagName === 'PDF-VIEWER' && node[PDFContext]) { + this._context = node[PDFContext] + this._updateFromContext() + break + } + node = node.parentElement + } + } + + _updateFromContext() { + if (!this._context) return + this._currentPage = this._context.getCurrentPage() + this._totalPages = this._context.getTotalPages() + } + + _handleUpdate = () => { + this._updateFromContext() + } + + handleThumbnailClick(e) { + this._context?.setCurrentPage(e.detail.pageNumber) + } + + renderThumbnails() { + if (!this._context) return html`` + const pdfDoc = this._context.getPdfDoc() + if (!pdfDoc || this._totalPages === 0) return html`` + + const thumbnails = [] + for (let i = 1; i <= this._totalPages; i++) { + thumbnails.push(html` + + `) + } + return thumbnails + } + + render() { + return html` + + ` + } +} + +customElements.define('pdf-sidebar', PDFSidebar) + +export { PDFContext } diff --git a/src/components/pdf-viewer/sidebar/pdf-sidebar.styles.js b/src/components/pdf-viewer/sidebar/pdf-sidebar.styles.js new file mode 100644 index 0000000..a134a05 --- /dev/null +++ b/src/components/pdf-viewer/sidebar/pdf-sidebar.styles.js @@ -0,0 +1,22 @@ +import { css } from 'lit' + +export default css` + :host { + display: block; + } + + .sidebar { + width: 200px; + background: #fafafa; + border-right: 1px solid #ddd; + overflow-y: auto; + height: 100%; + } + + .thumbnails-container { + padding: 1rem; + display: flex; + flex-direction: column; + gap: 1rem; + } +` diff --git a/src/components/pdf-viewer/thumbnail/pdf-thumbnail.js b/src/components/pdf-viewer/thumbnail/pdf-thumbnail.js new file mode 100644 index 0000000..55fcce8 --- /dev/null +++ b/src/components/pdf-viewer/thumbnail/pdf-thumbnail.js @@ -0,0 +1,82 @@ +import { LitElement, html } from 'lit' +import styles from './pdf-thumbnail.styles.js' + +export default class PDFThumbnail extends LitElement { + static get properties() { + return { + pageNumber: { type: Number }, + pdfDoc: { type: Object }, + scale: { type: Number }, + isActive: { type: Boolean } + } + } + + static get styles() { + return styles + } + + constructor() { + super() + this.pageNumber = 1 + this.pdfDoc = null + this.scale = 0.3 + this.isActive = false + } + + async firstUpdated() { + await this.renderThumbnail() + } + + async updated(changedProperties) { + if (changedProperties.has('pdfDoc') || changedProperties.has('pageNumber')) { + await this.renderThumbnail() + } + } + + async renderThumbnail() { + if (!this.pdfDoc) return + + try { + const page = await this.pdfDoc.getPage(this.pageNumber) + const canvas = this.shadowRoot.querySelector('#thumbnail-canvas') + if (!canvas) return + + const context = canvas.getContext('2d') + const viewport = page.getViewport({ scale: this.scale }) + + canvas.height = viewport.height + canvas.width = viewport.width + + const renderContext = { + canvasContext: context, + viewport: viewport + } + + await page.render(renderContext).promise + } catch (error) { + console.error('Error rendering thumbnail:', error) + } + } + + handleClick() { + this.dispatchEvent(new CustomEvent('thumbnail-click', { + detail: { pageNumber: this.pageNumber }, + bubbles: true, + composed: true + })) + } + + render() { + return html` +
+ +
${this.pageNumber}
+
+ ` + } +} + +customElements.define('pdf-thumbnail', PDFThumbnail) diff --git a/src/components/pdf-viewer/thumbnail/pdf-thumbnail.styles.js b/src/components/pdf-viewer/thumbnail/pdf-thumbnail.styles.js new file mode 100644 index 0000000..d701af1 --- /dev/null +++ b/src/components/pdf-viewer/thumbnail/pdf-thumbnail.styles.js @@ -0,0 +1,41 @@ +import { css } from 'lit' + +export default css` + :host { + display: block; + cursor: pointer; + } + + .thumbnail-container { + padding: 8px; + border: 2px solid transparent; + border-radius: 4px; + transition: all 0.2s ease; + background: white; + } + + .thumbnail-container:hover { + border-color: #4285f4; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + } + + .thumbnail-container.active { + border-color: #1967d2; + background: #e8f0fe; + } + + canvas { + display: block; + width: 100%; + height: auto; + border-radius: 2px; + } + + .page-label { + text-align: center; + margin-top: 4px; + font-size: 12px; + color: #5f6368; + font-weight: 500; + } +` diff --git a/src/components/pdf-viewer/toolbar/pdf-toolbar.js b/src/components/pdf-viewer/toolbar/pdf-toolbar.js new file mode 100644 index 0000000..817eca2 --- /dev/null +++ b/src/components/pdf-viewer/toolbar/pdf-toolbar.js @@ -0,0 +1,109 @@ +import { LitElement, html } from 'lit' +import styles from './pdf-toolbar.styles.js' + +const PDFContext = Symbol('pdf-context') + +export default class PDFToolbar extends LitElement { + static get properties() { + return { + _currentPage: { type: Number, state: true }, + _totalPages: { type: Number, state: true }, + _scale: { type: Number, state: true } + } + } + + static get styles() { + return styles + } + + constructor() { + super() + this._currentPage = 1 + this._totalPages = 0 + this._scale = 1.5 + this._context = null + } + + connectedCallback() { + super.connectedCallback() + this._findContext() + this.addEventListener('pdf-loaded', this._handleUpdate) + this.addEventListener('page-change', this._handleUpdate) + this.addEventListener('scale-change', this._handleUpdate) + } + + disconnectedCallback() { + super.disconnectedCallback() + this.removeEventListener('pdf-loaded', this._handleUpdate) + this.removeEventListener('page-change', this._handleUpdate) + this.removeEventListener('scale-change', this._handleUpdate) + } + + _findContext() { + let node = this.parentElement + while (node) { + if (node.tagName === 'PDF-VIEWER' && node[PDFContext]) { + this._context = node[PDFContext] + this._updateFromContext() + break + } + node = node.parentElement + } + } + + _updateFromContext() { + if (!this._context) return + this._currentPage = this._context.getCurrentPage() + this._totalPages = this._context.getTotalPages() + this._scale = this._context.getScale() + } + + _handleUpdate = () => { + this._updateFromContext() + } + + previousPage() { + this._context?.previousPage() + } + + nextPage() { + this._context?.nextPage() + } + + zoomIn() { + this._context?.zoomIn() + } + + zoomOut() { + this._context?.zoomOut() + } + + render() { + return html` +
+ + + Page ${this._currentPage} of ${this._totalPages} + + +
+ + ${Math.round(this._scale * 100)}% + +
+
+ ` + } +} + +customElements.define('pdf-toolbar', PDFToolbar) + +export { PDFContext } diff --git a/src/components/pdf-viewer/toolbar/pdf-toolbar.styles.js b/src/components/pdf-viewer/toolbar/pdf-toolbar.styles.js new file mode 100644 index 0000000..c493597 --- /dev/null +++ b/src/components/pdf-viewer/toolbar/pdf-toolbar.styles.js @@ -0,0 +1,54 @@ +import { css } from 'lit' + +export default css` + :host { + display: block; + } + + .toolbar { + padding: 1rem; + background: #f5f5f5; + border-bottom: 1px solid #ddd; + display: flex; + align-items: center; + gap: 1rem; + flex-wrap: wrap; + } + + button { + padding: 0.5rem 1rem; + border: 1px solid #ccc; + background: white; + border-radius: 4px; + cursor: pointer; + font-size: 0.9rem; + } + + button:hover:not(:disabled) { + background: #e9e9e9; + } + + button:disabled { + opacity: 0.5; + cursor: not-allowed; + } + + .page-info { + font-weight: 500; + color: #333; + } + + .zoom-controls { + display: flex; + align-items: center; + gap: 0.5rem; + margin-left: auto; + } + + .zoom-level { + min-width: 3rem; + text-align: center; + font-weight: 500; + color: #333; + } +` From cf219f77b008308b7f318753dc8f131df908ec83 Mon Sep 17 00:00:00 2001 From: Josh McLeod Date: Wed, 4 Feb 2026 12:00:00 -0500 Subject: [PATCH 06/31] WIP --- package.json | 1 + .../pdf-viewer/canvas/pdf-canvas.js | 94 ++++-------------- src/components/pdf-viewer/pdf-context.js | 3 + src/components/pdf-viewer/pdf-viewer.js | 98 +++++++------------ .../pdf-viewer/sidebar/pdf-sidebar.js | 73 ++++---------- .../pdf-viewer/thumbnail/pdf-thumbnail.js | 16 +-- .../pdf-viewer/toolbar/pdf-toolbar.js | 86 +++++----------- yarn.lock | 12 ++- 8 files changed, 122 insertions(+), 261 deletions(-) create mode 100644 src/components/pdf-viewer/pdf-context.js diff --git a/package.json b/package.json index d91599d..8acf40c 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "build": "tsc && rsync -a src/assets dist/" }, "dependencies": { + "@lit/context": "^1.1.6", "lit": "^3.3.2", "pdfjs-dist": "^5.4.624" }, diff --git a/src/components/pdf-viewer/canvas/pdf-canvas.js b/src/components/pdf-viewer/canvas/pdf-canvas.js index 39cb8a1..bb3b183 100644 --- a/src/components/pdf-viewer/canvas/pdf-canvas.js +++ b/src/components/pdf-viewer/canvas/pdf-canvas.js @@ -1,100 +1,49 @@ import { LitElement, html } from 'lit' +import { ContextConsumer } from '@lit/context' import styles from './pdf-canvas.styles.js' - -const PDFContext = Symbol('pdf-context') +import { pdfContext } from '../pdf-context.js' export default class PDFCanvas extends LitElement { - static get properties() { - return { - _currentPage: { type: Number, state: true }, - _scale: { type: Number, state: true } - } - } - static get styles() { return styles } constructor() { super() - this._currentPage = 1 - this._scale = 1.5 - this._context = null - } - - connectedCallback() { - super.connectedCallback() - this._findContext() - this.addEventListener('pdf-loaded', this._handlePDFLoaded) - this.addEventListener('page-change', this._handlePageChange) - this.addEventListener('scale-change', this._handleScaleChange) - } - - disconnectedCallback() { - super.disconnectedCallback() - this.removeEventListener('pdf-loaded', this._handlePDFLoaded) - this.removeEventListener('page-change', this._handlePageChange) - this.removeEventListener('scale-change', this._handleScaleChange) - } - - _findContext() { - let node = this.parentElement - while (node) { - if (node.tagName === 'PDF-VIEWER' && node[PDFContext]) { - this._context = node[PDFContext] - this._updateFromContext() - break - } - node = node.parentElement - } - } - - _updateFromContext() { - if (!this._context) return - this._currentPage = this._context.getCurrentPage() - this._scale = this._context.getScale() - } - - _handlePDFLoaded = async () => { - this._updateFromContext() - await this.renderPage(this._currentPage) - } - - _handlePageChange = async () => { - this._updateFromContext() - await this.renderPage(this._currentPage) + this._contextConsumer = new ContextConsumer(this, { + context: pdfContext, + callback: (value) => { + this.context = value + this.requestUpdate() + }, + subscribe: true + }) + this.context = null } - _handleScaleChange = async () => { - this._updateFromContext() - await this.renderPage(this._currentPage) + async updated(changedProperties) { + await this.renderPage() } - async firstUpdated() { - if (this._context && this._context.getPdfDoc()) { - await this.renderPage(this._currentPage) - } - } + async renderPage() { + if (!this.context?.pdfDoc) return - async renderPage(pageNumber) { - if (!this._context) return - const pdfDoc = this._context.getPdfDoc() - if (!pdfDoc) return + const { pdfDoc, currentPage, scale } = this.context try { - const page = await pdfDoc.getPage(pageNumber) + const page = await pdfDoc.getPage(currentPage) const canvas = this.shadowRoot.querySelector('#pdf-canvas') if (!canvas) return - const context = canvas.getContext('2d') - const viewport = page.getViewport({ scale: this._scale }) + const canvasContext = canvas.getContext('2d') + const viewport = page.getViewport({ scale }) canvas.height = viewport.height canvas.width = viewport.width const renderContext = { - canvasContext: context, - viewport: viewport + canvasContext, + viewport } await page.render(renderContext).promise @@ -114,4 +63,3 @@ export default class PDFCanvas extends LitElement { customElements.define('pdf-canvas', PDFCanvas) -export { PDFContext } diff --git a/src/components/pdf-viewer/pdf-context.js b/src/components/pdf-viewer/pdf-context.js new file mode 100644 index 0000000..e589bd8 --- /dev/null +++ b/src/components/pdf-viewer/pdf-context.js @@ -0,0 +1,3 @@ +import { createContext } from '@lit/context' + +export const pdfContext = createContext(Symbol('pdf-context')) diff --git a/src/components/pdf-viewer/pdf-viewer.js b/src/components/pdf-viewer/pdf-viewer.js index f792113..5dab83b 100644 --- a/src/components/pdf-viewer/pdf-viewer.js +++ b/src/components/pdf-viewer/pdf-viewer.js @@ -1,19 +1,24 @@ import { LitElement, html } from 'lit' +import { ContextProvider } from '@lit/context' import * as pdfjsLib from 'pdfjs-dist' import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.mjs?url' import styles from './pdf-viewer.styles.js' +import { pdfContext } from './pdf-context.js' import './toolbar/pdf-toolbar.js' import './sidebar/pdf-sidebar.js' import './canvas/pdf-canvas.js' pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker -const PDFContext = Symbol('pdf-context') export default class PDFViewer extends LitElement { static get properties() { return { - src: { type: String } + src: { type: String }, + pdfDoc: { type: Object, state: true }, + currentPage: { type: Number, state: true }, + totalPages: { type: Number, state: true }, + scale: { type: Number, state: true } } } @@ -29,86 +34,68 @@ export default class PDFViewer extends LitElement { this.totalPages = 0 this.scale = 1.5 - this[PDFContext] = { - getPdfDoc: () => this.pdfDoc, - getCurrentPage: () => this.currentPage, - getTotalPages: () => this.totalPages, - getScale: () => this.scale, + this._provider = new ContextProvider(this, { + context: pdfContext, + initialValue: this._createContextValue() + }) + } + + _createContextValue() { + return { + pdfDoc: this.pdfDoc, + currentPage: this.currentPage, + totalPages: this.totalPages, + scale: this.scale, setCurrentPage: (page) => { + console.log('setCurrentPage', page) this.currentPage = page - this.requestUpdate() - this.dispatchEvent(new CustomEvent('page-change', { - detail: { pageNumber: page }, - bubbles: true, - composed: true - })) }, setScale: (scale) => { this.scale = scale - this.requestUpdate() - this.dispatchEvent(new CustomEvent('scale-change', { - detail: { scale }, - bubbles: true, - composed: true - })) }, nextPage: () => { if (this.currentPage < this.totalPages) { this.currentPage++ - this.requestUpdate() - this.dispatchEvent(new CustomEvent('page-change', { - detail: { pageNumber: this.currentPage }, - bubbles: true, - composed: true - })) } }, previousPage: () => { if (this.currentPage > 1) { this.currentPage-- - this.requestUpdate() - this.dispatchEvent(new CustomEvent('page-change', { - detail: { pageNumber: this.currentPage }, - bubbles: true, - composed: true - })) } }, zoomIn: () => { this.scale += 0.25 - this.requestUpdate() - this.dispatchEvent(new CustomEvent('scale-change', { - detail: { scale: this.scale }, - bubbles: true, - composed: true - })) }, zoomOut: () => { if (this.scale > 0.5) { this.scale -= 0.25 - this.requestUpdate() - this.dispatchEvent(new CustomEvent('scale-change', { - detail: { scale: this.scale }, - bubbles: true, - composed: true - })) } } } } - async firstUpdated() { - if (this.src) { - await this.loadPDF() + updated(changedProperties) { + if ( + changedProperties.has('pdfDoc') || + changedProperties.has('currentPage') || + changedProperties.has('totalPages') || + changedProperties.has('scale') + ) { + console.log('setting value', this._createContextValue()) + this._provider.setValue(this._createContextValue()) } - } - updated(changedProperties) { if (changedProperties.has('src') && this.src) { this.loadPDF() } } + async firstUpdated() { + if (this.src) { + await this.loadPDF() + } + } + async loadPDF() { if (!this.src) return @@ -117,22 +104,8 @@ export default class PDFViewer extends LitElement { this.pdfDoc = await loadingTask.promise this.totalPages = this.pdfDoc.numPages this.currentPage = 1 - this.requestUpdate() - this.dispatchEvent(new CustomEvent('pdf-loaded', { - detail: { - totalPages: this.totalPages, - pdfDoc: this.pdfDoc - }, - bubbles: true, - composed: true - })) } catch (error) { console.error('Error loading PDF:', error) - this.dispatchEvent(new CustomEvent('pdf-error', { - detail: { error }, - bubbles: true, - composed: true - })) } } @@ -151,4 +124,3 @@ export default class PDFViewer extends LitElement { customElements.define('pdf-viewer', PDFViewer) -export { PDFContext } diff --git a/src/components/pdf-viewer/sidebar/pdf-sidebar.js b/src/components/pdf-viewer/sidebar/pdf-sidebar.js index 501bdd9..284df70 100644 --- a/src/components/pdf-viewer/sidebar/pdf-sidebar.js +++ b/src/components/pdf-viewer/sidebar/pdf-sidebar.js @@ -1,80 +1,42 @@ import { LitElement, html } from 'lit' +import { ContextConsumer } from '@lit/context' import styles from './pdf-sidebar.styles.js' +import { pdfContext } from '../pdf-context.js' import '../thumbnail/pdf-thumbnail.js' -const PDFContext = Symbol('pdf-context') export default class PDFSidebar extends LitElement { - static get properties() { - return { - _currentPage: { type: Number, state: true }, - _totalPages: { type: Number, state: true } - } - } - static get styles() { return styles } constructor() { super() - this._currentPage = 1 - this._totalPages = 0 - this._context = null - } - - connectedCallback() { - super.connectedCallback() - this._findContext() - this.addEventListener('pdf-loaded', this._handleUpdate) - this.addEventListener('page-change', this._handleUpdate) - } - - disconnectedCallback() { - super.disconnectedCallback() - this.removeEventListener('pdf-loaded', this._handleUpdate) - this.removeEventListener('page-change', this._handleUpdate) - } - - _findContext() { - let node = this.parentElement - while (node) { - if (node.tagName === 'PDF-VIEWER' && node[PDFContext]) { - this._context = node[PDFContext] - this._updateFromContext() - break - } - node = node.parentElement - } - } - - _updateFromContext() { - if (!this._context) return - this._currentPage = this._context.getCurrentPage() - this._totalPages = this._context.getTotalPages() + this._contextConsumer = new ContextConsumer(this, { + context: pdfContext, + callback: (value) => { + this.context = value + this.requestUpdate() + }, + subscribe: true + }) + this.context = null } - _handleUpdate = () => { - this._updateFromContext() - } + renderThumbnails() { + if (!this.context?.pdfDoc) return html`` - handleThumbnailClick(e) { - this._context?.setCurrentPage(e.detail.pageNumber) - } + const { pdfDoc, totalPages, currentPage } = this.context - renderThumbnails() { - if (!this._context) return html`` - const pdfDoc = this._context.getPdfDoc() - if (!pdfDoc || this._totalPages === 0) return html`` + if (!pdfDoc || totalPages === 0) return html`` const thumbnails = [] - for (let i = 1; i <= this._totalPages; i++) { + for (let i = 1; i <= totalPages; i++) { thumbnails.push(html` `) } @@ -94,4 +56,3 @@ export default class PDFSidebar extends LitElement { customElements.define('pdf-sidebar', PDFSidebar) -export { PDFContext } diff --git a/src/components/pdf-viewer/thumbnail/pdf-thumbnail.js b/src/components/pdf-viewer/thumbnail/pdf-thumbnail.js index 55fcce8..c1e9ff2 100644 --- a/src/components/pdf-viewer/thumbnail/pdf-thumbnail.js +++ b/src/components/pdf-viewer/thumbnail/pdf-thumbnail.js @@ -1,5 +1,7 @@ import { LitElement, html } from 'lit' +import { consume } from '@lit/context' import styles from './pdf-thumbnail.styles.js' +import { pdfContext } from '../pdf-context.js' export default class PDFThumbnail extends LitElement { static get properties() { @@ -7,7 +9,8 @@ export default class PDFThumbnail extends LitElement { pageNumber: { type: Number }, pdfDoc: { type: Object }, scale: { type: Number }, - isActive: { type: Boolean } + isActive: { type: Boolean }, + _context: { type: Object, state: true } } } @@ -21,6 +24,10 @@ export default class PDFThumbnail extends LitElement { this.pdfDoc = null this.scale = 0.3 this.isActive = false + this._context = null + consume(this, pdfContext, (value) => { + this._context = value + }) } async firstUpdated() { @@ -59,11 +66,8 @@ export default class PDFThumbnail extends LitElement { } handleClick() { - this.dispatchEvent(new CustomEvent('thumbnail-click', { - detail: { pageNumber: this.pageNumber }, - bubbles: true, - composed: true - })) + console.log('Thumbnail clicked:', this.pageNumber) + this._context?.setCurrentPage(this.pageNumber) } render() { diff --git a/src/components/pdf-viewer/toolbar/pdf-toolbar.js b/src/components/pdf-viewer/toolbar/pdf-toolbar.js index 817eca2..bd82fd8 100644 --- a/src/components/pdf-viewer/toolbar/pdf-toolbar.js +++ b/src/components/pdf-viewer/toolbar/pdf-toolbar.js @@ -1,100 +1,63 @@ import { LitElement, html } from 'lit' +import { ContextConsumer } from '@lit/context' import styles from './pdf-toolbar.styles.js' - -const PDFContext = Symbol('pdf-context') +import { pdfContext } from '../pdf-context.js' export default class PDFToolbar extends LitElement { - static get properties() { - return { - _currentPage: { type: Number, state: true }, - _totalPages: { type: Number, state: true }, - _scale: { type: Number, state: true } - } - } - static get styles() { return styles } constructor() { super() - this._currentPage = 1 - this._totalPages = 0 - this._scale = 1.5 - this._context = null - } - - connectedCallback() { - super.connectedCallback() - this._findContext() - this.addEventListener('pdf-loaded', this._handleUpdate) - this.addEventListener('page-change', this._handleUpdate) - this.addEventListener('scale-change', this._handleUpdate) - } - - disconnectedCallback() { - super.disconnectedCallback() - this.removeEventListener('pdf-loaded', this._handleUpdate) - this.removeEventListener('page-change', this._handleUpdate) - this.removeEventListener('scale-change', this._handleUpdate) - } - - _findContext() { - let node = this.parentElement - while (node) { - if (node.tagName === 'PDF-VIEWER' && node[PDFContext]) { - this._context = node[PDFContext] - this._updateFromContext() - break - } - node = node.parentElement - } - } - - _updateFromContext() { - if (!this._context) return - this._currentPage = this._context.getCurrentPage() - this._totalPages = this._context.getTotalPages() - this._scale = this._context.getScale() - } - - _handleUpdate = () => { - this._updateFromContext() + this._contextConsumer = new ContextConsumer(this, { + context: pdfContext, + callback: (value) => { + this.context = value + this.requestUpdate() + }, + subscribe: true + }) + this.context = null } previousPage() { - this._context?.previousPage() + this.context?.previousPage() } nextPage() { - this._context?.nextPage() + this.context?.nextPage() } zoomIn() { - this._context?.zoomIn() + this.context?.zoomIn() } zoomOut() { - this._context?.zoomOut() + this.context?.zoomOut() } render() { + if (!this.context) return html`` + + const { currentPage, totalPages, scale } = this.context + return html`
- - Page ${this._currentPage} of ${this._totalPages} + Page ${currentPage} of ${totalPages} -
- - ${Math.round(this._scale * 100)}% + ${Math.round(scale * 100)}% @@ -106,4 +69,3 @@ export default class PDFToolbar extends LitElement { customElements.define('pdf-toolbar', PDFToolbar) -export { PDFContext } diff --git a/yarn.lock b/yarn.lock index 93c8c9a..a26277a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -219,7 +219,16 @@ __metadata: languageName: node linkType: hard -"@lit/reactive-element@npm:^2.1.0": +"@lit/context@npm:^1.1.6": + version: 1.1.6 + resolution: "@lit/context@npm:1.1.6" + dependencies: + "@lit/reactive-element": "npm:^1.6.2 || ^2.1.0" + checksum: 10c0/203f761eda19c8b37d77f01d9a0148535c5b28c47b76e28b321cb6e5ed0546c45332512e68b1647ce92ca67690d8c66de31972ec115410b080d69fa25a5d86f3 + languageName: node + linkType: hard + +"@lit/reactive-element@npm:^1.6.2 || ^2.1.0, @lit/reactive-element@npm:^2.1.0": version: 2.1.2 resolution: "@lit/reactive-element@npm:2.1.2" dependencies: @@ -1273,6 +1282,7 @@ __metadata: version: 0.0.0-use.local resolution: "spider@workspace:." dependencies: + "@lit/context": "npm:^1.1.6" lit: "npm:^3.3.2" pdfjs-dist: "npm:^5.4.624" vite: "npm:^7.2.4" From 1330f8a82f84d71f4db99cf067502bcf59b59a22 Mon Sep 17 00:00:00 2001 From: Josh McLeod Date: Wed, 4 Feb 2026 12:13:59 -0500 Subject: [PATCH 07/31] Make scrollable --- data/the-veldt.pdf | Bin 0 -> 50334 bytes index.html | 2 +- .../pdf-viewer/canvas/pdf-canvas.js | 146 ++++++++++++++---- .../pdf-viewer/canvas/pdf-canvas.styles.js | 14 +- .../pdf-viewer/pdf-viewer-component.js | 22 +++ .../pdf-viewer/sidebar/pdf-sidebar.js | 26 +--- .../pdf-viewer/thumbnail/pdf-thumbnail.js | 36 ++--- .../pdf-viewer/toolbar/pdf-toolbar.js | 20 +-- 8 files changed, 171 insertions(+), 95 deletions(-) create mode 100644 data/the-veldt.pdf create mode 100644 src/components/pdf-viewer/pdf-viewer-component.js diff --git a/data/the-veldt.pdf b/data/the-veldt.pdf new file mode 100644 index 0000000000000000000000000000000000000000..70c88aba7bc1034eb92f20eba7cfe9fc4aae9193 GIT binary patch literal 50334 zcmcG$RdkzO(za=4j+tURW@ct)W_E0unK_1-nVFf{j+vR6nVDm@CvU%<>CAk+*8i>P z9vw(aTY7dqC#vhNyK0L>URacdftCq|)9S)?MT4z>tjV2IvIeGnSr22 zCwoJH0|Dzl7I=AK0MFs|tZ8pDyhtVr!IRCl1;)jvYOu)Q#ifbFHWw4#Ny zoIyFHee``izmb=QQY*$=Qec{{6cX1is8x;74?SK&27g2dz|S^~@?oe~c(v~k!HsZi zk8g?6)nip|(&~Au`p5R(-`)(Su}pB4?Y-#lUm35z00H%;)mhscGhh0@I{bKde>!%5 zI!|y|!$h=K1ajqshRe*y?Mup6Fnrw$SGuf2waLSq$1L^lczt|)s5-uWq0TBy2oQ>SA{U-?s3z)0jzBVS?Nx_oP!yA(CdF49Y=BasM@ zRFNMJiHeFx`sa}ucRN#ib9U!l#0x2T5r{4G}pfj0zNV!7lwAaRN>A%7=UEe`>(SCTx`mTU01eR2VUsTEzGF zj<&l|;a3<@gV%0RWeRA-|14@>H8uQEj=BNarrgYST9*Bx8!8(*AfYNBlege>XOSiM z5XFg9k68gS>pQPHh3gi`VWq0gWEcZ_Il5vQZ;7e;*|qe*l*EvVc|jcxHV`XxF$J3G zBoa&5ch3%S_9+qzXjj0{tSaDUWEc<58rBFgYPi~@>=ni#N7ydXNGq2-YuXq;sdsn< zi+U#Z%*0kvi^S7$_2>mcNrq5daC55lp=C&(vjP)}qKgQHHMEO^BtBGILu(e;qrd%U zaYZ8PEQ>c1m&Byymbhv#jxj8sHA!2XKyep=Zhm}qKNKn+@noA9XV}c4uL)~%vhA!s zr9($pa(Zr{gU*bv5$Q6g2K{A`a$TUiK;&bdY+N4Du~=u21>Bxi^f)t02GI8}s7~L7 ziskp|8Er>DT(`|8CMP!OO><>1$I-HksQpGaIo(x+p$3a&qpdjtuiPo!3m-1pnGiUo z1D4z(NUf*rmNGcDuZ0;7q@VKrj7^YeQwN|5ZIH5i>SIDwL}PMf;s}+`=z|$ACQ=R; zI1gMf+qKRIneP0DjRD{hD%omB4s&LPXE%mX>t?~3^K^nKFeePQ$GUXv4td8Gc&(Ye}QkDZ%BNCu&i7?+ygv z%mWVtcve`cMmk|yhjx55x z?F5+ty%9D(w%ff*rM;U2K!O2omz0LN3b}AGE-Gs+@8CfSJuAZuKy)#5; z3Tdq=)@191>C6vlkTBag8&U>pwgK>0Qb@+ARU;r|5~d7Qu6Nx-F6$fSj|7>J)i!|d zGEq7|D|?$@j1<$!41P$an$4s9S)S|wQNkQ&wQ&n~3j`3(e{Fmal1j>*xr=va%2k68 zm%#xhN19Il6vNQtY13~fb>!IR`ORt8)|WO=63ep{%^ ztr_e|5@-zDn-$U~K*f;KC?~UP(OGfNNR(mp%^v|y^O3yx6o~~p1G!HvKLB3!7Ab$a z0jIBV>2N-OpJt#Gur*6BXd3EoCWOBCDFrk3QXFz4%b(ga2QSd^04U`_{xyZ@^Rm>j zUt*fRppcbEPf;GmLKNklci_$&g)ia&>)ZQs85qYw2}q;Ahd(2ZMwGUTX0`4|Mj63j zq-7%TxUf^5ibWa0co4ZR)E$MjDW|4VZ-p!_G1{)xHab{sT5M{Kf2F z5dRmP8UDulztH)Qh2QM_|BB~-3*~N{d1U(PHYG%iq~qD<~;en~q~bKeYBp7_zs$8=X@Z`O%T{xXGS!`?`3% z`uXDHjbroM%hRaa`$Ik&M^#bwxzP}!LG|IH^&8iR*Ylxp+Igqvl?>;Ls}dxOjDP0Z z(--`=IG#9;Z`4JbowJ&0k2Tl3JA;FhU5ne-!8=~Lk1-Dyui^NMr(QQv9>cH7HbjIvSyF-!|ANsk+kZyFzV?~nU;yMxDz7;wzt z0&Yx?rqdO^xw!`;BhQX0i0Fnn(e5QxaXC}$1g_7m)n5%?argybA?%`@po3#BtL*Q8 ze!n!+(Z8ys?u4Wlp&iWX3ap2VrZ`L~nYP-wSMJj2JUC>^G6ZjK2R@?ipxtfBG(PQfR=-Q9(o_L7-FvF z(n+B@7ZOnw;<67zyda^P^pG4IVp_m3Bpvl!qH9oCNSk(Mw4;iS$u1xTy(B@WcT|jr zrya-bN1d&jF<$PA9@F+k401BbRpqvOyl>7l>Z%67B<&r&iahxRityh z2fk1Nk@0eQcp3oQa*p{@N`;?5rEXeD*p6b1nKd!DcC zr(@dHJOWV?fwtP;HwevWP4`7H(rPkPg+c>?QI`v5bF5};)*PE6ek3>z$I{hKg|=^O zDlAzvWU@Q&G@Td_4|`z;J-hO4nJR9$Uk+q=1F?HW4cdH#39e9gj4Rl0+2@j%?ayE~ zUIw2qov4(216SC7%S84Hp^HY_ZE$~pVeICh5!l>I!7v*)psUSO>5e^SVZ+0#gIB|< zau?#WXYz};q&wp~-kq`C(xtRn0ZqM|#0H>XMT_Z~w{6`K(kFs3lL&w}$>M3?%_IA^0&Ys=ToMo)k*CTAju z0W%83V3BU_=29WH)n(5dG|sDr!y(vnSe)Z9?(6t;#w)mND z@&c*i6JIJYH7uuIZt)v+C*1X>q1DE;ybnFhCa?*npmMd=f_Cd0fWJoL5q6^gt9nWWZ$_R;reB7xYgjq6;qIQ_CTto z9>ohg6H99(O2y&xZzMW^+8F$5d2$p9fYBloIpSWWiSkv9dY7>f6!|3P3Q|=bUlGl} zQnmjZ4*pr#{U;n``X3x*{>RE69QvZVMWs4OR7M~f_-%hWd9^!jxy*IylOt+qQb|eHn zpUeoM1)1HLV^`T6A7!1Zq@Awr-V9yNS2Uz;s5$oGiC6SzP)Ag*1TpclxRJ9 zf2&k-c=qt3Yp?3PZE~V|bLAbi7{NbzU+Yc2(V{j7f+ zpWm#K;U182fmKz6`1Q&!HA7*A^%UZsu^mSDxWQuEIEH z78*J_5;v}HBg^mr#S6_btHLdgs-;cT$7Wk52MR~O%f4iS?ZE-VIWHUKct<-7U}xXe zk(|u9y&2d&sJG3qF z<%g~|x*&8NM-ZGIlO$AlTVtD;$vi>R*3vbam>XBJL=Xj3q&uCUJo>c2t#P&`JIV-J zH(@GHo_wP_eTM-8)Y)4m9IP<4F?5)>ND4xRBBbDgFUwTwtx_9lQqyB!g(Xpc93jxs>t&o6DxV( zfpdo!xzQ&9;p-D+KZsNc)c}3bGN%<*uprkfCp)v&O2*X_U{uWDfj8wq1S+_Smjiy6?wU|ignK3dqN2eMG+m6IAK1+UnS zUn#v$lsSmX^r=HrVXQ&qS+(UEd&*g3qa1@MkDw+GbLnpuyr(pLPzs|cs z6d&BgTo8YDbaQ*lSUoDw^TYot9WGAvp;(|^7eo%FmK_JqvO5Hf3Ah`($XJhe8A0DE z>LP5O9eW+q)gf2hFDflBumg4uYd06glFUdBa3Qpa@z%&y<5@D2FYnbT!s0tZA|sJU zXrN>hFb74=Wl}>a|H5vn0KZtO;Hf`1>lYP=*@+4=rZ4Q+ zjie1HJY$C(Em!v_VXA@}xH}O88geq&OD+u=35VJHnjewuWy)RT&wo;v!#DQF`#Pi5)qhV7t!f(D4LJglX48&UFrj1S zGoITf%c-wF#r4qUorh;>-d5iQS?4F1toS2DNQqaETIRr_=XkDLPE^dGav@$R)Jy+> zXzP<}LNxK%(4#hriTVUYp1;_!aOH{Q#i_F!E#6` z?C=CwPEQp;?M`HPriJvb7@}Xc0tt;H4KY)lyr!5w9Z^8Sm;gU~0-(H40W;V6BNVu7 z&W^2{YWo@8L7fhjcn%CcX#BLdWmN!mw6v0MO~tCcvph&K4cue7%wNVbxi7ES1??^z z#0c;|^Ow}7stO8~d28a2oYsfudFgs;D2)Y{&`tI2##`Ec<=6D^5pB>?+Hwu6&2&nY zYxj~q50b~MdAuSlfH*4C8GiLlbTf7ZZjHDXnwoBrH5krRm~sTgHIl=~edcacrz~## zgv^Q1#z!b}6&~onj-{**L-cvJv>$-TNc8kEmV#Q--{twLyA4_kcBx^nP}qulz4BO| z7mhzLFDJMt%pM;=DRXnZ{ldZ9PF6^Sq*Wr{j_T$<)7#>oG<0I#p0=X5;&@Z%EepFn zzcnx|(Ik=5;xoU%lqO|0kdsO!0GU{Tlvlj7rg|AihX(D*h)`dANF59fBtx5;q(63KXrO{j`0gm$V+lpHiG0k zIvot`82`|IZ=W)Msg8ReDwsE1fPT; zw4M2isP=Stf4(>>XOpzaTRpthR&8D_Q2I_v>It_Evsr+cMd?Xv=;0GAd+lk92=>M?Hu{dS_5wS}NqjTB4^6g_x;8Enx5R4(F9hiD`bI^79(E}8 zo%E*LdUG2FUBbmNb@c$OS;pKPWFkKFcM}v@GGG~C0zY$e{zM@<{*pT{1z|9e1R)DB z-|mO1C(kW#^@1?CS6V6uH+)}PGf!&CNPCbYN4n5qk1K7zI%x^QVn^2EoU@E9CVbxE zkxUyh@aI6$Tz;#tFm`l#qZA}2W zjDrep4{G~?8pv`3l&xV*)RFI!DP48$nYtPK#koV!ry1wPW@0o&>1>u?g4fu?y$*HY z_)Y{s{D}=KaG@jAMmw%+Ovq9WleLiQJR$Vf!Rrqq_*s^GTCiOvL zwR(vZT?ERZa8-7W6MQ#&dDZA^;cZtYh!8{E)-HK%r+%hJJ2|}rW2H1GWBjK z-j*WcE_{}%4ikDTep{0e(LPi=y)3e-l)a^-?PB{j)Pa=fBbWGD#PaJR;B;@kAJF$o0UGnE~t-XFfw4{?Crkw^9kqkTM;75#vj zJhqG->egE(D@K1d8K8r6utjXE?^+|F_$e+Yp=S$QrL`%a+@O_4SiJ}}ikdMKKB4tl zTv$+!+MaaH1YcF&dIX?~(L;gMa9caL#EpGP8KZi|M-}Iy@$BjxENBl+8J|W6R|X=* zAJgW@0+Ym{aTJd=`(m~ZRyu7DfYvr(1grx=lcL3=SXkEs`&Z2yJ`8 zyg}9=EfFC%`pGI_33U_?5wF%$T#$Ct0l}C{U$}gDfipRel|vmeeX(>(PPH;&H^(}} zkNu&W5nJ*O{E#fzSx34+w|?allB97NqhxO#J~epbt2;J$g$z4F+BQ-rU8yWS+TNJ< z*4{2?OX;%*Dqt5BI(8lNiGz9pkysvPk=~w5X`yzLd@1a{_$ofcq?zlQ%Ozq31hei= ziy{8`EiW|IUw*)2p2E|kB1nXtnhoI-UUPJ15Sd}n(cMy?{Q`FWY+pN@CA6*A?A?;R zI3v;_nYbo<`ynfOYn)e#mvj)DQEbU)^=8a^;i0>I0g$M}9rQ)j-juq@lA$CTWDDv> z`t{=+S%CMt8+&P+0E|WWIjc7Vg*)-6L3J7@EVwQnW?EyRx;9#^3(%P#B8*9YuHC-L z)ZvKr24y5Xytao*mkT*HSt!3j*^=LRitK5=^)jwXW)0you1*!!+UcA8PKv1nuH{c} zd;SY4?lr1JD`O=iaP`IU-A=Q51A|?F&zJ@pv$8U|z!<4p{0CXPB%AY|$1YwN&eojx9OR#yc z>LjAKCU{d(R^9#Nt4BM#{ht(L{L6#=Z&2*-PUs&LWBl9i{TIdlvG6<8f0tr^1^>PC zZ;G+g|1T)k@QY%bU#BedqZx|1)cIuaK*7yUKp-K(SnkKLly;d|GQ7Tr_qWrtVGi_KQoPG{N53~stp?c_z7EWP1MJHE>c zkFD0>O4knut)DElyi@H-mM`0;AKbrc)yL!WtJP24<}Ld6yM8u{-L-F)Q%3Z-t{y$e zDDe~o7!2H_E@}+!uGZXCTnk~JsKHg>ZDV;>T6@E8+m@X2Oa@=<6OX0a*^|!uy|1l{ zR?mk>F*lpn0R1ukzPyt4qPs2I3udp#96lL%TeM%-tgwJsEN0T@!<;TTAR`N$45J5L z;v(MKHmE#yUSh$>pc!L@FJumn_BH*uE3Uu7a(xI&FjtOxfFiY-wLi$@K@zY(ArA|N zF$$837lO(84jW-gB5NonSPZY7YM(q>Z)+<9b69KxDNo9r`PFe~ty=~ajHIH#dx*Fv z>gRVwgALrrt0#g4k94z8CRSb!*RaXcrp+DXSNq38_UjlOi~!W;W@VKin8I<)8;=(T zs6%fA);!5TVSb}}jtt;&tnq zks@8CEzT~s9nLitNG`r#eFJK61tN1Dhzgw}BQwqSM6)h#uFSyG@=Ho+PkY*&wINV< zjI@_w^y-?d`MKWLgG*fvQp1gF}*Z39KiC>D7&hbMS!@g`9Z06D6D! z>vIb$Wdy*WC6f{iK?sTY3D%WEOHs)K9(H<<`>Gp40SHnJbGR>C1Ygm=Zl5&fRaF?p zin&*}StZzLCMg90NM>X}8DftA{0%n(q>^p2r}eeX!KrhqHIZSGrBrU-^~ z?Kf!-T4Yc=6K*@(Qr(39Sp~!vlo1^05IkNnNlzccb6`zfj;_?KCnz-t$sby1@fa25 z8|kfize;V?Vl~*lkgAZxMPGw&rU#KgcC`BNwe7SRk)AU%nE_Ab@i{kBFQ=9YdFk*f zK+tTrKzX%+&9sAMf=y?Jvtu;<9vmH10H}7Eg4GpwIDp`b!fDq-eGo)Itz1cMDrANZsf*RG>T?rE zd=oI6kK0~oI&EYwByk66!l@3<)hhAfo2(F&s+B93Mk_cprxYkQ{{la;4c^_hq~Lm= zP&nVT0T*Kt%Lx4utkf93RuSlBZw%zDgJ7Z+Ob$dz`L#9}fw(gd<#@mO;5rp$Tua4! z4P?#+sm$pG@ggI{N_?eEAtK>4XP%Vl%dejX-^`8Mz}O9GI0<%1F!l!wAw})kHk_v+ zZcVBo;S>%JXs3`Hn`_j_c|y1o2RxqlVMCJ#EVs`WHqxYBuc&3Id-Smz3xy$3{be!I z60KPclDsU3%-2M%3%w18JjrA3G+y8KF)}KwHa7JL7K#C2ebgKGt-T7hc#_p*r0fO; zFr!1FUMk2R^q+sUoh#^OSc4niOWs368v1=r$uB*ytC&AOfuH%hMprs=UAChZJkoFE zcV7E*#CgQkyf|~B*l5%=m%H~1Zmj#-5cj?`J8~*cJ2;ygKS+2q=FN#6`tSy$d+r8= z3Xs@k5DkV5P7v|@V__X^8{ixcMI{Zp5=dEgp5Dqi+ZM%E3xuAx=sgo81(9lR+VhJ0 zvgqWkl#8yeqCx>`YDaNd@zKSkx@CfyRpeZ!{`o@X6U9girP({=q-}lRzflH%n$Z72 zearY)|M>sW_htOs0sli8{OL;nSCzp(g8#pj0S61~|Jm;@_^01J`OELte+|cjuKBS; zSOYqaP;(^fB1O$FsJ8PWG2?V*ICy*v=eMS+>&!D*QX0;2{M2bP_9};aZuWe7zWUkv zypR-b4!*j?a6qjC@?&6XR~YGY@2M zgU4@4O`m^V;w8{UNQ!obWE{Z?qA3G{wLHensx2!+-cuM+UUwDm@ob)mK@;U)Tu_?I8ffK1`%wDchNt!T?(+S+ z%!Pm$yVy^Sdxm{13-Jjz8w@{KXCjl0B>}ro9~Ms9+#wa*tv-UqoAP65 z=zh|6mb(V>mX6aA>@qm-LEvcUKr#I_^5Qn!957KdSKA%pQ!b4pz}1fY1Qd7H9(L8Y z0=V86lc)9seXU7Et_iXNp}7f5yrhI2)0waQvQP6<*GB`SrO{tqGlwkf6|Slz4mo)N ze5~F(`G;F<;-Le46>sYrJAR(u*&70X`i>Dxp+Tyx#65su>PWWz1o|H58o?J?M9lBw zVHJ)W32T)A%i4mE2J|F|Y$`F*%Yc-tK*3SwVY}fP-aa;E{9UV*oS(Fzn^LvsKAhM? zeoPrSD&0wdOpumFxmr|oSLR6HhBFVJ2&>v_&2@v^W)0~alH%?w6eF{yT%ccdU%=^e zpGet*0d-eFn?=9_qq5$RFJ3R`0u%1WNwpZIvi5-44ROP*cM{Pgm2J!{uB{OQ=V+9G z2u}%OR4YJSITob{+yU#KbqSA$I&a%}=(-$?&kTX!EWoMJyu@TrBKY;KasQ z6|U-$eV*G5rAJVrsasindbNxm6~y&OPD;}T@y|*@F4BbcGQ_;yUJ-K;F8m>|0N8~S zBMG@ehh|@>7*G|&IJpiU%N(b27)G@c zhcYSjUdwzuN5;?q8`Bx(Qen%f`~*I@1uoB-tA}Tg>F01E#zsD<^}&nc52==)TDYa13g2M$>Ls< z{OV5Gv+-o6UTodyG#sH!A!^>KX2?oX2&X;QQVH}f5LTQWU)9k(Q>K+A<}IepBadN@ zp|}a;eAyW;AtUmcf*5U}yC`E?p)gstwI{gmw%oHS)r`e8KaNXGN*g5D|R?p)0(vrP#|td+Q1kA zDZIPos~1_q!7l8z_D~*`ILnd7(#8=eUej`yVpda+cnzr3AexMkQxd;du8oTI8kGrCl8GSJGA27A0D)G+7yCSCR%{*ZVVeEKaH zjVR0Gcexk0&KO0soOL*>)6Nq1BTOdjhH_p<`s0%5u3`l73z-0iIx{0;KYWqXQe`IQ z0Rfe(Z@es9I`8q-X}LmT-uQiu-Qw-3Ih{?#0M3(mOy%@T#}8w{T%b<}mQcuZg^Sn3 zCOvWY2t_T|0vtR8IkN(EmCj!Zbhu!GThcNSC^%32l!zVgMJhwk$?MCnVdy`rEx9JV zj)`Yf4oHSxfz=hX%luGT*o1Dyv-w)v{Hhs_^uu;N&DuU@MOu|ARIJz}ozt{@FCnZh z%_Ac4X~=C}_X9Z0`i%VFNR~f+^8ZA#{EsBgkPg-?T5e23>6V`7Wq5+BapiVqNd!SB?%vV&1}r}SDR zMt;~A1Wj?QKW!9mb?cv1O5Dj6f}T}Qj$Uq>MR2|`9DkZUZ1jZ8;};RNGks@Sdn(A^`LdYNq&f6;aeWbZ7z7q^(bHJLWs0R1{1gHNnQ zwv1$^Gn6rrW|?vy&=A>zakweY)=9m87&(zT(HzF(h~9U}^(N->wtusGe{)I}c`a&y zn1iS_C>;w=Ef~S&@IwF%dHQ8B$RzA??^t5#Je)sbgdJ|co4}U58l-t2YK;m*%{qf3 zvEwu-yGUnaIG$P9$@@XVF%A`DF6HNVXHY;bkVt3BPX(Yy%k*AG?sLOcnqx%;+?1V2 zquNXbeUZiqLS|IY#A=QsZVZ3FKH*K9j3#7LFeLRIJtic>VMGV&y01~OOvZ-zBm-FF z#`>esbPj#;xZV8YFVJ&J*T?B<8=@B2Ar8ilblgGU*<+PTg40w`HH)A7DssYMaVaYW z0jQ@=OCi(~%EmSoQ zNexuia@ZEu!+P<8@24ZLlsbwAU#;vis@gC7P~b${Cn#EI@sFwL%lMRlW!W#&S)P`2 zrsc%+K=h(K&DLFIX7}@p3Hz zlx5Y8qDool&B>}+Ewfn~KrdQaX&@(<&?|hGG6kF1IuA|8izUgSWv6(1K)PB0D|)6&p1+U$KxAEH1F?c z+ShB5$U@iFWv*kouGU!Z&I38kgrI20aAj(B(9uOvp#IsG%$1wRY4*Hj{3-Bzi9ba4 zYg|7U<838sFj4*Z_$oK*IXEDZsKOB+R4Jpo3w#X!BM7ofet4h9Nw!0+CDPqXZN`ED z3g0RDCyZ5iMOdb1a~{=TxH}K}9RG%-wIPzMiY8Raz8sZD5*|TZT{)*S9&htvtmxI_ z``7#p*SS;e3Wut}F^^t^@n_rO;SKpy<0M=5*RGbizMnZxzIA3Y>TX(CEr%*M?g%0^ z7=YOO0?vb}Xn2}{K3&YpRH;o@J#9LB1dHWrn3&a$UD@#zLWVrP|uvZxhD{sIHD+HNZ&{^O4wnd*#WoINTbRP zZ75#wz}x^}we1&(x`T@0mBbGq_0gf~|C!L4{_>E2mv8?`LjUK9oqx?9GydNvcmCO9 z{l~)ZQ2*V8{%523cS2`kVE#WR^al0cv&VlW^mb}ce_DuH^rK1XX*kP;Lqa#vs7{=3o?^#xe96|Yr+05fFMQu3A2z+;mm>6JHDhkw zgI5p&J%hWQFKN86K6t%&r{f-@Omy=nPHEv(?|sWd);X-+;?9R04+oDQ5H&3%J0I?j zXYR$5eejOqJD1b?v36i` z_b62KaWv7H?#-R$?KNmXOL^{Lb-Zj4=Z!@@3h}}Mq|r@fGX`^q4csW@CtblpS&i3bO=qDq%9UZN9K47aP zm(vG|3zjL!1d!Ebcyy33qrMmk_hDLLaJWp~9Q|?W<-97Q{p==aY2q#_tZ#ZgE#SfMVBTPO(2{IV`7HHAECD~sN!eN1`XOU$Qu+#j+g=HQ7*absRrE0Rl9+;sA|3d^&%`p-tE>$qy3# zwbWrXfZ~zLYFk-23<#aarE{(`G3(P^iL9Q>YnA7)@!82(&c325>%ne5{wR?j}<H8)`~*Vg~FCp&@rkchl_*oVyBmno64j z{OJh|R|j0=;&?OoOWSHpWpH~;tLTJ8&r88h)a%ByTOXz(ttKC{dj(pFitvIt0=y;~ zGnEW3TxO0i8Jk!^SHZf$D2vQN47i=I=hj#e)Ok90nbAN>&|k1mjDE zRt5SD^wnV#^bwPuTf(DIX{iZ~>KJB+?}#~}>B8pT;4JVQR*O9qd&5O-Qw53F%AK*&&YG>Sm&#?KI;LHv znu?GHa0Fy~hktFiJ5n1^w!>!0LM(s1rmK4w-Tybh_|r-J4}ih+&l9Kr0tVCHHs-%7 zfPXCf{{oDE1pmGBZ(uO8{BPQs>f^uKnQ$G46t5sEA}62K@2Zy?`E6+Isoay<8K#6_ zsnL&a)e=QQ5(`q;O&PBG8jWIz_`LI>;=;de3cMbI$lmHDU3PN6KRI~+ssRqoe$C{7 z)W>v7p&^CetIABaKeK*#J-Pe#-egrhkw}ZmvXXC8li$s(xe7M5f)t?Q#8*LTVm4Ba z$rUGepP@QLOCXOID7uSN^)jSoo}YnQy(gr;+t{I!mNZ3j9BvVXa~4t4V_r4?O+0=0nKN!^C@udrb`j5&&@FUmf%U=rMaI6C z7S{^bfWL1+08>mLw#Wh#!wb9l&RgtVBkC(;Cb9Di=P5C~pnoj_*O3`2t$`$1>1nHh zX(M4)uax3Ku5B}uP#v{0lQ6iL-4>UAbh_lm&0Ah&M#q;R7G@DyiWY!oH=N&X43HvN zg+MO>@b|pIW)K>ptq2kz$%=GX9O6t>bq|XASyg9tfC}cYN4#c?My5xAPzFg*>^RiG zhly$!AWNO$n3KES|C0uz(1CpXwV*UxC^p12!eo3hSswEML36)6Yn4Pwv_MVUm)+IK zNE)&B>`#DsR-@JzBSN{xvdalhX|wHg<*8lbQrwd4w5ZTeUnME^)I+A2lH(vj(IYXh zrYlQT;Pux&$+|A;Yw+@sRJRjSam}#d(Pw}4*?|tG{Ha}<9`$Qb<#pi+A?e!mWWgSk znX~|-5sMK5FDV;aFcK*f;8bV3d*t{nu7FcK-F*RRgkt-Ps7w%1FF6;}3TShc?aI;V za`E&Ne*z8Hm{S$^h`?fV+k}->OWdTaEOfOT@kpjD&+@B!Xju8#3U2sHDD<30Rt|;_ zE9SgGqI@0~mQUBiG{?(NJH?C4B#(VOj&PRemxU!D0JLf>yG;r{B$hxInV^FI+^flC z0A_<_Kz9Y0{gc#ppZ0e-bL#+X;sWcKv4B3~K2hb_@9k7lwo-2T^Y-`_lQx%t+yH?K zRjiadnM!Le%yP*4ST_E&!kp)0nMwx=XxYb}0KvPbGa4#PXe18cE)e+-f*W zQB%wL_jF=wBFXU3{HkY;bfr$FG|h(_lTLUoKdW(F)Xs8kqShXc!<5=2mYRept9G8D z0D}WjJ$sZSe|oGuFt@B2Bcy)^pL2u9`!_lNPb=m>$@zaD!}w!_h3Rh}=#QNLr#JIo zEfSdi?6&>R`ONJ98`noes_OP12Q&75HL4oF5Ta+fil6bX6QVbnQPluJd!clG9!#l` z!zF8iZ#l6#ePqbwa5zw48M=Fjko@Q=4m$pA`?&vQ`xrdC#|UbW4U&%+C)`e_Z8zjp z&2A=rqcyL4OQM1=?Jf|&>SuT7SlLdEd>@9BL|i=Ff4Iz#r)(d%J4lSOm#h-MoMd*^ z)18?~me`zUG|`P4JbZF-FEr!jE3&IF+kmV{nPg&4?67EPTXDK+92?)w9LQvbU_@)S zVYbQK95I&QUE#}m&>m24Y&(eA5OVLognSyb{in+V8~T$Frl?&iaapY%3?QBFsv7JM z>)NcsNMYlh8KG~ggYKNdWJ4qK?IYou`h1=iVPtb?`rNWM*ZI^fx z@@QD#&53&{TklPrMYgG=%A@@R8UvLk?PlZ{{_>RJ-e&~;4?6xFhNSBA+jyh^aP0x> zJe>Z6Y_I`}3rXNoFoXj^LCeH$>Wkj2Rf42RwZ1_p^5-@(r5rpd3;46cQyr!P0++fR z#Nxs$8eq{uCQ(?)KGXU$u-uCrgfk8PRl z8Sr5O@bFg(JX`Z#vLfCl3DqNy*)NU@t(Xw4f{5@m6osLr;t6y8vyTedsVC1U%U>$C z3Hm%x&dnhFN^d=magE8+&FJktx>IKxJ?T2CRxb*$#H9g<%@Q~mq;8AyE3jxg;c_9qm{bK zzC<@q$vaaTf&M9pESL_S?5C;=Tsh;I=dy`i^a4X)_Li#Z<_jCPDqt&7-ODO#OPx2I z3C%{w&rl@Fv6<(okMPOo4(OGjXjAmGNJU81SVBpK@PKZ zUJ6UGFTEB`Fi^P|#||&t4jVF5h;0Jq?&e3&$hEXQ^N-K2mRG%OmCGw5K4x|yE$%;pXwY><4M;0~)u%{_y7hpn%MH5km(tKR{i zD?WmVWax=Vr7x!}bE-jmlYVrC&j3N)f8QER|B!z`l~S#TK4qYP04cjMaWagc*9D&+ zK4;O>t!l5tq*bjv#O>VsXy1WKr56XJ!TE;bNsHYs-#)gs2N}5_i&cC*(w-V#iFm%# z*SJ+u?@iDWea71vpZ0E7Mlgf(zY)97o*CMX#2lG-l?{tj+3u&n`~qX|NX)Wd7`Ts`H1+Jz+?Mt^4NX3vw~qZnKmA`9T4>`3NMP$CK(vw|0F?_Ju__E-(~^}? zq6?ukoFET>jhpT61abM8uq&M^AB*h%Og-KgTuOict}7kaScxr(N#C53kIlL5!a9`_ zR-zE%2HC0B!|PJdZE+(FOTRA8Zn17{_(*7RGhxL>Bx1Rqw6IvBg+pOf1+gkmxu|Bk zsjD+@kd~nIRVq2%O2wGj%3NAja_x+T@QEvsta2-hz=IAeucZbsqcU5o*8JcNl}Z4o zD6`~@u~E5_P)oEzEH`*8xnPD9KtG>XQ(XcHlC%dQg%KW-KH?A3ig(<;NnU)WU#)L9 z&hN(M*{4kS9V{T@a~d=HO?F(rTDQ7P@@c@DXyXS^WBP)}zrov|!;Sw5-u^yS@F(8> zcE$d{+n-L=e;03mc4mI#jfsin|LlQP{PMt7S&`QN`d1tPhbJsl3)IXn3oO7z1S8QB zRHvRF=~q)DG?Oq0)y*a!ETob~fz)JTES-Z`n(_Swc;_=;nc3Un&2G`)w{KNZeBN*D z6?(LaJ~j3Tijy$qu(EP`4@aZVm;1Ni&a>azc7&44)7Q!aT(VV?EA5W;?!e4jcRP;B?4o(6?&;0mH;qm%j$Z23`OFHAZ<%!J@wYDS>D$GeGxd3(@r|E? zL|g(zVtMj;Cq1~*(CyB=g-%La-LW>xq-0t-Otnm2)>VSAdfOZ@|JVTCEMOTjnY3ga zzZ5;)#yxl6vp`s5-8|Uc&Q2l7zgO(#HSG%D4BxmK4vhT-%egm&6*h@AR82Ir4Io}y zZrwu#zU^aCgP;Y5VX<9?@bCWO!*K-sEefdnT-w+lgVlAF4mZI1ioNg>h7ig$Q*;qp z_A&5l*a1{l~_cBY@aZbJ3miCjS6d0 zDUpAvomP5wX$gG)HC9{^6Ca6fzqAE#uB2#YeVZiwgpFMXT3mFO-qNKYmejJZ;jyA+ z2AR}e065`FRmso=$#KG91G#G|Jr6cDXpfJxt`nxOzdfBsoDV^@MnI_vgHax%J$W6l zhuS2#`pD4u^lkyL;@E)CvApI^b#n+Yz+b3{yv#aW(0a^n{3!Us%~{^@atK(|7)EMy zvHLY%p&U%k&L3J4(`*!2%n1-y3(=;O@aaxCaXo+}(mha0~816Wk@Zy9Wupn`F+Md^69F zxvrUyKR~bUuIlPub+4|jt^#n2v3($uoCs~^jlL#BM&zXPbGyCigxTL;$x}ICLEh5^ zZXYcu_>GGoS(hhranAS@SCS+!@FMnVTf+@<9;KK}6EZd;!Od6dA2`}JwvE)El+;Xe zAn@9wddGZ}>ur#bQPH-?9i1xVp0!}mkf>t3p@3MFGrlOgVa40a%;GTZBG(fGZ11$v zR?R3_JNRmo8l1iI(*NM0VKbPX1bbwPlS03o)D+es8E8>ArZgZ@L~Ft?G9Mzm>(cl& z`%FKb;Qdj%TKgV93el`NA`Hxpu6h6#>9K#0AE#FhANhiF-dEAGT1asMT9NpK?3*cJ zGN;~xL#j;x0@? zY++iK$$CA>1^?bzBo#wq$22UXh^c`Hnrb#BQJm1fjH7(sfxtRrlu$~*-Ws0Q5f7>_ zx;tRWtxEkGqqA3#!hg;^iS=lk;HYpJx}0@CDg+w-I}(Yc=T1ypU#A-)cFq!_4r1q} zaRF*m1tH|u!ITBhlesc@;&GURvcVZibNNDXc6nfW2=IjVZ6uEN$oDq#s*o)ntm(qX zS36%QtQLKWjRB`}w&<>2M3HKfL$ORRD^tK1*D3?bNX`Z+cvQ3rwl$2uv?oZG#OJe_rVGQ_ezWU3i^k1L|@ZX}i0Dqgx{zj2s z1%HwHpQ6Yw;(zu0PZRp;+Qx*9?IBBW!b4z9ob5H zBi>i?4~)YU=pODgY0i%queP4nZ3?%tCvODx9)Ef@J)9jBS>Ae1gk{V+O}KJwiXyps z9!|_|UF;2iIBs~l?0#u^?uk^Z#u@{%^c<{w{PNnQF!^9_l%#GZ;c4V{GO1Gge$g%S z{DjiC`2qiY=k6z?FLnFL?Tsh%(oFKoEzg$+=AT@99fip&uLZ#eA-=r>hKZ@PBMvtC zS@Sej66FT7FOv=qcAD_T58t;h%^c8I7A?9M&s&a;c9%#Jy&Orp()3zuL+yBRFVX&P zyS^&%h8LH`T9vKu{pq{T%L6V$f<9f?^-S_2oy=?uPmZ9yM}@`$#b|U^(g-=5kt2L9S?CiS^(xZE0Kx2uTM}A)UcGa8@u1 zb8=^cIX|T63WyYV(ZVn-rWhbU-r~sAdP}5`5RZ-O3 zb-O#l+pbb8kBh2-0H*w`r&+=66j4RnnI6>!2x=L%q;=ouqzu2SzI>}KhyGL2PJyo; zm6{L4i~y%1hmWA3ahxWzlx@pOZKq|?`6Dape%2gj6*H;3-pWxB%9NUmi6y?~j58#A z1Gl1Z0^~{DrsC0}g<6ft!)u)^*w2@vQR>C)R^kZS6t8qC~CHgUeY|R&eG9tuG35Ktx$BmR+anG5Gw7W zjS|(I-w~-?4x!)4iofY4Hk2v378fS5LT>u5I#g>wPzTG7z{K_$Z)Ok>p=2646;!hh zw(km^q&q|`!lUNTu#@58ye3*ImAoLZ#@EeD=RiH_K%O|Q2zZZ<^RPy&5MaDr91uN% za(U1*9oTLfL_WX-qp4b$!Nq~CKsM$>i0?l!t^unZU~^R!G$kymsw((+q$WsL^HLRV zaBuO^%~f`uzVI_otstr)8(rSZ2g9r+I8^Zv7^qZR^rWVo-hY<^9zce;>fhoD3nQ&oWx!Gep(8<3lw2ttsQ=LMn8G{aqa#10UHx4#`>!&rs zK_N)Ag%MDlfA&al`K;a`)qM&^H}u zx2I)j?5Ra=-mu;LP;qh5x^vE0{n&Ti%;Ypo?4OyvBWQtktpYSJ1S!OwwU?<(-k9I^ z-<}We^$+jOqT}@{2y-3j)vV3{ZmR|o_Hhc#>F9T*R}Z2v^=bJFR{Z-u&e^J|TP-Zc zK{}{w5_hp6T@cS>)Nw=}SZaOR^N3w*ctq{d@R176RmoXN+m8RL7M*&{a*%o_eF4Rd z5w$+j#(4iJx4-4}g~V=gAs1>!5{^o_2th1oD4v*x15;#r*=gpsf!1c)G_9$EGTG>` z#ITpSC@q(j*?ld4snXAy?7`Uk2FwUtVe6xxGYIq0TYlz*(!s6P>jaFEMWlS}SQ^dV zykRW-p};0O1M;|SK=*|rC?KzPDNXo_feS@T*lGAek}!2ELn!L=K^EWSeyaz)lM8TO zsGI7Y%PNx636G3?chI2ia&J6jbuOwb4Uqz?`UdJo=*I*9AJSxqiH-yPtml@)hr$DD z0z#+=W5O-DE@|_aVN4FE6*(iw_JJTHlHAxqEBzqaZnjUPc-8oepZv4ta{S`{fY@D^13LPoq5-3c>OOn`ZsBi-p^T5~K6 z-f9CmFe1mVhxiBb-hYw$R&z|1E~2`YXHGyP3rX0$zo$MMrF=cbI0Z+*BC@ZA`Jl@01 zbAaxvIc=^RdNQy*k}VbMN>}aptIS5!8d}rEjg3+CPgkWFHG=zgdy;pVIrF!t+d@>} zjwr*pr$h^xay+Ra3L~{fneUwoa?uTg4$UGhP|ZqX`>oQ#y76}oi<5EB_SpcC_oW9Mq(JRfi<+)Eu zVQxS70^YK^5kZFp$d8nZ6w@Pg!*LWGadcPfKFyU}}=YhSDNadEIZh7lMuhFId4lQr1^|PktRLjVD zywniDn9VvH2t-dR0gfI!exZXR7!2j|1%ba&&l~on)t%M%{2a6sYaCl-_YJ zTa1QxF};n1us4fN4V=pI>&bIRSXxsSxQx{`xevV+hZszXE&{EauLOSxXN!bwgyh?nD*$_21}4|F=!eU)=iEKPu`J@4al2-d@gp?u-^%q^w`HpKrh{+%Z=*O{U3<> zrzP&cLDb)V%YP#3Z!_Q@i2A2#?thD@e;&;Jg{VL-mjA%$r!`!<2{!uOX#9X*FQ(9n zBQ;^&gq)|mWHK2)hBt{7!W+1eOj0|K)jbYD6jk`ls8hOAqT*7ba<;;W{dAFcT;ZG9 z{&Vf|BzyMWUeI^1!a$*vnzxvhhTV!l_idrq$;|A*N|EpDy7|Xp?v;Ey#Y{r^lUMk! znw|LWs3&8ON1`WBR%i>PXxV-Fr_PN-v%OlgI_%HbzpOC5>qo!+@oN1hW%h?h?b58- zZTUvl(2TXIVC|vcN_r)&fMh~;wFgP|Ny`?0yZw4o{>2r;38-V|+#{KD#M4jC{X1yb z`l6q4w0b1iC94LdZa;Y>`=+J*CJr^ur~PD#WcfCEGJ0pS@`P(RSRYj%e}P99_!Wo9 zX|!7~&cNE#1=rOg0$3g_25r$sK$|J4@V%vcr_c-TnmycT&f)z5pwd~rK5Ogs>#WXB z$N=Y^Oy}(vJKvJSnX-9?cLglUGZ%TR)y;6@U^9cH`qFVB<2wgEvWZKb?ct6hamY61 zE7v^}nI^63FUxFa=5r&9%qXQj!O}YVj(|D%iKj?Oe}iTx)V{5a+AHiDku7ID@y5GB z@&iWt%{`DOc-4zcuvsZIiV&~YFCb>5S-uT!hhxtPmKwSblb3CyF~usm{No2`Y7^-1 zNwsv6r}m-OWKXc)JIdG;yCk|GZKC7iR;wUqEcHHpzmO!_DnakEFolssl<3=~;=P!qC;p?VJ$=NVtb$953&E*tBSOuVaXcqmRgP2> z&mJ-Gb@v064zen-i+qStNj^76KKD-8u$vbk{3o7}Fa}Y{keUGJ_4X z%FnWx8jJUKTXeb%DF9hZBb*|rdO3Sw1m8u^i1Ub&t0}-f>B+IzGRaf6&x28M(()VJ zv{1U30`gZt#(5iq$<-nKjG?+Y4qTTPas;vw6F!I?)eH~j-Y(}s-9Tf$7j)VHCm*|L z;u9v9%_G#3<&BrvX$T~I;R*c}t}tPcI|c`@gl3nQzt4{GOkTBy6RVbnR5YNzVY9>T(cZbT*T>XORFsQEt8r>(tbN1n%idC)>v z703`JErM)>6KnV`J5oBE&Ma`h{PSzXGE0t*U3^@qNjaLF471)Z*v&I-X)z<~ZlaO3 z6Wq-FM}!_=hm;REveqs!#SeMGaNqEgPKw&w+u|!voG>MZh-J(0&kpqfA^{$QgRkNI-psAZ5n zts}7qcClGrAp@kO1C{wknvEjyHO^gl8?vd4eFMH0oSWkPwbjQZzEYeLcV-GEub=1# zWRx=g3ksQqq6!ON*u8&xm(KT<(qE z*qy4MOoh9V`t=ir1xYrppM@AiUc@mhH9;Ip?%CZCB~hAb29b%*KGvW)&MZywAdA9x z;3eCnX`0!dEuer`yte`=2O`#VMk0}#9w533-d6X7HpK`&8wE(hwHOopa|ze@Ww5FO zK|(#IlN{ofr>*GRy)1H|4ZSz0Z0*!yp;we!;$3KlO+-lpdCZEo$W9|=8JriphK>J1 z4P3eYU`JsQkvMn26T?#@3ofvy%oSN3@y4& zA4{#Mm?{j!;^;?AnXfu!KHB6$O?~$#4J>kE)mr9J{#&8Gvb~7#XqxASDW_pxHe_Bq zl?0KIPtc$*NU8r0YoKRa;a>*4{|46nE!qg^U!#ruD)@`k{}$H%Jdpbf)d=-Jugs6q4DK~REgR&PzB9z z9|vxJ%BnwB7>vqw=yW9HX*Fz{BOXkc#@Q?v(Z5?6Ct2=iP9_m#>S%WwGfc#H7Q8$P zoajtn>-0~=hEK=l@O-RrczRS;zu)qwHoaJho#iGy7_x?{{DeXunYg95!+$j}QhfEc z(x*_?U=(kV04I`_v@X&F+Uv{ckU6D&x6bFG4WVxXp`o=At=F^>-})FNKFb=*jc(RQ zJDXyEg_{@)+)AN7_@Vp#^I1XoE4&hQu3U4ENr7lB!FABq*Y1z;5e)s+@pMIHxx!># zaUR*@nMY|ZT>D(h^dw##u8VbE0=bG8st}ZCGDSZSd##U$QQ$Si>&y^A@Lt@>4l3K5*MUl2i^tzB7kR^TI1nw(;DG&Crs z(ETeCvjJgo+^txJu(QA|23G2xhfXyTQB_QJkS-szYjeZRN}dg$jp>z<%@b;z2pf^DfL<2lf@yP4+9O>t z-{kP`S~IwIar({(XEPoxWfYqKZAZ|l`Rj7a9kI)6iY?EV#a&HWITY8K&?X6u8ZZfO zSG;gvCvi7&3>^h^>&7k(8qtsFt+2;6z+{H99-~lgl0=fiH%57o^K{4ppPUG3Jt`9b zS{q@F^zRw0me?`inXyIefr8?aKux*gVA&Xv)wgMYX*W@PKI+>O7DyeiclnokJB%tb z5lW8r_ZUZ`!I7^sHTIV`sL#A)8b3%hO-FSt}=L2mv6D{Xi8zyUG7hCQEyZ|IySl&?OgA-{b`=$yZ;ENQMYoCBKh| z{S=UQq<1V|gzqGKfpj|?Q6F<6!mB}VL1Z@JFIaXyi7p`)h?R`=ogQ9Q^crEtbJsLgzx_-Q zY)%SY==iA2ji;dO@GOyev&T_u4e;{YUh^lE?PQsQq{-4aoADt$?I>Oowz8s+H&_11 z-O&x)+PpY=6Q5$;_aE))rw%NsB;ZV^lr)gE-=`FU7EJb36F<1%keN%GzM(5Bm>Whw zTEsu=$fQ^kDhbRF?=Oqqv%Iz2rNW}8zEY{g3>Jnxui##PdGh6Z$xNs)XXckqfw&4t zGsXvM1G=FB6d-%>oD(OPSdganwuDk`iXF7MFg6XRgB?qc#Jdt3Lb;Qtr=`$hb(p8v+b|G+){&kRRK zn!4hE0izh$0dNHO0HOuQU(vQ8&wqZT!M&AIvK#I)T&NL?t#8u^w~YI>t^1Kvxb^I+ z=nXcMoNYF2L?%a1J_gTi#4M@HJRTmQ9r2x_FySHmf`;y;D(hU}uoj z6JaVddr<53T_KR++rkO;ZU5!<@K*olXIJ%+(QZ2Pydt+K+3w}q@gHMXcjN*W=ICs- zF*CCAPs}1ysI6}CD+-)vPvKm9msH)cft4W-1x*ev<>2r5w^9X=9gxug${VIx|$*{t}W>*`g#DvwIdG z(+y>dqgZA7>aa`rTh5wK&CLV>m;Ev^j)E6!BC(>%8IelrkVGb@DEqepLrDzq#aG@j zhgGLb%;V8IE4nXqjP(3@k0=D6+hz*`YDd^9P%L+*{czV}C}NOmK zIv0c8g~tYsv~9r<=A-&C-K3v*--3hkn_MNYI8Wd!sx51Cgq(#})8^za%Iu4;^pZ@Z zUwNuZM6V+_ctpZ;H@hJ(Tz>M(a85#0cgMdBtwJs#-s}=xmYD(JEKnnLX+%|@c`9@_>5^%76N#bi0ryucZHME*ys$P@!c7Z!@gr#|v~GKxr! z?n?1-KXYZZ_Y}jMUf*O^8UjHxiRd#pupjl0_|vh)G^2B>RJVrPuI|)H8+mNuUj~CZ zLO!5ny&Nzu3~nZz&3WhrdoQ|6vA9cR zbHc>E_N?*=d4&mAk?G@omDUaf&i%cL4H5Hzp+3yvE8Zt%G1G z$Thy+OHXg-BpSS>>38jNS3MRq8?8kHYVXdWOyqbd&&so`6IpEbl-w{U-&h1%kI4EAf7=s=&XWu zf0u##2yGfL9OL9VS*J&;?8WP_xs$f;yjimP>{+6uas~07a<-TOqRf&kED~zGq;6?) z1Y$kU?tEa5@})QK&-oAEXhN>=Nh4jFP5U=H>2B{6&$YRk$;7tbDweTGzL^e*O-N%< z&yD^rj75CzPRyS_TJf1?IzRYCv8lbH(Y1TB?5Hb7$_3w>v#FaQN{KTUc3{jL3!<%~ z(@Tcu<5xbCZwn50Y5*wGOSwMl8NB(atGS5k8ZL$8n8+q{+FGn_jrP$k+>@8r_eRjJ zbCEShl9Vb;#s+L0w{yY$)J8ETZ^JZJH*TP4`nbf#NzGSV)qJ7k%kG=gD*bf~py9L% z{Vlw|T)d=T?3ymGV1^7F5Z=AnxQ2X%G7fchw%d;;W1j7j72i!@EZM=0LXOIoIRxt% zO;qMEUOdR^#=@01Jg7&lPZiui=O(~bk9}^Dku6~p+SZQO&wHFk$*xVSWS=s3V3Ul48 zjcUdFz|?L~O~5h!SZiv+QHY(6c&%_}&LMJ-hZ>j-9iAy)uOr;u#nliV{RC0r#?kzL zAmE?Y#{UKZf4k=YiGaUNoqr(UpXSH^=?)O|=XvK}2*}F$AKmC3j9TRSXO2H)yiE)W z_Rb|UaE`yg@&=HSg?LjQ0$@`Rbdc*C@**M~M zlAUyu;1OuO({%g6UH`jzMI+iNe=>*t4#;C9Woykz94>>r=_~BXyS|Hy#w+@bTHU>$ zIUOkdzG&G|A-*I^-c>^p82#>*?&P$WQwhple&VMd6*MsZbTWc@WYkN(`j8AR2Rp;i zba$RDg&C1TRx5pCTJN2aH`oi1Bcq9}ik`T&vn7!o~jDV8R z6qqs7KoltvGLi90Oau=RvKMPdZ^r#nRGS9VSsV$;Dp$9L>@RuJj(J_ovmByVQ-}0J zylg)QqYMiQ%a8~cBvd!t_?;nRQyx<0yX6qgVMNMFe{j9(qjz3bkRG#gM$0gCbp!er zngvt~MXHuWt&l}tTlG?w>pBY~Uss#ugYH@rW48g7+sjG_vuR*AAplE&H7+@STR;=MZi%k2IGNZ>t3nLa#4u*E5-8Tp4-D62~iLLUR*9f13MyuSzrduU2 z-UusxjtL_6qhF_0B;(EI(>MWn&P&;a44HZ;8EuE3+HH@UJ2XjBRH{L)yOG&TX1(yI z-*%y1)X%9?@i356F3wDJ{{qc&2vswYmI*%(_C7IupVDGif!c^fM%xc6Sn~Ew97Lu0 z@P4bES)myjoGpj@GX!m6t!$a;K^)mOts`m>EA|uBbvtSf9rSYdHi4iP)V+?bP@i^z zE^z_QOT$yPFZPyHp++*hFyuqYvRP@CQz%J8XhJIUso2u(szasHYc9SvHf>sq)Y2&^ zS4?J~!LcZZ@LzaieEk3=l)x-@A&$ouLLE4-la|N5r~bNk@rqx1R~N}O@yrB&c3XvB zvGs9@#;sJ2JT6=ZkTq>J+twMBab1)m(Y10OS_V!SVIGW1CsLz-D$LK$VAT`#^{}y_ z1bWQcyLL`AjCu+yGTs*pX-O;V?r^3UZKyFJKPha!YU{H_(Qb)SSr$9LKY>`~n=Frd z{%W;X+-u7D4ooc3Xf~CbEXV;S=!A@sxm?3|qOPsVO6p-M_a^%`3ED5GbhL;22TUrO zU18LJzBWkobbWWl?d6ZG?XlB3S9%(5Sb$q|*PhxC*YpxFi0O8qA#C_FU!QxLRPc*j zx-6hX~y}C z5V9g;ITJ&Bf(jL0sKX9xHGW*+J2Wl&HJJQT%r1>D0p7tthLY%3fjH^D5bdA_SIa{{ zoD|=wJ<6#Af+~61?cbF&$Nwno0_eAc`JYPqU&Ahd{xqijw@Ui&+^k>}_yY+3JWTsb zU9$oI1Gm8fjbXb(aPqfN@Htdz+|;B{%olMIAvZ=d4QGe`YJVo}oJMf+w*ceCzhxC9pKZiyH&1`fg?RBol)-v@(n|xm+v@! z`rPgWfHU6cZc(8T2eW9ufYz4jNz{8XfzMxS)_vy-S%nC}2O;q|I=lPh#gD)PHy^Y~ zGf{x6P2g?#)@Z8$y=#0~^`}=KRSfd=!vv{iJxf#|G zzF1~eMWPJZkYS-ep2Pn#9?5n*Ae}qL;X;9r>8^oE5+yvhq5)p>vcqVzVnQ# z`HhCPk(-(KS(FQL(qh=%Sr!8$5|Bdc40fH?IWpaiYA-%876v0w8-h~A0}PH&cJ_z! z=N&!)%g1VFZCl#O`3HZRe)=A!^deNG3M1)-?%QwjHa5me*GKE+e2rHo7T&k&TSnRr zmdXn-xwu1U_w&6*KmqX%1wt83O0kbMjj^7zirDIN0F5->0foJ+1|Mg=a-YvL2jk5m zD>@;UHf9G;&f8Ak=jk|du0~9Nd)u-+B%puTsdw>?e$#g1qoF`i62w1=8m*lMxwHMVXkg>!Dz-F zZ^rV8B9_VDVH}og?8`eED`{)$;G2-V{zO%J;iId@6+=PO8|gMs+!Te`XZ<2Sh4{@b zlhTdYk^N%Va|E@)U6A~d>gzkY1B$#kqE_N*iP}z|xP{mEzEtt9BuA9mc-QO{8y4Vz z6#VYy;Oxq%XYUE;#jP2ZFm z`iVQPgT7ezCL3pU{=GkYD^npk>P%S7%OIX!UonGpA*me=YUZ0|;+)>E*~MIh<e17P{H&j4T zI3v3$BAA?k1E0H#USYZ?+_;qOnn8+|o~ zBn0|WE?nFoO zJ=0De9^jQLr4#=T#s8O=I1M-@wydQy?Q@l9QxC^q&T;?t;4h07jTjFb3mb@)g@u)s z1;onEsr`53KTmzYI^M*^7@P;2;~zFGQE>WeXM6Dfzb#nIzy7zhw-Ws| z;3qal=HRsBzpIG+%2m$D%E`{e&d$vRc4lN`W94AtB_xQ{{I2*Ys8-U~YHtKJz3^= zQ<9?6V$UVdf?@kp?*A$Ya6J_dCooBAJ2U%VnbVa`%`L&XWjz2i!r(<^N{hs-;Amp% z_&gmn(k9^XSUP)v3sqd+zO^y6eI6gn^R)b8_iyO~e$5X!ps>bs8h9Nx79bY@$jZXR z3gTb`0XW$}Of1}-93Tz=kP9q!u+-VP03dd5CYI;NKo)K$4i4}Ha05W#KiN1r*}=2I z#=*@5VrKNSvIE%Iz|;Ip9K;C(cmDj8jf;bcl^r~5>|oYFU@|~( zO)y97OyKUoHQ9k6a7{3AAOOV8!o<8DH;@$w9uhlPj{o4F ziwn#Fn8Nd@I9Qlip2-0L9Gu|N-_qq^Re{lV~8i<=4ELZRxfX5C3&kN_Tv2y~!Gx*Fg*ROuq ze@lcD2p$}m-Di=pgUNw@%@K$N$i(&hPOt;m!6N@hGcFML{Q+M&eC8cI^;{g!@&|CT zu!0xFvwQ&DY@AGNK=3ng1t54rfAa@o<@o12tOtH`!E0XCUQ$|2 z-smlW`L8AYr)v3Aef{H0l%3;0_z=}omRESsg57#S?>W^IT#%59NXmK;6iDK5Wx^+P zKNllqiR87qqQkR1Ed|AVAz)T`SrD>a&DzsFp}|=~&R9kg;R;kRFA8;WCXkDBD1@&U z!)~{-v5mW1+u&gaaujB3s!>K1*thdB+nI8rd+zVL4!3d1+p$o)UN~?Ce?7A~;*2!G zZBW=xv)t!jR6Rym>DdR7N8j11mxzGU%_kvAdard1LXx33E%xO^8)X2tf)7 zCT0+gRMfl-^F4wSPY#p_cg7$tq$ecz*Ku!vPi2LCM;x5h@y|B6;LkJ}%@0xh0 zUDRvH5iABM2B?nLoq%ZS0K^IhY-ikYJ5@h+NEggD6TD7$(u2yQFy`c@L-E7Zl>qi=@0&1mIv5L$V|-Tw}Zf9_KLhJ=5?7qDji8;1T>9sUJ0f3FKh zslODti4j;IgLSGCKTezW7vzG!m;NsN3!uS;%)dDl zH3H*^y*U!IqLI0&6X5qXE#_y~asvFu-oIP_iO|1W|7rwY(e^H2J^UPfRMyf2%-=8X zLBOw^;K~5jU+4jT3ij$y2K-7T1OD|3vH@(rYyOJhrwo2Oe^p`sWupVI|Jnrte%(n^ z25YEa&%kQ@xtCuqBH&-Ydjb9K<=2K3ES0|*{_8{KdAML~0E-U%7Xa{&;prg#s{42K zUpV)#XI4P4)&rx9rHwQAbL1JholV6|pGE#m=O0PX-7^Zj5e*5an~u5J^L$dr+pQ@g zDRj~$lWd013(vCVqz@^gjH#6l59xbJks%2dG2}!nPIe!) zwJ}m+EG#+>PsB-DhFcEjVW(-n+lertHri`D;n!hzYQX>YumjGI`ZQ{B;IgO=35y3u zVK`Hv-GH_E)c&C%tiJf8qA#lLV`RFz+_a$Z;wos^6=n0&TlxhLb(@ua+NqAHhFSOu z9kpHR+1#TM5TU(hW~J=-t^8U2tj%(41g>x#iy^Crlc2Ld5)T8N7KAB3aV z;~-yXw5GZHOwSDuZ*Q~RX!K3u0T3NJ@3ALdW27Y63o2D`~k~_oaC=D zevR~l^Azd_+=6yz`s(Z|r6RI=fINi4h1nSL_Q$i?Kx-kv6xb}NmQGw2bf{wSa%R6L zh@%`Fhco^hVqi{^`WuHeLVYntipQti@4yu)}v^oB}|Aq59{*jl<zy>ufN-{ zsLOwI>6h`7hvx~fa4bxCOe4?4ty5LyR1t-w_|P*_t(-IEarw!ss8Le z=8xXn%;psCRY!FqdZc`dw};E4ajW=Jf;|GFek!NOD(h}E3MF+oqP~8jCm*KcJ8Elj zPBMNBBd+(dCi(nKiS*gj3@|-zzqea|z$Cn9SqP36I)!=aEu@a(6ZYTw>bU;#>HXm% zwEYX_ce5h_Cn9$thv*+_S88hog%)qduP<+O4{zXJ9k#dedGRh>qn#gSVRE;4nZVs| zLZA9$@_H?XMz^_Wq^X&>x*U>OxkggV8{}-S#kNR>ewJJv$cg8_@p2z#=|@9BM}NVI z-cT(XDB!wdsG8s36WA^Ui}(I#AatNFK1CiY579Vo$gW}R3`vep#>_^+a@ow9vt=1a zOP@)D#U)&x1Qlv6hjj`ly}L}9#Eh1$m(4k)06$Z^e7qD#CQ+L%{*$y!E?kUat=)I| zD&+xL_6amv=}ErKkGvLidB>_RUQuic!+|w(4$-t@Qbk;?iS|f<4EJK{d*eITvG3C? zW`4U|9YzfJmV%7%7kyZ<*R0|@>sj#_-{-@%`$PGQNOuYHt~02{gjX6Z97ZiIdX~x2 zRKGwg^sL4HynjVqB2x01bDcnEUZBI=rgNG%sWYfWuMH6J*s%FTW^m?qE?=5zt>0pE zC||L+sbjzH_%x@ZJ2}_#?V#_ykFkl<(_BljtiQd}Rx4biDZPNLwTZ0BL4%aZUQ$7E z!_oPb;!RTd`i<=%`eML=&Y3{KH9*kHdacU#uD#c5DOeH`>XRXU44F$9TAz6V4D4xN z$HVsUdIj)!+QS91F3@61Fqm?cOaMx?6R9}x0dxjGNG*ljD^#Soc8xxeb7-FC#9F&} z9;~6rMth<)J4hy6B8}tEnb0Wgp&mw%^13ja6rcD;?1zn`H<_b<^eX=V1$AHTp0g#9 z)khScIKLKQK`pO-oL?x_KIaCdgrBpV8J96!exBi=m`Ls8PPWi2jCaW8*oy>@CiO%QN zDK6V;XGdRA%_1w2`DdwILn$?9J<0JrpBj&l2vRaHl{n zP)=2~V#EK2OB zTVv88NqJFGWSN%o;nuWo(`$2blX~D?h_Jdv%>Lv@{5Z6d9DRztc-PWdSq|y@=|yMg zyQ8uhM=m0&aJ0#Y{tX_nH2!F))wbyek%Xx8@hBSnhNV(>fo^ZM*a5O;QqKBl{)r};rmDG!HB%3U;PbDCW#S!xCItXFg<+6@%{W!Xx z{m$7_*W`>LBgy(Qhv$ryk;>j9Xy8jT)Ju4V!hEhu6PMhw8f}U5K&6o3nYFYZQ_7`o zJi4x;se~hAGW%%eY_B%gO(b#5uMf=Ye-)tp*x{(LpzZ9+Ia13>?UIaY|n61LZtpp<%{ z2O4Vz>1dEf6~M#ZL0YfytZ8w@<6vNRDd5YoW%hK(sE<;Hsc+KWdWL+)>)A zuy;KwaXuZD>GnW;mtiwE^U5cYow8 z=-Fx`=4lZ0>Ywzq?EuzrI1EpoR|zUo#H(OHD&?ji*ez;o-k8a9E?3(JRAzkqm{5r0 z#MNbVV<+L=#&838Kx=$orYiPU$^g{<)yjkMW1?jilwbnnt2}b&R**4eO{nvW4L+ee z+*k&`fuKV<%o@hscxu92tcTY=ToVSmSlOm7Aq9hd*M`&ojgoAiu2AdkJev<}Y`>WBSE< zXfYyy0aGx$alU$C(_zTmU(4%XZBc_q59b3vxNBOdpT&^so^w1ggVx?00)|b?+ z(%N=HOkJB_21oO;&QUQ!)Tj%@j5VSSjeY2pH(1Z>V%4p%WwNz?9k>AdgnPOub~mWmOjaFma{* zXXa3o<(EsMbW+zGJEYWWOXX(32od8+F&ti1>^vopRwQM!-hDX)T4!1aEblie0a6G^ zE3{BLd0U&_tM|!mtCU~9AMkTU$XE_GolqqU*WsL^z^H*KG#z7QP^k0 zA5L{nr znUbRfMiAx%SfR`WBsv!|JmmZ5LN~@P{Fc$8@Fh}a*I1kIqeX1)+B-vSJgKB!R*QR& z3t=Xj3gD_e!rBzvI`rm;>hxoN>+5*00i6Vu9H*iVf%r-(VWY6_4uw-U7aTUn=-$wa zZvK0g+-SzkW|m`{b_nRUVC5|(T~Jue`w8Ujq=M~leeQNtA|zinS7JHuBdV0vl2qqu z+083ZR3Mo&QyJ{(vJKi>y^-;h(MsTUk3RaJD%!{voOO8np8Eu+FtB0VI=znwoeCA} zM)92si9n}BWfA&Td_SG9UCsueC-DRlMF|i{FKx%a5&7};GwIEBPwO-_xd7-^1XTw* zF}hSWO>%lvW2Z|t$wMLeH9Ka0fB!91n($KMH$&8{t`tRJ#5UA6W1NG@gsx&ctS)E4 z7;}$Mer-`bmxBPdk4N1-@yuL>O;R#!ScT=j!NHaL{ij>Tj}}{#H)_imbzgJR~mS?YrT=gLQWGnN=!A0A8`lYl5$wtfYO-wYwDeC>>A-Rv$1C9WYS|R)*u*|~s z!bP@LR$MXeTe5fzn*(Y1+;m)D4%>@cJO}Gd8P{*=y}nS2&Vyl4*BKM7Q&-$Y1z2Zi zVKX)QBRd*A8D`%Wn~EWv&cemSeRQ@?=K{2}{C z?mnM)tdt`B704j|bsiZEGXMCVJWqMn2D+S_BWAsbFu;dD%R0AC%0MGP5c{%zxSS(K zDV7i?gpO!!L%cs+zW=Hp^-G<P7&j%tL*?f_iZ`j6B*RK5!3c#(8*gHi>-aYZ&v29FjOl&8UOzfm%+qNg>#P-CtJ;B7bt%>c^^MBVm z>pU|TZ{PK|t9I>GRoxf6fA!h>Fr}FsK2{i2MbpgN3mJ{PHWraTpDGm}>HJDzCMs9L zpYS=J=_xlPE;kBvKlj?Cyq`p#s<4FJO`u)Nx|i><23_ZWlk~KefNvCJZDXebjp6aH zuj6#av86dqj1iZo@YGno>PWaVVY*mwc5vt(#kS<+;Zxz8B`v^ekTgVG#0Wh(wzQhr zsba*Q$xF)+tG)+gk7CnBz0U1oNc|E{yB9a6a7(>F)0FNyH=;ANUL8)@O=KmArC^Hd zx735N9bPFw&e{%HNwe>o5x#?Z+4gX2d~U%$O8V@od`6yrz0@E6=c9(ubN1)Vn!5MVtvbjms+`U9 zi`m*Ud}wYfnLae`v99`&neFoDZY>9i^A)skqI|;V>Gu;Z=2$5xsy1h`1~fD%`6nnu`UgGLJ_I54A~wh{QO@{J(@>o` zv(#>}G5S7}I03zAVHl^F#>9OWr*3%L=@p%WFe9lO+Xu5-OR|NI5BKCqgF-%&kGi9c zxIs*;8C@|6LJnc08RRE>QX2BZq>7s=dJUZ}iqb-Ij8CN!D(>3aspSVN+!3o^6q({m z!lZX%9{r8AymPQ|>TcIwW&QTgC& zAH{Q#T@_ZY24C*wEq+IFUkkH_Fu=qP`N< z{*-gGjukAuT|<^+W#xvCJ+7VP#i)E%j3VzM_n!TV@^t6&t6GrGT%%P3J2`aKc5ZWt zS~fG=`-?rW6)gs~|`>%m5v^dTL2Wf3D5;q71Dg0|y zDCfw*BkVKxaU&y9xHLzOioz+8`Yo@m%fGFCT}?Tby*;nv_51n9o9pm&zhLY=J!yp9 zE=G$N?ctF0bUYErM@|!4KcgHTWYoZA!`VB`67o~l3aSbakg({Y5@jLz;}-X~p@@y0 zT3vtXV&|YLk6YMmx1H4?{BDyi2!-g7w82$hTM)=>OpEUAqha0BWM#&p!QjGm?5St# z+-py(*jQ8RVkA_}I&PiDY&F?~3I`H&a(TyB50ENYoHnHNBkdLaszns^cdnkj1YZO2Ij+4FrBa;e-vSgi&06Oa z<7}TjEwDbhfXPwCun)73g((RT<0qrCke}wGCXlpb%E%F`hSh$FhyF!(z`DyN^i2|* zTK#hsU#av-2W7P_548lJZg=CURe84O0q)VSH{Cp=Ah=Z)qkhyS4}z<4-_)_rHNmUX zwx^2G%Gdz!{;0OBSNxrlK+**S9^xn7>v=)%Eb?s(T-RPJ!GZg5LV zv$?K#4PA^8WfwB0JW-T&2uYePFulbz?he6k35c53ZdyVfudK+7Z2p`?K+qCKHsQ0E(Qigd z8x~+oauN?kstJKuaLB}UnbWXUt~JrNNj;brwnH~1Qfuyn0T+dRtq~!C_A|udk||pZ z3Xt+D1wrxzr(fG%b_Cx8ru#q{Y-1bJ7Z7pA21mA7iky8ZIx`*>~7AtnqKS>)O`qZGz;6r3irI%l1wIu zTDVa}%J@;*#L8j^eWnguHDs;Kqx(u>tXLv~ioQ%24AWwjwy~XJo~m+6*eR^%wq~1} zM@QVy6|h8>5GzRpgU9Bgdy7aQkW<6Q>_}M1OXwC^%&e7=u)muk4RQyp4#s6x20>@` z^I4MI2xnPi;R=3QY^AMm;&_?t<5?aj&Nv%~cD1(-?GGUMM*X<2=1v5a8ie_5VMP7X zv#_;CDsu|INI$%uv$|Nn=ty_D6odi}$V{ z>9+IhnT2hn2Mp_&Ti0a$kYf=fW62#Ea;Kkt1_>O8*1u&ql7Y%bqKJ$Q5|c&<`|93q zG8vKZQkmARp$rwsX38bTU3Vm-;TPX{a`pHH$0it;y$0qm4trO!r4hE(7No@;1+7HH zQ|ozqtv-uq=gfe_&y;y#Wj_75lXXuB+6OyLCgj;s(8AULZ?q{KsDI0jAB-HKp%9j- z8fEXjkQM-;5Az=v@i-EWaf)P$JXU!qhqgBs_ftQC5!k|25`v6bqt*U4knL!&cJ7$= zl@E>?H-Bc^_?I1oivTX#t`Ru1h;hCIDRayi>I})UNIq@GlNJdWRtqV;p1l^j8m;Ni zB%X80JU$;aw~KstSo}p+T~7eNm65IfHi4!mEe^ox44E~$vhqrzbG?AEIqRL~D!W*- znkB&nI3@S2!i5mg+5(M&M+=q_6a==CMai*qThA`4l;l9Uq|vWrh|Vl(uHG#~CThc9 zq5tZ8L86ZY5%sKBQk%&gM*=AS;v9x-B0`YY72Dq#iSZfp$(#Acfjnp8le`f+J>}u` z6G8a*R49s+Lmqe47!s2>DcFM)qWMKt!N&7!vz^itPr5ui!~yIOFp42tt)_A%^T#9t zo{x@u6|K`gA6~S2E3jGtRx7wdGkb9w2Mkmd_9zKd~oMb@}r22j~8EE2-K(Z@1J zQ0H&H_6bs09OcJ}AMRU?#6M^NDufX%h@qIKKlWReNhk~qvBPg%Y?%SqBJk^d&CU(C z%?iMSGIroe*=h1(0DPOAye^7O-Kh*~U4KTc>;(S)6+IMT+BYgn%{>u_>e%_+_i*;6 zJtwVMX8laq47kTQMzzIBR6HV$8Luq8b7u`N9rSL~a(dhH)iUh08f7}hcE5#qJ8OC* zPwy@(v*t?RGtC^XUp4kM9aTruPMhCqN?R+J;$D&jtCWuPjmSunXwU^l4yxu#*?)1A zbE2|mIWhPE9)6WIz09hzo*1Eo%e1;S-BFsR5g2JdW3NNOq>W`czsuQ$ z9E{V7qu*T$t0TO%4TTC^8$FCy8l;I2mG{LVGICFAW(ty=Ab+OyF?`n z$CXo)bnfhFgV#eW9GN6-?DvbNvjysCyl5q(n|QeJ8HG z-3nHh@B=^2bqf-MkgndXSfn}^4Z|(B5%<8aXWCXB?FeabE1uEU?q)^wCnQ!<=Uc(S z*HQS6wkml-(eji*Eq?BUNNB{BhX!dlHuJ><(dhnWh>`w44IKezQ(6}#HEh}cz99lPG4|7$hmI_ z)|B*$S+`Ds8k4x4BAOd($dW_v>)A&24D##~--Vj-W5Mr!x5`Kt+rxg^NcrX|K*=(H z%(9$elT+2S8Oi(pNIDms*~eU=*kyfYcH}9h=U9j#JS5+K!(iA=K$MKm@%XquRh4Zk z#EluTlqM)~pc3NF&-R=%GS0X!VLx_}88&ZUhX2B02#+xrROF9S=_y zk#8;0kQK?$9UR~+nx$A_0;p49r?61r6JZxYXG;axp7LZ1SZ5UvGC?tQieOY}b=VBV zjL5@$bv)i2)tL4u^cQ@Q68uEqbbavV@kNZZ`g`cfDaW-3h=bA^8|JsA_+|%=NGXj@ zmIl3%jPKjS8)ETZMlP=PtxDqA&sSyd;dl%Yv$C_k6O%8j-NDINzsZVF&EBvya!s}n zN#H%BvSjRPW;Zizr6(CLY>u-w+l$oZ-8+sOO^aM?Tc0LsXkG?)bLH6#FO_)0X*NF{Mw;++8D0gLhz=} zVDx=bd;9~QYVO*@PUU&^lhror+l!`j`&$)hA*PwQBZ;(eTcvt9-&_gVVb-+*-Fl5I zUH!Ozt4rgX*ZR8(eG6s%4G0hGL8hZB)q<@n@0jFzf42j3CaXnDexOUL@=AElvsBf! zftp9WCwsKd(pwYd-p{JE;iUGnZ1L=0rPnD!>R#8334(=Eg_c_od|b{@5>%Vm{r<-O z@?IlP^CX$b9^+wtQ;49c`;gbZR#%wB>^k^rm~zB<&De zb+yIym{=6GVl(_^$(wI@o`o!?61PL;YmBKacMdL7{UKUn*u6&*^!4XfSkGt&rTG zxna+aL>k(#3F0Oh%8sx;$`GEougEtBq@Ory3-Vyl62nND)FM5(d`*;ydZG57vM^_# z&ElG&Z`NWyqaA0#{g8EXl0rt1OofJtC8v0ylAF-@qPXhRkT7eR7)XHb*DzU&DK3jo zTw8|zo{scPZ_^Z>@_yr8-fsg~N~)Xrj25&o0Cz z3_-OalpYM_?}@!!fr5i|R5+(}u6d$q8;iGBkb88yxB5-7P!bcfG!r^fveRPD9L1cH zqAn91b4$^_dAq}N3YAaOx%rCEo+-v)XdP)Jz@qf0s3RAtfk1rttyl=Dib3OW@b{rK z*oF*Un{VLOT&&}=Cb_MQie+6i{sYGybM51B8~e6*b<&aLnVG(kW`?a!k;|iiTIs$G zKy$n3v`D8M%^O`l#W-3-oBMZ8vC)bptWwPM8I;DrM1{)QC0fP~+-VTm&#fSNgj8J5 z2*di4#Fw58p4z01Y@Hdz(GbE?jYy;-kB#%9X-!$?GYph%4ftUhVU?QWSePoM4SNRc zh?!CKo2}!L+*uIOGRgY=YI(?C(#ab}#8ovhx7}3`n5YL_NNpDo!@UzhMll5^e(>+) zOW?h@Eu9UUx=Blsw!0Z7q6cshLj(WcPGssWElhh%Ajj5zCQz(%+rEyiSJ9L9X1hZ7 zbIW%(PSDZ&27G5dJPhfrwLw zQ7tJ8BgNA_0z)v3?xVF7t%-@I(WAXz0spOr3h9K;A&+id)veM81VIV>J)p;>G~nbj zZeRjIMb!jCQcrw5jz!7t32hnOiDsGBNn5$eyh}wwd@>PrQH`I>RuVFFLCDdxdX%81 zlA0_4iCZJ1y1%#yF|HW@5vqi*Ovxg~bIzeL_gqEUlHAB+oE#0w2EEPsxhxoYSN--f zU5(xqhkS=ZE<@}i{P`6wZ1nMR% zb%)JeOrhrzGhi@hOG3hoXT6lJLfxuaN4NGXL&@*o{a4?mLroE`c2*iS7jIYC(QZlB zuVYHIlzut+N~xD6qA)nMv@8)#^vF;r#Bj26jECy1`#PBNJLmks?s>W zvLTC0)QOn~n4}4>qVA)vV}vbaSuzxqTpJ{f@8$zUyvSW)zDGr_;4><<^v%A5d1gxm z>vOZEjf;^!gcUJ#x5SMy@(xf|*W2p4IqmL7Qa)EoM^&uvI5jVawVR%es!Wzqe{St+ zz3(yutJ?0&HrdGYSe4^@_n$J-?R?yT4)^-$m0ZcylhUsR8!a*l{Y$!w17KX}f4(2a8f6f97irZOVMl?@ zaky5+yx2*V9wyhap9b8V2ouuMmzwMr;W%{+U(bFqD2s^-e zaKT9+U+eBg8K~%m4I39@W_Um6|o#k+%#V?4|R9Z2~3O?99E0m@V*6gbaU zki-u?tVrstGB?XZuR@$JXh>ungCdSm+&4zW^-w-KL_Sn&~DT(N7Qq@pyh^Ff1%|DmN6ZZ_RUQ41( zuNW6EM952bosWcVH(;ekMC&HMJ`~-Mqio9x@rNI!Lm&LLs@R|8F^MQE*9(j4YS89T-;?1?fF==uuWjapE{llx4$?{=_?hJj?zPca|yN4T#w?wzBW!C~yO!hVP zF13Ci{qP5PlRuf+RWdUR*@1kpOv0^lPlj;~c+{fbUV4cZHB_R%XN?|;cmAklIf5GC z#EN#*()7sd_Vi9`IpZYhlbVD2wo#_c0g4d=Nz9i}qArF*pKIV@xu7mRh8tGG65V^1 zW)AHjSIA%rErYEw9xq<_EyK-R8}iK!;_TJ@uLI+avwirh!B;6y)E#-{d~?gji9_A+ znk}Kz#P5(UYOB%}pmu4G=vqc5l}be`%4>kuqr8)ogNcx_;o!USSL-ZO;q;j$odpi* zb6tJA&uk8jm5hw{v*&TM^i#7{=k(a$SRSj@2fBD$ z@pu%nC=P~Al?#S<-nX8M*(W1|nwiz=5#HYt!l-JIT{`sv9=}qT2~EvveXMiL0@Wr6 z*rG`zUhgd$lS>96drQAYnVZh9B&Hz{C4L`K2VP4&V~IDJKAIhZ z_otlor+4S!3EmG5E7Kc!Te!;F%5K-Amw6Y99j{`?9D4Ps`LYq##OiN(jiOfKMt0{} zMC*4i=N{6M?pI!sT39ZwooOiw;c$vSuwI1}?$a02k{1!uN-X<7PZIA>q520{SRm?K zAU=v&-0cFiAD|7P-x?C!T)AGD`sY!h0(Xf4nUAVGdkx731GsxJ&ya;2`<#UATR(cY z)aDKf7jv1&j>lnekD?G+kkOIpNP>a_?=0W%wN4p2=$-orL*_1HGsnNc#!=`;$1 z#hs+%n3Ja+jHBS|GQ12In)f7T{0^>Llf@`~Q8)EJ*B!J4a|8;DDN_2p4xkZN;d@A5 zhwbr7@ZRV*<^tRcncEjJesn;`ia)*2rncG(QxA;+Oe|SHv&2!-y5?$6SE%O_V*>cV zF2XH?dutQ;qF>$foRw)b8WMatVm6=qqC(Xfc(aFh2DBxzGVUnHJZ9*0FP}w3wno@B zyEWQ;jr{HteDu0o%%{_%eWV3f>pzH8lZKsX?~XzUz9&&hjndK)w%Yz|E_F*ee7{}V zZADJLt6py~#AV}uB#@|9?{a+`D8}k`I}G^Vj(w}sUT_$wb?)n@I2n=jc&sP9aNHcv zoC^imLcf)Y$RDd_GQ9*(lW}UA1VQwm&(SjuKtvUGl;T2^Wz@>oV_!N*JvCDSOen}4 z9*cyBr#Sk3dAE@zz^rsmAF!(N&vNTrFn=7(wr zD}}m-s;tA%Tf_9XweE#oQ)9}TS|fr902N~>eZNT^UyfV0YYsmzX4g`*nk{pDc>QA- zF#$($XXorR0yD$&ll|A4OSo?qE8)-ajoMF;FRj|L^hPQxSf(SMJ1jg|69m@jleHP1 z@zA^XgPM;7@>!Y&69D^VQmKuxzUcb)^^JiWEIqY-%Sx+QYS{>4)uOrU}Zooz-_sSv)Rgsf%Ed{dN|8&HAQPqfL$V%d?7q?pSO4>;du7 zm;%~=j0`_Ejs+K;_lVF$veGsY_MD#HBtK40c1Dszatb-+7u0fS8d3eE#Me}8bGP&*TM9(+i;z@Q zE$EPW#B~aIU?n|OPC;v|i#LfFNMxM}v_eg*HNuzz>Ml-zlD_6H_=xYS%$NNpLv)o?Cc#ZkKk-F~Gg7sQw$?a5$A;Y=Y zgwHH(Rk*wW#I(3yKE3pd>utXES9KPDAU>Z@&ilkTLa61bPKjC(t9ak+cmz_!MR{yK zJj@l`;e96mI6ZiwB;*PAU_|nli9AwAjDds4DqAQ4kdfoiGQh3ptKcR{sX8b+lpL8x zuar!euv;dfsm9Iv=`!PZ;qYm{%TzvfxN^B~IBxiqeMN*xQYs8rN{;U_N3|&Xy3)sE zIgD;e8ZaC(pOX!!rPUQXK*m)Km5Lv~r!?63GIBA!w5}Il-Tvw9koft-G8CnI@V0d> zQHHm6v$^+I+fgQKoU|p%uMMkocjl(P-sN>US49k6|IjVNK=3}bO!#Pl^7&%D9rIwz zq&zmEVa6tUnhNPA!L8y&4Q`t-ztSU9lZOt&#^>t!yj1HQRPDbH?o# zPm$`yxTZi$OJXpxxE}r<6mC4R4Ota4t};P_!`;|*8lPmwRHrM%^U^}X*$Z2{WNn*? zkL8yYm{jHdr#wOW6P<3?;T`#hYA-_$U)~H<(km@&dR|?v`6+rBdE3l(s<>1Wn~MpZ z%PQ?Qj2U@8P+2-PYx>;`$C;*!Ikxfh^YoYRFAp87owS5%p8yx{Fk2+81^&@#L zJS)=`Tpnu9yQax8%`vFRobk5sQ*znIH&M3PNe=JMGK+=!kC}!l=6zJ_WS$JpX>|=( zcbY4+m_=3|5$%t;G<-A%ZPdQ@2UrGPHc6hOa+9}_vpUd18AaO4#*}nRn&8S?A$_=| z?Qc`GVC_O$`ilMt3XRa#r1|yXVO47j)>`^xHc;t+O`}{)-cai))8=7|h-l&xwaG>J zyNr|JIk|g{ydM0WRr*JM<)~DqEp)dqd&4CuKTJg#Wx0&UryS5;ti1c$#{Rd?o3^al zbzZ#C(UJ5dH=m9gvnRhuc)RwR5-%?Mvjken&_Pv<@U<1jtc zUKEmE-A6>2`j-ej?;gPiA*Q3!0X_Fr6q0kb*O;FRjPEglj~R$ngXMm?pmJTzPkF{L zSM~_6Wgy3F6^ghN(FHR-b>72scwe16I+m_-5K&Q-M}MeB03Jjhcm|$(0G6SaVSu`d z$e*XE8jItWjY_38G<|UKO6iH96f^@*RP4Hx1^tkPB2xQ$6ea@==#w|GLlP4ut`17U zq82gzASO8M@nLXbh*h3G7~CL+4PKraZ~KtJ{(CT&6y>nhy7(lxbEnWA1Vc-*+|xA^tGzi-M!1nkhoncPW8Y%Ih0*``x?Vcbv!iHtpz4N&ux z_W|^i?1u}P0XYmhYlclVw1x53kdAEgdk9k`9=Ky9n_#RZa#P>Zt!Thu&5#ws~ZNp`@n%{k>_Wpxwj zw9DV$3`_5w>hn+Yx5n^AbnSv%DiO(a$@v0jZV5m~!(Reai=Zq{iiW!-lI7m1?z%x` z&!K57snn$q0(mLsbLB@s>F#*&coDJ-ec{?l@p};@k?50D0&Itej(CY6s}-bjDIN6# zZo7LdPnz{>_dsj3n?U<1KBazn3V{3m<$+haPX8J)udTd|CHN>)v@D7-T#U@XI6V+l z@bJb{y?OMwX0-yVe+a4*>FIJwfNqbaG>8-yCf|N+WyxuVGLox9*Uqj&p;=Fdhzxv8 ze1kc++cI|7iHciL&CRFkJiBTV7ksc5tEv`O?~&Zx!wvywFxLTL%Pt^f`6vU9ioIHU zlD!(gp1tb#g}51;LzG#?KBM%A_x#{Ku-Q17H{_vcMAfAbpWDSG@MR*8DvU4nlpoo6zs@+2m0mW6|sWT#I7k zlwgNH>`TUCez?Z{X7oyXvj@duhvV`{$m=}*P!PivNE_B&7;>1z;+&Q_ErJk#;?nHq z-03)ayN|9sSr@adMgp1O+3e@HV63?*zWL)`Hzsr){jvH!R;YtJdvkM&jmk0zFEy2j zvE8%aql$@9vuH^$gu~?*P9~_A)+^*HTMdAD98_vIsJ8XC=JUh3%{Jd>ua^e~UViS5 z%p0b)jfAr^FX;4_>1Nma+}y~x4qbC*RSPM!@f^6R(m(hy&wvJt(t9R61MZUK6Zn|+cd(3$gP^Sa?t%cF?d z!~XdTtLc}Ul`>}YcdGUB>o=p2LBbi~+XLyKz0pkCCN^JXoHQKN<%!aESHK>;JK1=lqb`B&rO_vIBB!{?#t{xU!Oy5{;Rz%jaKkYw+1WW00|O8PNiY8+$o%=1{qKz^u@)bY z<6*?f&1uNR#%*Z8X#k|BFq@c~vKkt5G8=Pqa~lc}|Noo(4-w$xY~bkpzn^3PP MI0_0e`LA&Q2lCj++yDRo literal 0 HcmV?d00001 diff --git a/index.html b/index.html index 87691b1..c5bd432 100644 --- a/index.html +++ b/index.html @@ -8,6 +8,6 @@ - + diff --git a/src/components/pdf-viewer/canvas/pdf-canvas.js b/src/components/pdf-viewer/canvas/pdf-canvas.js index bb3b183..926b403 100644 --- a/src/components/pdf-viewer/canvas/pdf-canvas.js +++ b/src/components/pdf-viewer/canvas/pdf-canvas.js @@ -1,62 +1,142 @@ -import { LitElement, html } from 'lit' -import { ContextConsumer } from '@lit/context' +import { html } from 'lit' +import { PDFViewerComponent } from '../pdf-viewer-component.js' import styles from './pdf-canvas.styles.js' -import { pdfContext } from '../pdf-context.js' -export default class PDFCanvas extends LitElement { +export default class PDFCanvas extends PDFViewerComponent { static get styles() { return styles } + static get properties() { + return { + renderedPages: { type: Number, state: true } + } + } + constructor() { super() - this._contextConsumer = new ContextConsumer(this, { - context: pdfContext, - callback: (value) => { - this.context = value - this.requestUpdate() - }, - subscribe: true - }) - this.context = null + this.renderedPages = 0 + this.isScrolling = false + this.scrollTimeout = null } async updated(changedProperties) { - await this.renderPage() + if (this.context?.pdfDoc) { + const needsRerender = changedProperties.has('context') && ( + !changedProperties.get('context')?.pdfDoc || + changedProperties.get('context')?.scale !== this.context.scale + ) + + if (needsRerender || this.renderedPages === 0) { + await this.renderAllPages() + } + } + + if (changedProperties.has('context') && this.context?.currentPage) { + const oldContext = changedProperties.get('context') + if (oldContext?.currentPage !== this.context.currentPage && !this.isScrolling) { + setTimeout(() => this.scrollToPage(this.context.currentPage), 100) + } + } } - async renderPage() { + async renderAllPages() { if (!this.context?.pdfDoc) return - const { pdfDoc, currentPage, scale } = this.context + const { pdfDoc, totalPages, scale } = this.context + const container = this.shadowRoot.querySelector('.canvas-container') + if (!container) return + + container.innerHTML = '' + + for (let pageNum = 1; pageNum <= totalPages; pageNum++) { + try { + const page = await pdfDoc.getPage(pageNum) + const viewport = page.getViewport({ scale }) + + const pageWrapper = document.createElement('div') + pageWrapper.className = 'page-wrapper' - try { - const page = await pdfDoc.getPage(currentPage) - const canvas = this.shadowRoot.querySelector('#pdf-canvas') - if (!canvas) return + const canvas = document.createElement('canvas') + const canvasContext = canvas.getContext('2d') - const canvasContext = canvas.getContext('2d') - const viewport = page.getViewport({ scale }) + canvas.height = viewport.height + canvas.width = viewport.width + canvas.dataset.pageNumber = pageNum - canvas.height = viewport.height - canvas.width = viewport.width + await page.render({ + canvasContext, + viewport + }).promise - const renderContext = { - canvasContext, - viewport + pageWrapper.appendChild(canvas) + container.appendChild(pageWrapper) + } catch (error) { + console.error(`Error rendering page ${pageNum}:`, error) } + } + + this.renderedPages = totalPages + } + + scrollToPage(pageNum) { + const container = this.shadowRoot.querySelector('.canvas-container') + const canvas = this.shadowRoot.querySelector(`canvas[data-page-number="${pageNum}"]`) + + if (container && canvas) { + const containerTop = container.getBoundingClientRect().top + const canvasTop = canvas.getBoundingClientRect().top + const offset = canvasTop - containerTop + container.scrollTop - 32 + + container.scrollTo({ + top: offset, + behavior: 'smooth' + }) + } + } + + handleScroll() { + if (!this.context?.setCurrentPage) return + + this.isScrolling = true + clearTimeout(this.scrollTimeout) + + this.scrollTimeout = setTimeout(() => { + const container = this.shadowRoot.querySelector('.canvas-container') + if (!container) return + + const containerRect = container.getBoundingClientRect() + const centerY = containerRect.top + containerRect.height / 2 + + const canvases = this.shadowRoot.querySelectorAll('canvas') + let currentPage = 1 + + for (const canvas of canvases) { + const rect = canvas.getBoundingClientRect() + if (rect.top <= centerY && rect.bottom >= centerY) { + currentPage = parseInt(canvas.dataset.pageNumber, 10) + break + } + } + + if (currentPage !== this.context.currentPage) { + this.context.setCurrentPage(currentPage) + } + + this.isScrolling = false + }, 150) + } - await page.render(renderContext).promise - } catch (error) { - console.error('Error rendering page:', error) + firstUpdated() { + const container = this.shadowRoot.querySelector('.canvas-container') + if (container) { + container.addEventListener('scroll', () => this.handleScroll()) } } render() { return html` -
- -
+
` } } diff --git a/src/components/pdf-viewer/canvas/pdf-canvas.styles.js b/src/components/pdf-viewer/canvas/pdf-canvas.styles.js index 7d74b78..dee5584 100644 --- a/src/components/pdf-viewer/canvas/pdf-canvas.styles.js +++ b/src/components/pdf-viewer/canvas/pdf-canvas.styles.js @@ -8,17 +8,25 @@ export default css` } .canvas-container { - flex: 1; + height: 100%; overflow: auto; display: flex; - justify-content: center; - align-items: flex-start; + flex-direction: column; + align-items: center; padding: 2rem; background: #e9e9e9; + gap: 1rem; + } + + .page-wrapper { + display: flex; + justify-content: center; + margin-bottom: 1rem; } canvas { box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); background: white; + display: block; } ` diff --git a/src/components/pdf-viewer/pdf-viewer-component.js b/src/components/pdf-viewer/pdf-viewer-component.js new file mode 100644 index 0000000..89d5cb0 --- /dev/null +++ b/src/components/pdf-viewer/pdf-viewer-component.js @@ -0,0 +1,22 @@ +import { LitElement } from 'lit' +import { ContextConsumer } from '@lit/context' +import { pdfContext } from './pdf-context.js' + +export class PDFViewerComponent extends LitElement { + static get styles() { + return [] + } + + constructor() { + super() + this._contextConsumer = new ContextConsumer(this, { + context: pdfContext, + callback: (value) => { + this.context = value + this.requestUpdate() + }, + subscribe: true + }) + this.context = null + } +} diff --git a/src/components/pdf-viewer/sidebar/pdf-sidebar.js b/src/components/pdf-viewer/sidebar/pdf-sidebar.js index 284df70..84c16ad 100644 --- a/src/components/pdf-viewer/sidebar/pdf-sidebar.js +++ b/src/components/pdf-viewer/sidebar/pdf-sidebar.js @@ -1,42 +1,26 @@ -import { LitElement, html } from 'lit' -import { ContextConsumer } from '@lit/context' +import { html } from 'lit' +import { PDFViewerComponent } from '../pdf-viewer-component.js' import styles from './pdf-sidebar.styles.js' -import { pdfContext } from '../pdf-context.js' import '../thumbnail/pdf-thumbnail.js' -export default class PDFSidebar extends LitElement { +export default class PDFSidebar extends PDFViewerComponent { static get styles() { return styles } - constructor() { - super() - this._contextConsumer = new ContextConsumer(this, { - context: pdfContext, - callback: (value) => { - this.context = value - this.requestUpdate() - }, - subscribe: true - }) - this.context = null - } - renderThumbnails() { if (!this.context?.pdfDoc) return html`` - const { pdfDoc, totalPages, currentPage } = this.context + const { totalPages } = this.context - if (!pdfDoc || totalPages === 0) return html`` + if (totalPages === 0) return html`` const thumbnails = [] for (let i = 1; i <= totalPages; i++) { thumbnails.push(html` `) } diff --git a/src/components/pdf-viewer/thumbnail/pdf-thumbnail.js b/src/components/pdf-viewer/thumbnail/pdf-thumbnail.js index c1e9ff2..2a2abb5 100644 --- a/src/components/pdf-viewer/thumbnail/pdf-thumbnail.js +++ b/src/components/pdf-viewer/thumbnail/pdf-thumbnail.js @@ -1,16 +1,11 @@ -import { LitElement, html } from 'lit' -import { consume } from '@lit/context' +import { html } from 'lit' +import { PDFViewerComponent } from '../pdf-viewer-component.js' import styles from './pdf-thumbnail.styles.js' -import { pdfContext } from '../pdf-context.js' -export default class PDFThumbnail extends LitElement { +export default class PDFThumbnail extends PDFViewerComponent { static get properties() { return { - pageNumber: { type: Number }, - pdfDoc: { type: Object }, - scale: { type: Number }, - isActive: { type: Boolean }, - _context: { type: Object, state: true } + pageNumber: { type: Number } } } @@ -21,13 +16,7 @@ export default class PDFThumbnail extends LitElement { constructor() { super() this.pageNumber = 1 - this.pdfDoc = null this.scale = 0.3 - this.isActive = false - this._context = null - consume(this, pdfContext, (value) => { - this._context = value - }) } async firstUpdated() { @@ -35,16 +24,21 @@ export default class PDFThumbnail extends LitElement { } async updated(changedProperties) { - if (changedProperties.has('pdfDoc') || changedProperties.has('pageNumber')) { + if (changedProperties.has('pageNumber')) { + await this.renderThumbnail() + } + if (this.context?.pdfDoc) { await this.renderThumbnail() } } async renderThumbnail() { - if (!this.pdfDoc) return + if (!this.context?.pdfDoc) return + + const pdfDoc = this.context.pdfDoc try { - const page = await this.pdfDoc.getPage(this.pageNumber) + const page = await pdfDoc.getPage(this.pageNumber) const canvas = this.shadowRoot.querySelector('#thumbnail-canvas') if (!canvas) return @@ -67,13 +61,15 @@ export default class PDFThumbnail extends LitElement { handleClick() { console.log('Thumbnail clicked:', this.pageNumber) - this._context?.setCurrentPage(this.pageNumber) + this.context?.setCurrentPage(this.pageNumber) } render() { + const isActive = this.context?.currentPage === this.pageNumber + return html`
diff --git a/src/components/pdf-viewer/toolbar/pdf-toolbar.js b/src/components/pdf-viewer/toolbar/pdf-toolbar.js index bd82fd8..bff154d 100644 --- a/src/components/pdf-viewer/toolbar/pdf-toolbar.js +++ b/src/components/pdf-viewer/toolbar/pdf-toolbar.js @@ -1,26 +1,12 @@ -import { LitElement, html } from 'lit' -import { ContextConsumer } from '@lit/context' +import { html } from 'lit' +import { PDFViewerComponent } from '../pdf-viewer-component.js' import styles from './pdf-toolbar.styles.js' -import { pdfContext } from '../pdf-context.js' -export default class PDFToolbar extends LitElement { +export default class PDFToolbar extends PDFViewerComponent { static get styles() { return styles } - constructor() { - super() - this._contextConsumer = new ContextConsumer(this, { - context: pdfContext, - callback: (value) => { - this.context = value - this.requestUpdate() - }, - subscribe: true - }) - this.context = null - } - previousPage() { this.context?.previousPage() } From db9b4c0c8cabb11dddf8be3729c56730afd8e5d6 Mon Sep 17 00:00:00 2001 From: Josh McLeod Date: Wed, 4 Feb 2026 13:24:46 -0500 Subject: [PATCH 08/31] Text highlighting --- src/components/pdf-viewer/README.md | 13 +++++ .../pdf-viewer/canvas/pdf-canvas.js | 47 +++++++++++++++++++ .../pdf-viewer/canvas/pdf-canvas.styles.js | 25 ++++++++++ 3 files changed, 85 insertions(+) diff --git a/src/components/pdf-viewer/README.md b/src/components/pdf-viewer/README.md index a275930..454ad7d 100644 --- a/src/components/pdf-viewer/README.md +++ b/src/components/pdf-viewer/README.md @@ -5,6 +5,7 @@ A composable PDF viewer web component built with Lit and PDF.js. ## Features - 📄 Full PDF rendering with PDF.js +- 📝 Text selection and copying - 🖼️ Thumbnail navigation sidebar - 🔍 Zoom controls (in/out) - ⏮️ Page navigation (previous/next) @@ -137,6 +138,18 @@ The component uses Shadow DOM with CSS custom properties for styling. Each sub-c Child components access shared state through a Symbol-based context pattern. See `COMPONENT-ARCHITECTURE.md` for technical details. +## Text Selection + +The PDF viewer implements a text layer overlay system similar to the official PDF.js viewer, allowing users to select and copy text directly from the PDF: + +- Text is rendered in an invisible layer positioned precisely over the PDF canvas +- Selection highlighting appears when text is selected +- Users can copy selected text to clipboard using standard browser shortcuts (Cmd/Ctrl+C) +- Text layer automatically scales with zoom level +- No additional configuration required - works out of the box + +The text layer is implemented in the `pdf-canvas` component using PDF.js's `getTextContent()` API to extract and position text elements. + ## License See the main project LICENSE file. diff --git a/src/components/pdf-viewer/canvas/pdf-canvas.js b/src/components/pdf-viewer/canvas/pdf-canvas.js index 926b403..872dcd1 100644 --- a/src/components/pdf-viewer/canvas/pdf-canvas.js +++ b/src/components/pdf-viewer/canvas/pdf-canvas.js @@ -1,4 +1,5 @@ import { html } from 'lit' +import * as pdfjsLib from 'pdfjs-dist' import { PDFViewerComponent } from '../pdf-viewer-component.js' import styles from './pdf-canvas.styles.js' @@ -56,6 +57,9 @@ export default class PDFCanvas extends PDFViewerComponent { const pageWrapper = document.createElement('div') pageWrapper.className = 'page-wrapper' + pageWrapper.style.position = 'relative' + pageWrapper.style.width = `${viewport.width}px` + pageWrapper.style.height = `${viewport.height}px` const canvas = document.createElement('canvas') const canvasContext = canvas.getContext('2d') @@ -69,7 +73,16 @@ export default class PDFCanvas extends PDFViewerComponent { viewport }).promise + const textLayerDiv = document.createElement('div') + textLayerDiv.className = 'textLayer' + textLayerDiv.style.width = `${viewport.width}px` + textLayerDiv.style.height = `${viewport.height}px` + pageWrapper.appendChild(canvas) + pageWrapper.appendChild(textLayerDiv) + + await this.renderTextLayer(page, textLayerDiv, viewport) + container.appendChild(pageWrapper) } catch (error) { console.error(`Error rendering page ${pageNum}:`, error) @@ -79,6 +92,40 @@ export default class PDFCanvas extends PDFViewerComponent { this.renderedPages = totalPages } + async renderTextLayer(page, textLayerDiv, viewport) { + try { + const textContent = await page.getTextContent() + + textContent.items.forEach((textItem) => { + const tx = pdfjsLib.Util.transform( + viewport.transform, + textItem.transform + ) + + const fontSize = Math.sqrt((tx[2] * tx[2]) + (tx[3] * tx[3])) + const fontHeight = fontSize + + const textDiv = document.createElement('div') + textDiv.style.position = 'absolute' + textDiv.style.left = `${tx[4]}px` + textDiv.style.top = `${tx[5] - fontHeight}px` + textDiv.style.fontSize = `${fontSize}px` + textDiv.style.fontFamily = textItem.fontName || 'sans-serif' + + if (textItem.str.length > 0) { + const width = textItem.width * viewport.scale + textDiv.style.width = `${width}px` + } + + textDiv.textContent = textItem.str + + textLayerDiv.appendChild(textDiv) + }) + } catch (error) { + console.error('Error rendering text layer:', error) + } + } + scrollToPage(pageNum) { const container = this.shadowRoot.querySelector('.canvas-container') const canvas = this.shadowRoot.querySelector(`canvas[data-page-number="${pageNum}"]`) diff --git a/src/components/pdf-viewer/canvas/pdf-canvas.styles.js b/src/components/pdf-viewer/canvas/pdf-canvas.styles.js index dee5584..0d96a3d 100644 --- a/src/components/pdf-viewer/canvas/pdf-canvas.styles.js +++ b/src/components/pdf-viewer/canvas/pdf-canvas.styles.js @@ -22,6 +22,7 @@ export default css` display: flex; justify-content: center; margin-bottom: 1rem; + position: relative; } canvas { @@ -29,4 +30,28 @@ export default css` background: white; display: block; } + + .textLayer { + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; + overflow: hidden; + opacity: 0.2; + line-height: 1.0; + pointer-events: auto; + } + + .textLayer > div { + color: transparent; + position: absolute; + white-space: pre; + cursor: text; + transform-origin: 0% 0%; + } + + .textLayer ::selection { + background: rgba(0, 100, 255, 0.8); + } ` From dfe3f9d1845ff3c51730032b66db5e1e433154b4 Mon Sep 17 00:00:00 2001 From: Josh McLeod Date: Wed, 4 Feb 2026 13:28:33 -0500 Subject: [PATCH 09/31] Account for device pixel ratio --- src/components/pdf-viewer/canvas/pdf-canvas.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/components/pdf-viewer/canvas/pdf-canvas.js b/src/components/pdf-viewer/canvas/pdf-canvas.js index 872dcd1..bf5b130 100644 --- a/src/components/pdf-viewer/canvas/pdf-canvas.js +++ b/src/components/pdf-viewer/canvas/pdf-canvas.js @@ -50,6 +50,8 @@ export default class PDFCanvas extends PDFViewerComponent { container.innerHTML = '' + const devicePixelRatio = window.devicePixelRatio || 1 + for (let pageNum = 1; pageNum <= totalPages; pageNum++) { try { const page = await pdfDoc.getPage(pageNum) @@ -64,13 +66,17 @@ export default class PDFCanvas extends PDFViewerComponent { const canvas = document.createElement('canvas') const canvasContext = canvas.getContext('2d') - canvas.height = viewport.height - canvas.width = viewport.width + canvas.height = viewport.height * devicePixelRatio + canvas.width = viewport.width * devicePixelRatio + canvas.style.width = `${viewport.width}px` + canvas.style.height = `${viewport.height}px` canvas.dataset.pageNumber = pageNum + const scaledViewport = page.getViewport({ scale: scale * devicePixelRatio }) + await page.render({ canvasContext, - viewport + viewport: scaledViewport }).promise const textLayerDiv = document.createElement('div') From f3784f546d7871ccd0d569d41b5bf1e1b8c4d9aa Mon Sep 17 00:00:00 2001 From: Josh McLeod Date: Wed, 4 Feb 2026 13:33:36 -0500 Subject: [PATCH 10/31] Extract pdf page component --- .../pdf-viewer/canvas/pdf-canvas.js | 123 +++++------------- .../pdf-viewer/canvas/pdf-canvas.styles.js | 37 ------ src/components/pdf-viewer/page/pdf-page.js | 110 ++++++++++++++++ .../pdf-viewer/page/pdf-page.styles.js | 36 +++++ 4 files changed, 177 insertions(+), 129 deletions(-) create mode 100644 src/components/pdf-viewer/page/pdf-page.js create mode 100644 src/components/pdf-viewer/page/pdf-page.styles.js diff --git a/src/components/pdf-viewer/canvas/pdf-canvas.js b/src/components/pdf-viewer/canvas/pdf-canvas.js index bf5b130..9d42717 100644 --- a/src/components/pdf-viewer/canvas/pdf-canvas.js +++ b/src/components/pdf-viewer/canvas/pdf-canvas.js @@ -1,7 +1,7 @@ import { html } from 'lit' -import * as pdfjsLib from 'pdfjs-dist' import { PDFViewerComponent } from '../pdf-viewer-component.js' import styles from './pdf-canvas.styles.js' +import '../page/pdf-page.js' export default class PDFCanvas extends PDFViewerComponent { static get styles() { @@ -10,13 +10,13 @@ export default class PDFCanvas extends PDFViewerComponent { static get properties() { return { - renderedPages: { type: Number, state: true } + pages: { type: Array, state: true } } } constructor() { super() - this.renderedPages = 0 + this.pages = [] this.isScrolling = false this.scrollTimeout = null } @@ -28,8 +28,8 @@ export default class PDFCanvas extends PDFViewerComponent { changedProperties.get('context')?.scale !== this.context.scale ) - if (needsRerender || this.renderedPages === 0) { - await this.renderAllPages() + if (needsRerender || this.pages.length === 0) { + await this.loadPages() } } @@ -41,105 +41,36 @@ export default class PDFCanvas extends PDFViewerComponent { } } - async renderAllPages() { + async loadPages() { if (!this.context?.pdfDoc) return - const { pdfDoc, totalPages, scale } = this.context - const container = this.shadowRoot.querySelector('.canvas-container') - if (!container) return - - container.innerHTML = '' - - const devicePixelRatio = window.devicePixelRatio || 1 + const { pdfDoc, totalPages } = this.context + const pages = [] for (let pageNum = 1; pageNum <= totalPages; pageNum++) { try { const page = await pdfDoc.getPage(pageNum) - const viewport = page.getViewport({ scale }) - - const pageWrapper = document.createElement('div') - pageWrapper.className = 'page-wrapper' - pageWrapper.style.position = 'relative' - pageWrapper.style.width = `${viewport.width}px` - pageWrapper.style.height = `${viewport.height}px` - - const canvas = document.createElement('canvas') - const canvasContext = canvas.getContext('2d') - - canvas.height = viewport.height * devicePixelRatio - canvas.width = viewport.width * devicePixelRatio - canvas.style.width = `${viewport.width}px` - canvas.style.height = `${viewport.height}px` - canvas.dataset.pageNumber = pageNum - - const scaledViewport = page.getViewport({ scale: scale * devicePixelRatio }) - - await page.render({ - canvasContext, - viewport: scaledViewport - }).promise - - const textLayerDiv = document.createElement('div') - textLayerDiv.className = 'textLayer' - textLayerDiv.style.width = `${viewport.width}px` - textLayerDiv.style.height = `${viewport.height}px` - - pageWrapper.appendChild(canvas) - pageWrapper.appendChild(textLayerDiv) - - await this.renderTextLayer(page, textLayerDiv, viewport) - - container.appendChild(pageWrapper) + pages.push({ page, pageNumber: pageNum }) } catch (error) { - console.error(`Error rendering page ${pageNum}:`, error) + console.error(`Error loading page ${pageNum}:`, error) } } - this.renderedPages = totalPages + this.pages = pages } - async renderTextLayer(page, textLayerDiv, viewport) { - try { - const textContent = await page.getTextContent() - - textContent.items.forEach((textItem) => { - const tx = pdfjsLib.Util.transform( - viewport.transform, - textItem.transform - ) - - const fontSize = Math.sqrt((tx[2] * tx[2]) + (tx[3] * tx[3])) - const fontHeight = fontSize - - const textDiv = document.createElement('div') - textDiv.style.position = 'absolute' - textDiv.style.left = `${tx[4]}px` - textDiv.style.top = `${tx[5] - fontHeight}px` - textDiv.style.fontSize = `${fontSize}px` - textDiv.style.fontFamily = textItem.fontName || 'sans-serif' - - if (textItem.str.length > 0) { - const width = textItem.width * viewport.scale - textDiv.style.width = `${width}px` - } - - textDiv.textContent = textItem.str - - textLayerDiv.appendChild(textDiv) - }) - } catch (error) { - console.error('Error rendering text layer:', error) - } - } scrollToPage(pageNum) { const container = this.shadowRoot.querySelector('.canvas-container') - const canvas = this.shadowRoot.querySelector(`canvas[data-page-number="${pageNum}"]`) + const pageElements = this.shadowRoot.querySelectorAll('pdf-page') + const targetPage = Array.from(pageElements).find( + el => el.pageNumber === pageNum + ) - if (container && canvas) { + if (container && targetPage) { const containerTop = container.getBoundingClientRect().top - const canvasTop = canvas.getBoundingClientRect().top - const offset = canvasTop - containerTop + container.scrollTop - 32 + const pageTop = targetPage.getBoundingClientRect().top + const offset = pageTop - containerTop + container.scrollTop - 32 container.scrollTo({ top: offset, @@ -161,13 +92,13 @@ export default class PDFCanvas extends PDFViewerComponent { const containerRect = container.getBoundingClientRect() const centerY = containerRect.top + containerRect.height / 2 - const canvases = this.shadowRoot.querySelectorAll('canvas') + const pageElements = this.shadowRoot.querySelectorAll('pdf-page') let currentPage = 1 - for (const canvas of canvases) { - const rect = canvas.getBoundingClientRect() + for (const pageElement of pageElements) { + const rect = pageElement.getBoundingClientRect() if (rect.top <= centerY && rect.bottom >= centerY) { - currentPage = parseInt(canvas.dataset.pageNumber, 10) + currentPage = pageElement.pageNumber break } } @@ -189,7 +120,15 @@ export default class PDFCanvas extends PDFViewerComponent { render() { return html` -
+
+ ${this.pages.map(({ page, pageNumber }) => html` + + + `)} +
` } } diff --git a/src/components/pdf-viewer/canvas/pdf-canvas.styles.js b/src/components/pdf-viewer/canvas/pdf-canvas.styles.js index 0d96a3d..0179288 100644 --- a/src/components/pdf-viewer/canvas/pdf-canvas.styles.js +++ b/src/components/pdf-viewer/canvas/pdf-canvas.styles.js @@ -17,41 +17,4 @@ export default css` background: #e9e9e9; gap: 1rem; } - - .page-wrapper { - display: flex; - justify-content: center; - margin-bottom: 1rem; - position: relative; - } - - canvas { - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); - background: white; - display: block; - } - - .textLayer { - position: absolute; - left: 0; - top: 0; - right: 0; - bottom: 0; - overflow: hidden; - opacity: 0.2; - line-height: 1.0; - pointer-events: auto; - } - - .textLayer > div { - color: transparent; - position: absolute; - white-space: pre; - cursor: text; - transform-origin: 0% 0%; - } - - .textLayer ::selection { - background: rgba(0, 100, 255, 0.8); - } ` diff --git a/src/components/pdf-viewer/page/pdf-page.js b/src/components/pdf-viewer/page/pdf-page.js new file mode 100644 index 0000000..24e08f9 --- /dev/null +++ b/src/components/pdf-viewer/page/pdf-page.js @@ -0,0 +1,110 @@ +import { html, LitElement } from 'lit' +import * as pdfjsLib from 'pdfjs-dist' +import styles from './pdf-page.styles.js' + +export default class PDFPage extends LitElement { + static get styles() { + return styles + } + + static get properties() { + return { + page: { type: Object }, + scale: { type: Number }, + pageNumber: { type: Number } + } + } + + constructor() { + super() + this.page = null + this.scale = 1 + this.pageNumber = 1 + } + + async updated(changedProperties) { + if (this.page && (changedProperties.has('page') || changedProperties.has('scale'))) { + await this.renderPage() + } + } + + async renderPage() { + if (!this.page) return + + const viewport = this.page.getViewport({ scale: this.scale }) + const devicePixelRatio = window.devicePixelRatio || 1 + + const pageWrapper = this.shadowRoot.querySelector('.page-wrapper') + if (!pageWrapper) return + + pageWrapper.style.width = `${viewport.width}px` + pageWrapper.style.height = `${viewport.height}px` + + const canvas = this.shadowRoot.querySelector('canvas') + const canvasContext = canvas.getContext('2d') + + canvas.height = viewport.height * devicePixelRatio + canvas.width = viewport.width * devicePixelRatio + canvas.style.width = `${viewport.width}px` + canvas.style.height = `${viewport.height}px` + + const scaledViewport = this.page.getViewport({ scale: this.scale * devicePixelRatio }) + + await this.page.render({ + canvasContext, + viewport: scaledViewport + }).promise + + const textLayerDiv = this.shadowRoot.querySelector('.textLayer') + textLayerDiv.style.width = `${viewport.width}px` + textLayerDiv.style.height = `${viewport.height}px` + textLayerDiv.innerHTML = '' + + await this.renderTextLayer(viewport, textLayerDiv) + } + + async renderTextLayer(viewport, textLayerDiv) { + try { + const textContent = await this.page.getTextContent() + + textContent.items.forEach((textItem) => { + const tx = pdfjsLib.Util.transform( + viewport.transform, + textItem.transform + ) + + const fontSize = Math.sqrt((tx[2] * tx[2]) + (tx[3] * tx[3])) + const fontHeight = fontSize + + const textDiv = document.createElement('div') + textDiv.style.position = 'absolute' + textDiv.style.left = `${tx[4]}px` + textDiv.style.top = `${tx[5] - fontHeight}px` + textDiv.style.fontSize = `${fontSize}px` + textDiv.style.fontFamily = textItem.fontName || 'sans-serif' + + if (textItem.str.length > 0) { + const width = textItem.width * viewport.scale + textDiv.style.width = `${width}px` + } + + textDiv.textContent = textItem.str + + textLayerDiv.appendChild(textDiv) + }) + } catch (error) { + console.error('Error rendering text layer:', error) + } + } + + render() { + return html` +
+ +
+
+ ` + } +} + +customElements.define('pdf-page', PDFPage) diff --git a/src/components/pdf-viewer/page/pdf-page.styles.js b/src/components/pdf-viewer/page/pdf-page.styles.js new file mode 100644 index 0000000..cd1788c --- /dev/null +++ b/src/components/pdf-viewer/page/pdf-page.styles.js @@ -0,0 +1,36 @@ +import { css } from 'lit' + +export default css` + :host { + display: block; + } + + .page-wrapper { + position: relative; + margin-bottom: 16px; + } + + canvas { + display: block; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + } + + .textLayer { + position: absolute; + left: 0; + top: 0; + overflow: hidden; + opacity: 0.2; + line-height: 1; + text-align: initial; + pointer-events: none; + } + + .textLayer > div { + color: transparent; + position: absolute; + white-space: pre; + cursor: text; + transform-origin: 0% 0%; + } +` From 186ef6ff7731d4254b067226d4c3b1da97024476 Mon Sep 17 00:00:00 2001 From: Jullian Calkins Date: Wed, 4 Feb 2026 13:38:59 -0500 Subject: [PATCH 11/31] Remove updated() from pdf-thumbnail --- src/components/pdf-viewer/thumbnail/pdf-thumbnail.js | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/components/pdf-viewer/thumbnail/pdf-thumbnail.js b/src/components/pdf-viewer/thumbnail/pdf-thumbnail.js index 2a2abb5..a0c0957 100644 --- a/src/components/pdf-viewer/thumbnail/pdf-thumbnail.js +++ b/src/components/pdf-viewer/thumbnail/pdf-thumbnail.js @@ -23,15 +23,6 @@ export default class PDFThumbnail extends PDFViewerComponent { await this.renderThumbnail() } - async updated(changedProperties) { - if (changedProperties.has('pageNumber')) { - await this.renderThumbnail() - } - if (this.context?.pdfDoc) { - await this.renderThumbnail() - } - } - async renderThumbnail() { if (!this.context?.pdfDoc) return @@ -68,7 +59,7 @@ export default class PDFThumbnail extends PDFViewerComponent { const isActive = this.context?.currentPage === this.pageNumber return html` -
From 0a5aa18704f870d1531c8a8a417a1a4eaaeca0d9 Mon Sep 17 00:00:00 2001 From: Jullian Calkins Date: Wed, 4 Feb 2026 14:02:25 -0500 Subject: [PATCH 12/31] Prevent simultaineous rendering --- src/components/pdf-viewer/page/pdf-page.js | 22 +++++++++++++++++-- .../pdf-viewer/thumbnail/pdf-thumbnail.js | 20 +++++++++++++++-- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/src/components/pdf-viewer/page/pdf-page.js b/src/components/pdf-viewer/page/pdf-page.js index 24e08f9..b99c5f1 100644 --- a/src/components/pdf-viewer/page/pdf-page.js +++ b/src/components/pdf-viewer/page/pdf-page.js @@ -20,6 +20,7 @@ export default class PDFPage extends LitElement { this.page = null this.scale = 1 this.pageNumber = 1 + this._renderTask = null } async updated(changedProperties) { @@ -31,6 +32,8 @@ export default class PDFPage extends LitElement { async renderPage() { if (!this.page) return + this.#cancelRenderTask() + const viewport = this.page.getViewport({ scale: this.scale }) const devicePixelRatio = window.devicePixelRatio || 1 @@ -50,10 +53,13 @@ export default class PDFPage extends LitElement { const scaledViewport = this.page.getViewport({ scale: this.scale * devicePixelRatio }) - await this.page.render({ + this._renderTask = this.page.render({ canvasContext, viewport: scaledViewport - }).promise + }) + + await this._renderTask.promise + this._renderTask = null const textLayerDiv = this.shadowRoot.querySelector('.textLayer') textLayerDiv.style.width = `${viewport.width}px` @@ -97,6 +103,11 @@ export default class PDFPage extends LitElement { } } + disconnectedCallback() { + super.disconnectedCallback() + this.#cancelRenderTask() + } + render() { return html`
@@ -105,6 +116,13 @@ export default class PDFPage extends LitElement {
` } + + #cancelRenderTask() { + if (this._renderTask) { + this._renderTask.cancel() + this._renderTask = null + } + } } customElements.define('pdf-page', PDFPage) diff --git a/src/components/pdf-viewer/thumbnail/pdf-thumbnail.js b/src/components/pdf-viewer/thumbnail/pdf-thumbnail.js index a0c0957..221368a 100644 --- a/src/components/pdf-viewer/thumbnail/pdf-thumbnail.js +++ b/src/components/pdf-viewer/thumbnail/pdf-thumbnail.js @@ -17,6 +17,7 @@ export default class PDFThumbnail extends PDFViewerComponent { super() this.pageNumber = 1 this.scale = 0.3 + this._renderTask = null } async firstUpdated() { @@ -26,6 +27,8 @@ export default class PDFThumbnail extends PDFViewerComponent { async renderThumbnail() { if (!this.context?.pdfDoc) return + this.#cancelRenderTask() + const pdfDoc = this.context.pdfDoc try { @@ -44,14 +47,20 @@ export default class PDFThumbnail extends PDFViewerComponent { viewport: viewport } - await page.render(renderContext).promise + this._renderTask = page.render(renderContext) + await this._renderTask.promise + this._renderTask = null } catch (error) { console.error('Error rendering thumbnail:', error) } } + disconnectedCallback() { + super.disconnectedCallback() + this.#cancelRenderTask() + } + handleClick() { - console.log('Thumbnail clicked:', this.pageNumber) this.context?.setCurrentPage(this.pageNumber) } @@ -68,6 +77,13 @@ export default class PDFThumbnail extends PDFViewerComponent {
` } + + #cancelRenderTask() { + if (this._renderTask) { + this._renderTask.cancel() + this._renderTask = null + } + } } customElements.define('pdf-thumbnail', PDFThumbnail) From dbbc894cb53bdd5a4f1f112fbb2bed881d4efa9e Mon Sep 17 00:00:00 2001 From: Josh McLeod Date: Wed, 4 Feb 2026 14:02:27 -0500 Subject: [PATCH 13/31] Prefix components --- index.html | 4 ++-- .../canvas/{pdf-canvas.js => rm-pdf-canvas.js} | 14 +++++++------- ...-canvas.styles.js => rm-pdf-canvas.styles.js} | 0 src/components/pdf-viewer/index.js | 4 ++-- .../page/{pdf-page.js => rm-pdf-page.js} | 4 ++-- ...{pdf-page.styles.js => rm-pdf-page.styles.js} | 0 .../{pdf-viewer.js => rm-pdf-viewer.js} | 16 ++++++++-------- ...-viewer.styles.js => rm-pdf-viewer.styles.js} | 0 .../{pdf-sidebar.js => rm-pdf-sidebar.js} | 10 +++++----- ...idebar.styles.js => rm-pdf-sidebar.styles.js} | 0 .../{pdf-thumbnail.js => rm-pdf-thumbnail.js} | 4 ++-- ...nail.styles.js => rm-pdf-thumbnail.styles.js} | 0 .../{pdf-toolbar.js => rm-pdf-toolbar.js} | 4 ++-- ...oolbar.styles.js => rm-pdf-toolbar.styles.js} | 0 14 files changed, 30 insertions(+), 30 deletions(-) rename src/components/pdf-viewer/canvas/{pdf-canvas.js => rm-pdf-canvas.js} (91%) rename src/components/pdf-viewer/canvas/{pdf-canvas.styles.js => rm-pdf-canvas.styles.js} (100%) rename src/components/pdf-viewer/page/{pdf-page.js => rm-pdf-page.js} (97%) rename src/components/pdf-viewer/page/{pdf-page.styles.js => rm-pdf-page.styles.js} (100%) rename src/components/pdf-viewer/{pdf-viewer.js => rm-pdf-viewer.js} (88%) rename src/components/pdf-viewer/{pdf-viewer.styles.js => rm-pdf-viewer.styles.js} (100%) rename src/components/pdf-viewer/sidebar/{pdf-sidebar.js => rm-pdf-sidebar.js} (78%) rename src/components/pdf-viewer/sidebar/{pdf-sidebar.styles.js => rm-pdf-sidebar.styles.js} (100%) rename src/components/pdf-viewer/thumbnail/{pdf-thumbnail.js => rm-pdf-thumbnail.js} (94%) rename src/components/pdf-viewer/thumbnail/{pdf-thumbnail.styles.js => rm-pdf-thumbnail.styles.js} (100%) rename src/components/pdf-viewer/toolbar/{pdf-toolbar.js => rm-pdf-toolbar.js} (92%) rename src/components/pdf-viewer/toolbar/{pdf-toolbar.styles.js => rm-pdf-toolbar.styles.js} (100%) diff --git a/index.html b/index.html index c5bd432..ceec02e 100644 --- a/index.html +++ b/index.html @@ -5,9 +5,9 @@ Spider Web Components - + - + diff --git a/src/components/pdf-viewer/canvas/pdf-canvas.js b/src/components/pdf-viewer/canvas/rm-pdf-canvas.js similarity index 91% rename from src/components/pdf-viewer/canvas/pdf-canvas.js rename to src/components/pdf-viewer/canvas/rm-pdf-canvas.js index 9d42717..91010ee 100644 --- a/src/components/pdf-viewer/canvas/pdf-canvas.js +++ b/src/components/pdf-viewer/canvas/rm-pdf-canvas.js @@ -1,7 +1,7 @@ import { html } from 'lit' import { PDFViewerComponent } from '../pdf-viewer-component.js' -import styles from './pdf-canvas.styles.js' -import '../page/pdf-page.js' +import styles from './rm-pdf-canvas.styles.js' +import '../page/rm-pdf-page.js' export default class PDFCanvas extends PDFViewerComponent { static get styles() { @@ -62,7 +62,7 @@ export default class PDFCanvas extends PDFViewerComponent { scrollToPage(pageNum) { const container = this.shadowRoot.querySelector('.canvas-container') - const pageElements = this.shadowRoot.querySelectorAll('pdf-page') + const pageElements = this.shadowRoot.querySelectorAll('rm-pdf-page') const targetPage = Array.from(pageElements).find( el => el.pageNumber === pageNum ) @@ -92,7 +92,7 @@ export default class PDFCanvas extends PDFViewerComponent { const containerRect = container.getBoundingClientRect() const centerY = containerRect.top + containerRect.height / 2 - const pageElements = this.shadowRoot.querySelectorAll('pdf-page') + const pageElements = this.shadowRoot.querySelectorAll('rm-pdf-page') let currentPage = 1 for (const pageElement of pageElements) { @@ -122,16 +122,16 @@ export default class PDFCanvas extends PDFViewerComponent { return html`
${this.pages.map(({ page, pageNumber }) => html` - - + `)}
` } } -customElements.define('pdf-canvas', PDFCanvas) +customElements.define('rm-pdf-canvas', PDFCanvas) diff --git a/src/components/pdf-viewer/canvas/pdf-canvas.styles.js b/src/components/pdf-viewer/canvas/rm-pdf-canvas.styles.js similarity index 100% rename from src/components/pdf-viewer/canvas/pdf-canvas.styles.js rename to src/components/pdf-viewer/canvas/rm-pdf-canvas.styles.js diff --git a/src/components/pdf-viewer/index.js b/src/components/pdf-viewer/index.js index 164dceb..518442d 100644 --- a/src/components/pdf-viewer/index.js +++ b/src/components/pdf-viewer/index.js @@ -1,2 +1,2 @@ -export { default as PDFViewer } from './pdf-viewer.js' -export { PDFContext } from './pdf-viewer.js' +export { default as PDFViewer } from './rm-pdf-viewer.js' +export { PDFContext } from './rm-pdf-viewer.js' diff --git a/src/components/pdf-viewer/page/pdf-page.js b/src/components/pdf-viewer/page/rm-pdf-page.js similarity index 97% rename from src/components/pdf-viewer/page/pdf-page.js rename to src/components/pdf-viewer/page/rm-pdf-page.js index b99c5f1..74fea15 100644 --- a/src/components/pdf-viewer/page/pdf-page.js +++ b/src/components/pdf-viewer/page/rm-pdf-page.js @@ -1,6 +1,6 @@ import { html, LitElement } from 'lit' import * as pdfjsLib from 'pdfjs-dist' -import styles from './pdf-page.styles.js' +import styles from './rm-pdf-page.styles.js' export default class PDFPage extends LitElement { static get styles() { @@ -125,4 +125,4 @@ export default class PDFPage extends LitElement { } } -customElements.define('pdf-page', PDFPage) +customElements.define('rm-pdf-page', PDFPage) diff --git a/src/components/pdf-viewer/page/pdf-page.styles.js b/src/components/pdf-viewer/page/rm-pdf-page.styles.js similarity index 100% rename from src/components/pdf-viewer/page/pdf-page.styles.js rename to src/components/pdf-viewer/page/rm-pdf-page.styles.js diff --git a/src/components/pdf-viewer/pdf-viewer.js b/src/components/pdf-viewer/rm-pdf-viewer.js similarity index 88% rename from src/components/pdf-viewer/pdf-viewer.js rename to src/components/pdf-viewer/rm-pdf-viewer.js index 5dab83b..901feb4 100644 --- a/src/components/pdf-viewer/pdf-viewer.js +++ b/src/components/pdf-viewer/rm-pdf-viewer.js @@ -2,11 +2,11 @@ import { LitElement, html } from 'lit' import { ContextProvider } from '@lit/context' import * as pdfjsLib from 'pdfjs-dist' import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.mjs?url' -import styles from './pdf-viewer.styles.js' +import styles from './rm-pdf-viewer.styles.js' import { pdfContext } from './pdf-context.js' -import './toolbar/pdf-toolbar.js' -import './sidebar/pdf-sidebar.js' -import './canvas/pdf-canvas.js' +import './toolbar/rm-pdf-toolbar.js' +import './sidebar/rm-pdf-sidebar.js' +import './canvas/rm-pdf-canvas.js' pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker @@ -112,15 +112,15 @@ export default class PDFViewer extends LitElement { render() { return html`
- +
- - + +
` } } -customElements.define('pdf-viewer', PDFViewer) +customElements.define('rm-pdf-viewer', PDFViewer) diff --git a/src/components/pdf-viewer/pdf-viewer.styles.js b/src/components/pdf-viewer/rm-pdf-viewer.styles.js similarity index 100% rename from src/components/pdf-viewer/pdf-viewer.styles.js rename to src/components/pdf-viewer/rm-pdf-viewer.styles.js diff --git a/src/components/pdf-viewer/sidebar/pdf-sidebar.js b/src/components/pdf-viewer/sidebar/rm-pdf-sidebar.js similarity index 78% rename from src/components/pdf-viewer/sidebar/pdf-sidebar.js rename to src/components/pdf-viewer/sidebar/rm-pdf-sidebar.js index 84c16ad..4299eda 100644 --- a/src/components/pdf-viewer/sidebar/pdf-sidebar.js +++ b/src/components/pdf-viewer/sidebar/rm-pdf-sidebar.js @@ -1,7 +1,7 @@ import { html } from 'lit' import { PDFViewerComponent } from '../pdf-viewer-component.js' -import styles from './pdf-sidebar.styles.js' -import '../thumbnail/pdf-thumbnail.js' +import styles from './rm-pdf-sidebar.styles.js' +import '../thumbnail/rm-pdf-thumbnail.js' export default class PDFSidebar extends PDFViewerComponent { @@ -19,9 +19,9 @@ export default class PDFSidebar extends PDFViewerComponent { const thumbnails = [] for (let i = 1; i <= totalPages; i++) { thumbnails.push(html` - + > `) } return thumbnails @@ -38,5 +38,5 @@ export default class PDFSidebar extends PDFViewerComponent { } } -customElements.define('pdf-sidebar', PDFSidebar) +customElements.define('rm-pdf-sidebar', PDFSidebar) diff --git a/src/components/pdf-viewer/sidebar/pdf-sidebar.styles.js b/src/components/pdf-viewer/sidebar/rm-pdf-sidebar.styles.js similarity index 100% rename from src/components/pdf-viewer/sidebar/pdf-sidebar.styles.js rename to src/components/pdf-viewer/sidebar/rm-pdf-sidebar.styles.js diff --git a/src/components/pdf-viewer/thumbnail/pdf-thumbnail.js b/src/components/pdf-viewer/thumbnail/rm-pdf-thumbnail.js similarity index 94% rename from src/components/pdf-viewer/thumbnail/pdf-thumbnail.js rename to src/components/pdf-viewer/thumbnail/rm-pdf-thumbnail.js index 221368a..e7796ec 100644 --- a/src/components/pdf-viewer/thumbnail/pdf-thumbnail.js +++ b/src/components/pdf-viewer/thumbnail/rm-pdf-thumbnail.js @@ -1,6 +1,6 @@ import { html } from 'lit' import { PDFViewerComponent } from '../pdf-viewer-component.js' -import styles from './pdf-thumbnail.styles.js' +import styles from './rm-pdf-thumbnail.styles.js' export default class PDFThumbnail extends PDFViewerComponent { static get properties() { @@ -86,4 +86,4 @@ export default class PDFThumbnail extends PDFViewerComponent { } } -customElements.define('pdf-thumbnail', PDFThumbnail) +customElements.define('rm-pdf-thumbnail', PDFThumbnail) diff --git a/src/components/pdf-viewer/thumbnail/pdf-thumbnail.styles.js b/src/components/pdf-viewer/thumbnail/rm-pdf-thumbnail.styles.js similarity index 100% rename from src/components/pdf-viewer/thumbnail/pdf-thumbnail.styles.js rename to src/components/pdf-viewer/thumbnail/rm-pdf-thumbnail.styles.js diff --git a/src/components/pdf-viewer/toolbar/pdf-toolbar.js b/src/components/pdf-viewer/toolbar/rm-pdf-toolbar.js similarity index 92% rename from src/components/pdf-viewer/toolbar/pdf-toolbar.js rename to src/components/pdf-viewer/toolbar/rm-pdf-toolbar.js index bff154d..4fbe8aa 100644 --- a/src/components/pdf-viewer/toolbar/pdf-toolbar.js +++ b/src/components/pdf-viewer/toolbar/rm-pdf-toolbar.js @@ -1,6 +1,6 @@ import { html } from 'lit' import { PDFViewerComponent } from '../pdf-viewer-component.js' -import styles from './pdf-toolbar.styles.js' +import styles from './rm-pdf-toolbar.styles.js' export default class PDFToolbar extends PDFViewerComponent { static get styles() { @@ -53,5 +53,5 @@ export default class PDFToolbar extends PDFViewerComponent { } } -customElements.define('pdf-toolbar', PDFToolbar) +customElements.define('rm-pdf-toolbar', PDFToolbar) diff --git a/src/components/pdf-viewer/toolbar/pdf-toolbar.styles.js b/src/components/pdf-viewer/toolbar/rm-pdf-toolbar.styles.js similarity index 100% rename from src/components/pdf-viewer/toolbar/pdf-toolbar.styles.js rename to src/components/pdf-viewer/toolbar/rm-pdf-toolbar.styles.js From 5258668a181b424c19b932987e8e8061c77313ad Mon Sep 17 00:00:00 2001 From: Jullian Calkins Date: Wed, 4 Feb 2026 14:10:14 -0500 Subject: [PATCH 14/31] Catch RenderingCancelledException errors --- src/components/pdf-viewer/page/rm-pdf-page.js | 29 ++++++++++++------- .../pdf-viewer/thumbnail/rm-pdf-thumbnail.js | 5 +++- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/src/components/pdf-viewer/page/rm-pdf-page.js b/src/components/pdf-viewer/page/rm-pdf-page.js index 74fea15..1e16749 100644 --- a/src/components/pdf-viewer/page/rm-pdf-page.js +++ b/src/components/pdf-viewer/page/rm-pdf-page.js @@ -53,20 +53,27 @@ export default class PDFPage extends LitElement { const scaledViewport = this.page.getViewport({ scale: this.scale * devicePixelRatio }) - this._renderTask = this.page.render({ - canvasContext, - viewport: scaledViewport - }) + try { + this._renderTask = this.page.render({ + canvasContext, + viewport: scaledViewport + }) - await this._renderTask.promise - this._renderTask = null + await this._renderTask.promise + this._renderTask = null - const textLayerDiv = this.shadowRoot.querySelector('.textLayer') - textLayerDiv.style.width = `${viewport.width}px` - textLayerDiv.style.height = `${viewport.height}px` - textLayerDiv.innerHTML = '' + const textLayerDiv = this.shadowRoot.querySelector('.textLayer') + textLayerDiv.style.width = `${viewport.width}px` + textLayerDiv.style.height = `${viewport.height}px` + textLayerDiv.innerHTML = '' - await this.renderTextLayer(viewport, textLayerDiv) + await this.renderTextLayer(viewport, textLayerDiv) + } catch (error) { + if (error.name !== 'RenderingCancelledException') { + console.error('Error rendering page:', error) + } + this._renderTask = null + } } async renderTextLayer(viewport, textLayerDiv) { diff --git a/src/components/pdf-viewer/thumbnail/rm-pdf-thumbnail.js b/src/components/pdf-viewer/thumbnail/rm-pdf-thumbnail.js index e7796ec..3dbf52b 100644 --- a/src/components/pdf-viewer/thumbnail/rm-pdf-thumbnail.js +++ b/src/components/pdf-viewer/thumbnail/rm-pdf-thumbnail.js @@ -51,7 +51,10 @@ export default class PDFThumbnail extends PDFViewerComponent { await this._renderTask.promise this._renderTask = null } catch (error) { - console.error('Error rendering thumbnail:', error) + if (error.name !== 'RenderingCancelledException') { + console.error('Error rendering thumbnail:', error) + } + this._renderTask = null } } From d36072d7899496dec334e9a13726909f4e36e50d Mon Sep 17 00:00:00 2001 From: Josh McLeod Date: Wed, 4 Feb 2026 14:15:23 -0500 Subject: [PATCH 15/31] Fix text selection # Conflicts: # src/components/pdf-viewer/page/rm-pdf-page.js --- src/components/pdf-viewer/page/rm-pdf-page.js | 4 ++-- src/components/pdf-viewer/page/rm-pdf-page.styles.js | 11 ++++++++--- src/components/pdf-viewer/rm-pdf-viewer.js | 2 -- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/components/pdf-viewer/page/rm-pdf-page.js b/src/components/pdf-viewer/page/rm-pdf-page.js index 1e16749..041c8f7 100644 --- a/src/components/pdf-viewer/page/rm-pdf-page.js +++ b/src/components/pdf-viewer/page/rm-pdf-page.js @@ -62,7 +62,7 @@ export default class PDFPage extends LitElement { await this._renderTask.promise this._renderTask = null - const textLayerDiv = this.shadowRoot.querySelector('.textLayer') + const textLayerDiv = this.shadowRoot.querySelector('.text-layer') textLayerDiv.style.width = `${viewport.width}px` textLayerDiv.style.height = `${viewport.height}px` textLayerDiv.innerHTML = '' @@ -119,7 +119,7 @@ export default class PDFPage extends LitElement { return html`
-
+
` } diff --git a/src/components/pdf-viewer/page/rm-pdf-page.styles.js b/src/components/pdf-viewer/page/rm-pdf-page.styles.js index cd1788c..06c18bc 100644 --- a/src/components/pdf-viewer/page/rm-pdf-page.styles.js +++ b/src/components/pdf-viewer/page/rm-pdf-page.styles.js @@ -15,7 +15,7 @@ export default css` box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } - .textLayer { + .text-layer { position: absolute; left: 0; top: 0; @@ -23,14 +23,19 @@ export default css` opacity: 0.2; line-height: 1; text-align: initial; - pointer-events: none; + pointer-events: auto; } - .textLayer > div { + .text-layer > div { color: transparent; position: absolute; white-space: pre; cursor: text; transform-origin: 0% 0%; + user-select: text; + } + + .text-layer > div::selection { + background-color: rgba(0, 150, 255, 0.9); } ` diff --git a/src/components/pdf-viewer/rm-pdf-viewer.js b/src/components/pdf-viewer/rm-pdf-viewer.js index 901feb4..6efde7c 100644 --- a/src/components/pdf-viewer/rm-pdf-viewer.js +++ b/src/components/pdf-viewer/rm-pdf-viewer.js @@ -47,7 +47,6 @@ export default class PDFViewer extends LitElement { totalPages: this.totalPages, scale: this.scale, setCurrentPage: (page) => { - console.log('setCurrentPage', page) this.currentPage = page }, setScale: (scale) => { @@ -81,7 +80,6 @@ export default class PDFViewer extends LitElement { changedProperties.has('totalPages') || changedProperties.has('scale') ) { - console.log('setting value', this._createContextValue()) this._provider.setValue(this._createContextValue()) } From 9322494dc6c180f8c642916dbb681a08578551e8 Mon Sep 17 00:00:00 2001 From: Jullian Calkins Date: Wed, 4 Feb 2026 14:34:18 -0500 Subject: [PATCH 16/31] Fix scrolling to selected page --- .../pdf-viewer/canvas/rm-pdf-canvas.js | 24 ++++--------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/src/components/pdf-viewer/canvas/rm-pdf-canvas.js b/src/components/pdf-viewer/canvas/rm-pdf-canvas.js index 91010ee..e82ae04 100644 --- a/src/components/pdf-viewer/canvas/rm-pdf-canvas.js +++ b/src/components/pdf-viewer/canvas/rm-pdf-canvas.js @@ -17,27 +17,16 @@ export default class PDFCanvas extends PDFViewerComponent { constructor() { super() this.pages = [] - this.isScrolling = false this.scrollTimeout = null } - async updated(changedProperties) { - if (this.context?.pdfDoc) { - const needsRerender = changedProperties.has('context') && ( - !changedProperties.get('context')?.pdfDoc || - changedProperties.get('context')?.scale !== this.context.scale - ) - - if (needsRerender || this.pages.length === 0) { - await this.loadPages() - } + async updated() { + if (this.context?.pdfDoc && this.pages.length === 0) { + await this.loadPages() } - if (changedProperties.has('context') && this.context?.currentPage) { - const oldContext = changedProperties.get('context') - if (oldContext?.currentPage !== this.context.currentPage && !this.isScrolling) { - setTimeout(() => this.scrollToPage(this.context.currentPage), 100) - } + if (this.context?.currentPage !== this._contextCurrentPage) { + this.scrollToPage(this.context.currentPage) } } @@ -82,7 +71,6 @@ export default class PDFCanvas extends PDFViewerComponent { handleScroll() { if (!this.context?.setCurrentPage) return - this.isScrolling = true clearTimeout(this.scrollTimeout) this.scrollTimeout = setTimeout(() => { @@ -106,8 +94,6 @@ export default class PDFCanvas extends PDFViewerComponent { if (currentPage !== this.context.currentPage) { this.context.setCurrentPage(currentPage) } - - this.isScrolling = false }, 150) } From d7cef3332ace5f97a3e22af78373da78a2f90dff Mon Sep 17 00:00:00 2001 From: Josh McLeod Date: Wed, 4 Feb 2026 14:48:53 -0500 Subject: [PATCH 17/31] Add icons and Rubik font --- index.html | 1 + src/assets/application.css | 3 + src/assets/icons/arrow-left.svg | 1 + src/assets/icons/arrow-right.svg | 1 + src/assets/icons/close-sidebar.svg | 1 + src/assets/icons/close.svg | 1 + src/assets/icons/download.svg | 1 + src/assets/icons/print.svg | 1 + src/assets/icons/zoom-in.svg | 1 + src/assets/icons/zoom-out.svg | 1 + .../pdf-viewer/rm-pdf-viewer.styles.js | 4 ++ .../sidebar/rm-pdf-sidebar.styles.js | 3 + .../pdf-viewer/toolbar/rm-pdf-toolbar.js | 60 +++++++++++++------ .../toolbar/rm-pdf-toolbar.styles.js | 28 +++++++-- 14 files changed, 85 insertions(+), 22 deletions(-) create mode 100644 src/assets/application.css create mode 100644 src/assets/icons/arrow-left.svg create mode 100644 src/assets/icons/arrow-right.svg create mode 100644 src/assets/icons/close-sidebar.svg create mode 100644 src/assets/icons/close.svg create mode 100644 src/assets/icons/download.svg create mode 100644 src/assets/icons/print.svg create mode 100644 src/assets/icons/zoom-in.svg create mode 100644 src/assets/icons/zoom-out.svg diff --git a/index.html b/index.html index ceec02e..e9ca35f 100644 --- a/index.html +++ b/index.html @@ -6,6 +6,7 @@ Spider Web Components + diff --git a/src/assets/application.css b/src/assets/application.css new file mode 100644 index 0000000..293d3b1 --- /dev/null +++ b/src/assets/application.css @@ -0,0 +1,3 @@ +body { + margin: 0; +} diff --git a/src/assets/icons/arrow-left.svg b/src/assets/icons/arrow-left.svg new file mode 100644 index 0000000..7120ac5 --- /dev/null +++ b/src/assets/icons/arrow-left.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/arrow-right.svg b/src/assets/icons/arrow-right.svg new file mode 100644 index 0000000..1192434 --- /dev/null +++ b/src/assets/icons/arrow-right.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/close-sidebar.svg b/src/assets/icons/close-sidebar.svg new file mode 100644 index 0000000..9bba2ad --- /dev/null +++ b/src/assets/icons/close-sidebar.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/close.svg b/src/assets/icons/close.svg new file mode 100644 index 0000000..7df7012 --- /dev/null +++ b/src/assets/icons/close.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/download.svg b/src/assets/icons/download.svg new file mode 100644 index 0000000..6affdf1 --- /dev/null +++ b/src/assets/icons/download.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/print.svg b/src/assets/icons/print.svg new file mode 100644 index 0000000..56c9aa2 --- /dev/null +++ b/src/assets/icons/print.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/zoom-in.svg b/src/assets/icons/zoom-in.svg new file mode 100644 index 0000000..a152736 --- /dev/null +++ b/src/assets/icons/zoom-in.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/zoom-out.svg b/src/assets/icons/zoom-out.svg new file mode 100644 index 0000000..45f1bf5 --- /dev/null +++ b/src/assets/icons/zoom-out.svg @@ -0,0 +1 @@ + diff --git a/src/components/pdf-viewer/rm-pdf-viewer.styles.js b/src/components/pdf-viewer/rm-pdf-viewer.styles.js index 336a14e..87b251f 100644 --- a/src/components/pdf-viewer/rm-pdf-viewer.styles.js +++ b/src/components/pdf-viewer/rm-pdf-viewer.styles.js @@ -1,15 +1,19 @@ import { css } from 'lit' export default css` + :host { display: block; width: 100%; height: 100vh; + } + .pdf-viewer-container { display: flex; flex-direction: column; height: 100%; } + .content-container { display: flex; flex: 1; diff --git a/src/components/pdf-viewer/sidebar/rm-pdf-sidebar.styles.js b/src/components/pdf-viewer/sidebar/rm-pdf-sidebar.styles.js index a134a05..ad68a73 100644 --- a/src/components/pdf-viewer/sidebar/rm-pdf-sidebar.styles.js +++ b/src/components/pdf-viewer/sidebar/rm-pdf-sidebar.styles.js @@ -1,8 +1,11 @@ import { css } from 'lit' export default css` + @import url('https://fonts.googleapis.com/css2?family=Rubik:wght@300;400;500;600;700&display=swap'); + :host { display: block; + font-family: 'Rubik', sans-serif; } .sidebar { diff --git a/src/components/pdf-viewer/toolbar/rm-pdf-toolbar.js b/src/components/pdf-viewer/toolbar/rm-pdf-toolbar.js index 4fbe8aa..17ff374 100644 --- a/src/components/pdf-viewer/toolbar/rm-pdf-toolbar.js +++ b/src/components/pdf-viewer/toolbar/rm-pdf-toolbar.js @@ -1,6 +1,13 @@ import { html } from 'lit' import { PDFViewerComponent } from '../pdf-viewer-component.js' import styles from './rm-pdf-toolbar.styles.js' +import closeIcon from '../../../assets/icons/close.svg' +import arrowLeftIcon from '../../../assets/icons/arrow-left.svg' +import arrowRightIcon from '../../../assets/icons/arrow-right.svg' +import printIcon from '../../../assets/icons/print.svg' +import downloadIcon from '../../../assets/icons/download.svg' +import zoomOutIcon from '../../../assets/icons/zoom-out.svg' +import zoomInIcon from '../../../assets/icons/zoom-in.svg' export default class PDFToolbar extends PDFViewerComponent { static get styles() { @@ -30,24 +37,43 @@ export default class PDFToolbar extends PDFViewerComponent { return html`
- - - Page ${currentPage} of ${totalPages} - - -
- - ${Math.round(scale * 100)}% - +
+ +
+ + + Page ${currentPage} of ${totalPages} + + +
+ +
+ + ${Math.round(scale * 100)}% + +
+ +
+ + +
+ +
` } diff --git a/src/components/pdf-viewer/toolbar/rm-pdf-toolbar.styles.js b/src/components/pdf-viewer/toolbar/rm-pdf-toolbar.styles.js index c493597..3668413 100644 --- a/src/components/pdf-viewer/toolbar/rm-pdf-toolbar.styles.js +++ b/src/components/pdf-viewer/toolbar/rm-pdf-toolbar.styles.js @@ -1,18 +1,22 @@ import { css } from 'lit' export default css` + @import url('https://fonts.googleapis.com/css2?family=Rubik:wght@300;400;500;600;700&display=swap'); + :host { display: block; + font-family: 'Rubik', sans-serif; + font-size: 14px; } .toolbar { - padding: 1rem; - background: #f5f5f5; + padding: 8px; + background: #ffffff; border-bottom: 1px solid #ddd; display: flex; align-items: center; - gap: 1rem; flex-wrap: wrap; + justify-content: space-between; } button { @@ -24,6 +28,15 @@ export default css` font-size: 0.9rem; } + .btn--icon { + padding: 0.25rem; + display: flex; + align-items: center; + justify-content: center; + background: transparent; + border: none; + } + button:hover:not(:disabled) { background: #e9e9e9; } @@ -38,11 +51,16 @@ export default css` color: #333; } - .zoom-controls { + .toolbar__section { + display: flex; + align-items: center; + gap: 3rem; + } + + .toolbar__section-group { display: flex; align-items: center; gap: 0.5rem; - margin-left: auto; } .zoom-level { From 503581fef695051239e47373ec0aef27eb96d157 Mon Sep 17 00:00:00 2001 From: Josh McLeod Date: Wed, 4 Feb 2026 14:52:25 -0500 Subject: [PATCH 18/31] Use system font --- src/components/pdf-viewer/rm-pdf-viewer.styles.js | 3 ++- src/components/pdf-viewer/sidebar/rm-pdf-sidebar.styles.js | 3 --- src/components/pdf-viewer/toolbar/rm-pdf-toolbar.styles.js | 3 --- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/components/pdf-viewer/rm-pdf-viewer.styles.js b/src/components/pdf-viewer/rm-pdf-viewer.styles.js index 87b251f..c134687 100644 --- a/src/components/pdf-viewer/rm-pdf-viewer.styles.js +++ b/src/components/pdf-viewer/rm-pdf-viewer.styles.js @@ -5,7 +5,8 @@ export default css` display: block; width: 100%; height: 100vh; - + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + font-size: 14px; } .pdf-viewer-container { diff --git a/src/components/pdf-viewer/sidebar/rm-pdf-sidebar.styles.js b/src/components/pdf-viewer/sidebar/rm-pdf-sidebar.styles.js index ad68a73..a134a05 100644 --- a/src/components/pdf-viewer/sidebar/rm-pdf-sidebar.styles.js +++ b/src/components/pdf-viewer/sidebar/rm-pdf-sidebar.styles.js @@ -1,11 +1,8 @@ import { css } from 'lit' export default css` - @import url('https://fonts.googleapis.com/css2?family=Rubik:wght@300;400;500;600;700&display=swap'); - :host { display: block; - font-family: 'Rubik', sans-serif; } .sidebar { diff --git a/src/components/pdf-viewer/toolbar/rm-pdf-toolbar.styles.js b/src/components/pdf-viewer/toolbar/rm-pdf-toolbar.styles.js index 3668413..a6bb5e8 100644 --- a/src/components/pdf-viewer/toolbar/rm-pdf-toolbar.styles.js +++ b/src/components/pdf-viewer/toolbar/rm-pdf-toolbar.styles.js @@ -1,12 +1,9 @@ import { css } from 'lit' export default css` - @import url('https://fonts.googleapis.com/css2?family=Rubik:wght@300;400;500;600;700&display=swap'); :host { display: block; - font-family: 'Rubik', sans-serif; - font-size: 14px; } .toolbar { From 3fa4825cb2555826194114b5d09d8172885467e3 Mon Sep 17 00:00:00 2001 From: Josh McLeod Date: Wed, 4 Feb 2026 15:22:31 -0500 Subject: [PATCH 19/31] Implement print button --- src/components/pdf-viewer/rm-pdf-viewer.js | 65 ++++++++++++++++++- .../pdf-viewer/toolbar/rm-pdf-toolbar.js | 6 +- 2 files changed, 68 insertions(+), 3 deletions(-) diff --git a/src/components/pdf-viewer/rm-pdf-viewer.js b/src/components/pdf-viewer/rm-pdf-viewer.js index 6efde7c..b2501f9 100644 --- a/src/components/pdf-viewer/rm-pdf-viewer.js +++ b/src/components/pdf-viewer/rm-pdf-viewer.js @@ -69,11 +69,72 @@ export default class PDFViewer extends LitElement { if (this.scale > 0.5) { this.scale -= 0.25 } + }, + print: async () => { + await this.printPDF() + } + } + } + + async printPDF() { + if (!this.pdfDoc) return + + try { + const iframe = document.createElement('iframe') + iframe.style.display = 'none' + document.body.appendChild(iframe) + + const iframeDoc = iframe.contentDocument || iframe.contentWindow.document + + const style = iframeDoc.createElement('style') + style.textContent = ` + @page { + margin: 0; + size: auto; + } + body { + margin: 0; + } + img { + display: block; + width: 100%; + } + ` + iframeDoc.head.appendChild(style) + + for (let pageNum = 1; pageNum <= this.totalPages; pageNum++) { + const page = await this.pdfDoc.getPage(pageNum) + const viewport = page.getViewport({ scale: 1.5 }) + + const canvas = document.createElement('canvas') + const context = canvas.getContext('2d') + canvas.width = viewport.width + canvas.height = viewport.height + + await page.render({ + canvasContext: context, + viewport: viewport + }).promise + + const img = document.createElement('img') + img.src = canvas.toDataURL() + img.style.width = '100%' + img.style.pageBreakAfter = pageNum < this.totalPages ? 'always' : 'auto' + iframeDoc.body.appendChild(img) } + + iframe.contentWindow.focus() + iframe.contentWindow.print() + + setTimeout(() => { + document.body.removeChild(iframe) + }, 1000) + } catch (error) { + console.error('Error printing PDF:', error) } } - updated(changedProperties) { + async updated(changedProperties) { if ( changedProperties.has('pdfDoc') || changedProperties.has('currentPage') || @@ -84,7 +145,7 @@ export default class PDFViewer extends LitElement { } if (changedProperties.has('src') && this.src) { - this.loadPDF() + await this.loadPDF() } } diff --git a/src/components/pdf-viewer/toolbar/rm-pdf-toolbar.js b/src/components/pdf-viewer/toolbar/rm-pdf-toolbar.js index 17ff374..c50a79a 100644 --- a/src/components/pdf-viewer/toolbar/rm-pdf-toolbar.js +++ b/src/components/pdf-viewer/toolbar/rm-pdf-toolbar.js @@ -30,6 +30,10 @@ export default class PDFToolbar extends PDFViewerComponent { this.context?.zoomOut() } + print() { + this.context?.print() + } + render() { if (!this.context) return html`` @@ -62,7 +66,7 @@ export default class PDFToolbar extends PDFViewerComponent {
- -
From 1b54415a0dc2517fe97a2d7fe1bb96713d6724ec Mon Sep 17 00:00:00 2001 From: Josh McLeod Date: Wed, 4 Feb 2026 15:34:22 -0500 Subject: [PATCH 21/31] Collapsible sidebar --- src/assets/icons/open-sidebar.svg | 1 + src/components/pdf-viewer/rm-pdf-viewer.js | 12 +++++++--- .../pdf-viewer/sidebar/rm-pdf-sidebar.js | 7 ++++-- .../sidebar/rm-pdf-sidebar.styles.js | 15 +++++++++++- .../pdf-viewer/toolbar/rm-pdf-toolbar.js | 12 +++++++++- .../toolbar/rm-pdf-toolbar.styles.js | 24 +++++++++++++++++++ 6 files changed, 64 insertions(+), 7 deletions(-) create mode 100644 src/assets/icons/open-sidebar.svg diff --git a/src/assets/icons/open-sidebar.svg b/src/assets/icons/open-sidebar.svg new file mode 100644 index 0000000..1da26db --- /dev/null +++ b/src/assets/icons/open-sidebar.svg @@ -0,0 +1 @@ + diff --git a/src/components/pdf-viewer/rm-pdf-viewer.js b/src/components/pdf-viewer/rm-pdf-viewer.js index 8a1cdc1..a8e749c 100644 --- a/src/components/pdf-viewer/rm-pdf-viewer.js +++ b/src/components/pdf-viewer/rm-pdf-viewer.js @@ -10,7 +10,6 @@ import './canvas/rm-pdf-canvas.js' pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker - export default class PDFViewer extends LitElement { static get properties() { return { @@ -18,7 +17,8 @@ export default class PDFViewer extends LitElement { pdfDoc: { type: Object, state: true }, currentPage: { type: Number, state: true }, totalPages: { type: Number, state: true }, - scale: { type: Number, state: true } + scale: { type: Number, state: true }, + sidebarCollapsed: { type: Boolean, state: true } } } @@ -33,6 +33,7 @@ export default class PDFViewer extends LitElement { this.currentPage = 1 this.totalPages = 0 this.scale = 1.5 + this.sidebarCollapsed = false this._provider = new ContextProvider(this, { context: pdfContext, @@ -46,6 +47,7 @@ export default class PDFViewer extends LitElement { currentPage: this.currentPage, totalPages: this.totalPages, scale: this.scale, + sidebarCollapsed: this.sidebarCollapsed, setCurrentPage: (page) => { this.currentPage = page }, @@ -75,6 +77,9 @@ export default class PDFViewer extends LitElement { }, download: () => { this.downloadPDF() + }, + toggleSidebar: () => { + this.sidebarCollapsed = !this.sidebarCollapsed } } } @@ -155,7 +160,8 @@ export default class PDFViewer extends LitElement { changedProperties.has('pdfDoc') || changedProperties.has('currentPage') || changedProperties.has('totalPages') || - changedProperties.has('scale') + changedProperties.has('scale') || + changedProperties.has('sidebarCollapsed') ) { this._provider.setValue(this._createContextValue()) } diff --git a/src/components/pdf-viewer/sidebar/rm-pdf-sidebar.js b/src/components/pdf-viewer/sidebar/rm-pdf-sidebar.js index 4299eda..c322cfe 100644 --- a/src/components/pdf-viewer/sidebar/rm-pdf-sidebar.js +++ b/src/components/pdf-viewer/sidebar/rm-pdf-sidebar.js @@ -3,7 +3,6 @@ import { PDFViewerComponent } from '../pdf-viewer-component.js' import styles from './rm-pdf-sidebar.styles.js' import '../thumbnail/rm-pdf-thumbnail.js' - export default class PDFSidebar extends PDFViewerComponent { static get styles() { return styles @@ -28,8 +27,12 @@ export default class PDFSidebar extends PDFViewerComponent { } render() { + if (!this.context) return html`` + + const { sidebarCollapsed } = this.context + return html` -