15 Commits

Author SHA1 Message Date
b76886e3ec fix: align PDF workspace with Toolbox shell 2026-07-23 12:57:50 +02:00
92dd3bcab3 fix: reverse Toolbox header orientation 2026-07-23 01:52:53 +02:00
f22714c37f fix: stabilize Toolbox navigation 2026-07-23 01:13:57 +02:00
5cfbd48282 merge: preserve remote main history for 0.4.0 2026-07-23 00:30:00 +02:00
8425d44658 feat: add shared Toolbox header and themes 2026-07-23 00:29:21 +02:00
d40a288a4e feat: integrate PDF Tools with toolbox portal 2026-07-20 18:13:43 +02:00
bff9e059d8 Move roadmap backlog out of README 2026-07-07 18:52:14 +02:00
5f017d3b22 Bump version to 0.3.4 2026-07-07 18:43:05 +02:00
6386326473 toHex polyfill 2026-07-06 09:42:15 +02:00
54d4dc9545 fix missing toHey function 2026-07-03 09:17:17 +02:00
499db16fce Merge pull request 'v0.3.2' (#3) from v0.3.2 into main
Reviewed-on: zemion/pdf-tools#3
2026-06-06 11:52:22 +02:00
0b975558d0 Merge branch 'main' into v0.3.2 2026-06-06 11:52:16 +02:00
91abda1744 README.md aktualisiert 2026-05-17 12:35:36 +02:00
f0b0f678f1 Merge pull request 'zip download of single pdfs' (#2) from v.0.3.0 into main
Reviewed-on: zemion/pdf-tools#2
2026-05-17 02:58:46 +02:00
c27efaec90 Merge pull request 'publish initial v.0.3.0' (#1) from v.0.3.0 into main
Reviewed-on: zemion/pdf-tools#1
2026-05-17 02:43:54 +02:00
40 changed files with 1322 additions and 637 deletions

3
.gitignore vendored
View File

@@ -142,3 +142,6 @@ vite.config.ts.timestamp-*
todo.txt todo.txt
chatgpt_continuation.md5 chatgpt_continuation.md5
# Generated release artifacts
release/

1
.npmrc Normal file
View File

@@ -0,0 +1 @@
@add-ideas:registry=https://git.add-ideas.de/api/packages/zemion/npm/

View File

@@ -4,6 +4,59 @@ All notable changes to `pdf-tools` are documented here.
The project follows a pragmatic versioning scheme while the app is still below `1.0.0`: minor versions mark coherent user-facing milestones; patch versions mark fixes and small improvements. The project follows a pragmatic versioning scheme while the app is still below `1.0.0`: minor versions mark coherent user-facing milestones; patch versions mark fixes and small improvements.
## 0.4.3 — Aligned Toolbox content width
### Changed
- Expanded the PDF workspace's maximum content width from `900px` to `90rem` so it aligns with the shared Toolbox shell bar and central catalogue.
- Bumped the app/package version to `0.4.3`.
## 0.4.2 — Corrected Toolbox header orientation
### Changed
- Upgraded the Toolbox contract, React shell, and validation testkit to `0.2.2`.
- Moved the Toolbox icon and `add·ideas Toolbox` label to the fixed far-left block, kept the app title and one tagline centered, and moved the fixed control cluster to the far right in the reverse order: Help, the PDF-specific Gitea source link, Apps, and Personalize.
- Bumped the app/package version to `0.4.2`.
## 0.4.1 — Stable Toolbox header layout
### Changed
- Upgraded the Toolbox contract, React shell, and validation testkit to `0.2.1`.
- Adopted the stable shared top bar: the fixed left cluster contains Personalize, Apps, the PDF-specific Gitea source link, and Help; the app title and one tagline remain centered; and the fixed far-right block contains the Toolbox icon and `add·ideas Toolbox` label.
## 0.4.0 — Shared Toolbox header and themes
### Added
- Added the standard Toolbox header controls for in-app help, the app-specific Gitea source link, appearance personalization, and application switching.
- Added explicit light, dark, and system-following themes for the PDF workspace, saved-workspace list, merge queue, dialogs, status messages, and controls.
### Changed
- Upgraded the Toolbox contract, React shell, and validation testkit to `0.2.0`.
- The header now uses the manifest's repository metadata for the source link instead of carrying a release-specific source action.
- Replaced application color literals with semantic PDF workspace variables while intentionally keeping rendered PDF pages on a white paper surface.
- Scoped application button and card styles to the shell's main content so they cannot override shared header controls.
- Bumped the app/package version to `0.4.0`.
## 0.3.4 — PDF.js compatibility and Toolbox integration
### Added
- Added a generated Toolbox App Contract manifest and shared Toolbox Shell integration with standalone fallback.
- Added deterministic, checksummed release ZIP packaging for artifact-based toolbox assembly.
### Changed
- Centralized PDF.js setup in a shared module so thumbnail rendering and page preview use the same legacy PDF.js build and worker configuration.
- Added a local PDF.js worker wrapper that installs a `Uint8Array.prototype.toHex` compatibility shim before PDF.js worker code runs.
- Added the same `toHex` compatibility shim in the main app bundle for browsers that do not provide it natively.
- Made Vite asset URLs relative so the app can be hosted at arbitrary nested paths.
- Corrected the documented licence identifier to `AGPL-3.0-only`.
- Bumped the app/package version to `0.3.4`.
## 0.3.2 — Multi-file merge queue release ## 0.3.2 — Multi-file merge queue release
### Added ### Added

106
README.md
View File

@@ -4,7 +4,9 @@
Current hosted version: <https://pdftools.add-ideas.de> Current hosted version: <https://pdftools.add-ideas.de>
Current release: **v0.3.1 — Split ZIP export release**. See [`CHANGELOG.md`](CHANGELOG.md) for release notes and milestone history. Current development release: **v0.4.3 — aligned Toolbox content width**. See [`CHANGELOG.md`](CHANGELOG.md) for milestone history.
Active work is tracked in Gitea issues. Durable project context is mirrored into the Gitea wiki from repository docs, including [`ROADMAP.md`](ROADMAP.md).
The app is a static React/Vite single-page application. There is no backend service, no server-side queue, and no server-side document storage. When hosted correctly, the server only delivers HTML, JavaScript, CSS, and static assets; PDF processing happens in the user's browser. The app is a static React/Vite single-page application. There is no backend service, no server-side queue, and no server-side document storage. When hosted correctly, the server only delivers HTML, JavaScript, CSS, and static assets; PDF processing happens in the user's browser.
@@ -40,6 +42,8 @@ This makes the project especially useful for self-hosted environments, public-se
- **Thumbnail cache by page and rotation**: Rotated thumbnails are cached and only changed thumbnails need to be regenerated. - **Thumbnail cache by page and rotation**: Rotated thumbnails are cached and only changed thumbnails need to be regenerated.
- **Stable page references**: Duplicated pages and reordered pages are tracked as workspace page references rather than only by original page number. - **Stable page references**: Duplicated pages and reordered pages are tracked as workspace page references rather than only by original page number.
- **Merge choices**: Loading another PDF can replace the current document, append pages, or insert pages at a chosen position. - **Merge choices**: Loading another PDF can replace the current document, append pages, or insert pages at a chosen position.
- **PDF.js compatibility setup**: PDF.js is configured through one shared module and a local worker wrapper so the app can polyfill `Uint8Array.prototype.toHex` before PDF.js runs in older browsers.
- **Toolbox appearance controls**: The shared header provides light, dark, and system-following themes and keeps the app switcher, help, and source controls in the same place as the Toolbox portal.
- **In-app help**: The app includes a Help/Tutorial dialog with keyboard shortcuts and workflow explanations. - **In-app help**: The app includes a Help/Tutorial dialog with keyboard shortcuts and workflow explanations.
## Current features ## Current features
@@ -93,7 +97,7 @@ Keyboard shortcuts are ignored while typing in form fields.
## In-app documentation concept ## In-app documentation concept
The app includes a Help/Tutorial dialog reachable from the header via **Help ?**, `F1`, or `?`. The app includes a Help/Tutorial dialog reachable from the header's **?** control, `F1`, or `?`.
Recommended structure for in-app documentation: Recommended structure for in-app documentation:
@@ -117,6 +121,18 @@ browser <-- HTTPS --> static web server / reverse proxy --> built app assets
There is no application server to operate. Administrators only need to host the built files from `dist/`. There is no application server to operate. Administrators only need to host the built files from `dist/`.
The production build is relocatable and can also be assembled into an
[Add Ideas Toolbox](https://git.add-ideas.de/zemion/toolbox-portal) deployment.
`toolbox-app.json` describes this app through the framework-independent
[Toolbox App Contract](https://git.add-ideas.de/zemion/toolbox-sdk). The same
artifact remains fully usable on its own.
The shared first-party shell always provides help, source, and appearance
controls. When launched with a same-origin catalogue URL, for example
`?toolbox=/toolbox.catalog.json`, it also connects the application switcher to
the current Toolbox catalogue. Missing, invalid, and cross-origin catalogues
are ignored so the app falls back to standalone mode.
### Build and test from source ### Build and test from source
```bash ```bash
@@ -135,6 +151,19 @@ npm run build
The production build is written to `dist/`. The production build is written to `dist/`.
Create a versioned, checksummed release artifact with:
```bash
npm run release:artifact
```
This writes `release/pdf-tools-<version>.zip` and its `.sha256` file. The ZIP
contains the static app, its toolbox manifest and changelog, plus the application
source-code location, license, and the full license/notice texts found for
bundled runtime dependencies under `LICENSES/`. Existing artifacts for the same
version are never replaced implicitly; after intentionally reviewing a rebuild, use
`npm run release:artifact -- --force`.
Useful individual development commands: Useful individual development commands:
```bash ```bash
@@ -219,7 +248,7 @@ The app expects modern browser APIs, including:
- Canvas; - Canvas;
- modern JavaScript modules. - modern JavaScript modules.
Use current versions of Chromium, Firefox, Safari, or Edge. The app includes a small `Uint8Array.prototype.toHex` compatibility shim for PDF.js fingerprint handling in browsers that do not provide that typed-array helper natively. Use current versions of Chromium, Firefox, Safari, or Edge where possible.
## Versioning and release baseline ## Versioning and release baseline
@@ -228,10 +257,10 @@ The application version shown in the header is defined in `src/version.ts`. The
The current development baseline is: The current development baseline is:
```text ```text
v0.3.1 — Split ZIP export release v0.4.3 — aligned Toolbox content width and light/dark/system themes
``` ```
This release preserves the browser-only workspace baseline and adds split-result ZIP downloads on top of the selection-workspace feature. Workspace state, thumbnail handling, generated download URLs, page-grid components, tests, type-checking, linting, and formatting are separated enough to support additional feature work without turning `App.tsx` back into a monolith. This branch preserves the browser-only workspace baseline and PDF.js compatibility setup while using Toolbox SDK `0.2.2`. The displayed app/package version is `0.4.3`; the shared header keeps the Toolbox icon and `add·ideas Toolbox` label in a fixed far-left block, the app title and one tagline centered, and Help, the PDF source link, Apps, and Personalize in a fixed far-right cluster. The PDF workspace now shares the shell bar's `90rem` maximum width. Workspace state, thumbnail handling, generated download URLs, page-grid components, tests, type-checking, linting, and formatting remain separated enough to support additional feature work without turning `App.tsx` back into a monolith.
## Project structure ## Project structure
@@ -248,6 +277,9 @@ src/
ReorderPanel.tsx Page grid, selection, drag/drop, copy/delete/rotate ReorderPanel.tsx Page grid, selection, drag/drop, copy/delete/rotate
WorkspacePanel.tsx Workspace save/load/reset and undo/redo history WorkspacePanel.tsx Workspace save/load/reset and undo/redo history
pdf/ pdf/
pdfJs.ts Shared pdf.js setup and worker configuration
pdfjsWorker.ts Local pdf.js worker wrapper with compatibility shim
uint8ArrayToHexPolyfill.ts Compatibility shim for PDF.js fingerprint handling
pdfService.ts pdf-lib operations: load, merge, split, export pdfService.ts pdf-lib operations: load, merge, split, export
pdfThumbnailService.ts pdf.js thumbnail rendering pdfThumbnailService.ts pdf.js thumbnail rendering
pdfZipService.ts Browser-side ZIP packaging for split results pdfZipService.ts Browser-side ZIP packaging for split results
@@ -260,68 +292,10 @@ src/
version.ts App version displayed in the header version.ts App version displayed in the header
``` ```
## Roadmap ## Roadmap and backlog
### Milestone 1: Fast preview and thumbnails The roadmap and non-goals now live in [`ROADMAP.md`](ROADMAP.md). Open roadmap items are imported into Gitea issues so priorities, status, and implementation notes stay out of the README.
- [x] Remove unused legacy page list view.
- [x] Bound thumbnail generation by width and height.
- [x] Display thumbnails progressively.
- [x] Add preview page flipping.
- [x] Attach preview controls to the modal container.
- [x] Add first keyboard shortcuts.
- [x] Cache thumbnails by page and rotation.
- [x] Regenerate only changed rotated thumbnails.
- [x] Show software version number.
### Milestone 2: Real page workspace
- [x] Introduce stable page references instead of only original page indices.
- [x] Support duplicate selected pages.
- [x] Save and reload the last state from browser storage.
- [x] Support named workspaces.
- [x] Store PDF binaries directly in IndexedDB.
- [x] Reset workspace.
- [x] Add command history as a foundation for undo/redo.
- [x] Add undo/redo.
- [x] Display undo/redo history with redo entries visually separated.
- [x] Extract selection as a new active workspace.
- [ ] Reduce undo/redo storage footprint if large documents make snapshots too heavy.
- [ ] Add grid/list view toggle.
### Milestone 3: Better merge and mobile handling
- [ ] Add a full multi-file merge queue.
- [ ] Support drag-and-drop of PDFs into the page grid at the hovered position.
- [ ] Add custom long-press drag on mobile.
- [ ] Consolidate frequently used actions into a toolbar.
### Milestone 4: Structural PDF editing
- [ ] Metadata editing.
- [ ] Crop pages.
- [ ] Add tools directly in the preview overlay.
- [ ] Read/fill/flatten forms.
- [ ] Read bookmarks, then evaluate bookmark editing.
- [ ] Read annotations, then evaluate annotation writing.
### Milestone 5: Export and power tools
- [ ] Basic text extraction.
- [x] ZIP export for split results.
- [ ] Optimize/compress MVP.
- [ ] Carefully scoped encrypted PDF handling.
## Non-goals for now
- Server-side PDF processing.
- Collaborative editing.
- User accounts.
- OCR.
- Full content-stream editing.
- Digital signature creation/validation workflows.
- DMS replacement functionality.
## License ## License
GPL-3.0. See `LICENSE`. AGPL-3.0-only. See `LICENSE`.

63
ROADMAP.md Normal file
View File

@@ -0,0 +1,63 @@
# pdf-tools Roadmap
Active roadmap items are tracked as Gitea issues. This file is kept as the source for issue import and wiki sync; completed items remain here as release context.
## Milestone 1: Fast preview and thumbnails
- [x] Remove unused legacy page list view.
- [x] Bound thumbnail generation by width and height.
- [x] Display thumbnails progressively.
- [x] Add preview page flipping.
- [x] Attach preview controls to the modal container.
- [x] Add first keyboard shortcuts.
- [x] Cache thumbnails by page and rotation.
- [x] Regenerate only changed rotated thumbnails.
- [x] Show software version number.
## Milestone 2: Real page workspace
- [x] Introduce stable page references instead of only original page indices.
- [x] Support duplicate selected pages.
- [x] Save and reload the last state from browser storage.
- [x] Support named workspaces.
- [x] Store PDF binaries directly in IndexedDB.
- [x] Reset workspace.
- [x] Add command history as a foundation for undo/redo.
- [x] Add undo/redo.
- [x] Display undo/redo history with redo entries visually separated.
- [x] Extract selection as a new active workspace.
- [ ] Reduce undo/redo storage footprint if large documents make snapshots too heavy.
- [ ] Add grid/list view toggle.
## Milestone 3: Better merge and mobile handling
- [x] Add a full multi-file merge queue.
- [ ] Support drag-and-drop of PDFs into the page grid at the hovered position.
- [ ] Add custom long-press drag on mobile.
- [ ] Consolidate frequently used actions into a toolbar.
## Milestone 4: Structural PDF editing
- [ ] Metadata editing.
- [ ] Crop pages.
- [ ] Add tools directly in the preview overlay.
- [ ] Read/fill/flatten forms.
- [ ] Read bookmarks, then evaluate bookmark editing.
- [ ] Read annotations, then evaluate annotation writing.
## Milestone 5: Export and power tools
- [ ] Basic text extraction.
- [x] ZIP export for split results.
- [ ] Optimize/compress MVP.
- [ ] Carefully scoped encrypted PDF handling.
## Non-goals for now
- Server-side PDF processing.
- Collaborative editing.
- User accounts.
- OCR.
- Full content-stream editing.
- Digital signature creation/validation workflows.
- DMS replacement functionality.

View File

@@ -43,7 +43,7 @@ export default tseslint.config(
}, },
}, },
{ {
files: ['*.config.{js,ts}', 'eslint.config.js'], files: ['*.config.{js,ts}', 'eslint.config.js', 'scripts/**/*.mjs'],
languageOptions: { languageOptions: {
globals: { globals: {
...globals.node, ...globals.node,

View File

@@ -0,0 +1,24 @@
core-js 3.49.0 — MIT License
Source: https://github.com/zloirock/core-js/tree/v3.49.0
Copyright (c) 20132025 Denis Pushkarev (zloirock.ru)
Copyright (c) 20252026 CoreJS Company (core-js.io)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

508
package-lock.json generated
View File

@@ -1,13 +1,16 @@
{ {
"name": "pdf-tools", "name": "pdf-tools",
"version": "0.3.2", "version": "0.4.3",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "pdf-tools", "name": "pdf-tools",
"version": "0.3.2", "version": "0.4.3",
"license": "AGPL-3.0-only",
"dependencies": { "dependencies": {
"@add-ideas/toolbox-contract": "^0.2.2",
"@add-ideas/toolbox-shell-react": "^0.2.2",
"fflate": "^0.8.3", "fflate": "^0.8.3",
"pdf-lib": "^1.17.1", "pdf-lib": "^1.17.1",
"pdfjs-dist": "^5.7.284", "pdfjs-dist": "^5.7.284",
@@ -15,6 +18,7 @@
"react-dom": "^19.2.6" "react-dom": "^19.2.6"
}, },
"devDependencies": { "devDependencies": {
"@add-ideas/toolbox-testkit": "^0.2.2",
"@eslint/js": "^10.0.1", "@eslint/js": "^10.0.1",
"@testing-library/react": "^16.3.2", "@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1", "@testing-library/user-event": "^14.6.1",
@@ -35,6 +39,38 @@
"vitest": "^4.1.6" "vitest": "^4.1.6"
} }
}, },
"node_modules/@add-ideas/toolbox-contract": {
"version": "0.2.2",
"resolved": "https://git.add-ideas.de/api/packages/zemion/npm/%40add-ideas%2Ftoolbox-contract/-/0.2.2/toolbox-contract-0.2.2.tgz",
"integrity": "sha512-VcZ8j4O2PgFaIYgxs6r9u0uPxA+hHKwhCW+omfeVCtbjAAYhH2KhRlBGm0ERVVYcK5kchyzZLXrC4LKWRotVzg==",
"license": "Apache-2.0"
},
"node_modules/@add-ideas/toolbox-shell-react": {
"version": "0.2.2",
"resolved": "https://git.add-ideas.de/api/packages/zemion/npm/%40add-ideas%2Ftoolbox-shell-react/-/0.2.2/toolbox-shell-react-0.2.2.tgz",
"integrity": "sha512-w/xbLCd50a2Jq7vQ9Z9ygUOuqlOCRkIYlk4vSJx0mf4REJNeMYi3PE2MbaLUC4DkQWyrmatsa5HNT89o0l91BA==",
"license": "Apache-2.0",
"dependencies": {
"@add-ideas/toolbox-contract": "0.2.2"
},
"peerDependencies": {
"react": ">=18 <20",
"react-dom": ">=18 <20"
}
},
"node_modules/@add-ideas/toolbox-testkit": {
"version": "0.2.2",
"resolved": "https://git.add-ideas.de/api/packages/zemion/npm/%40add-ideas%2Ftoolbox-testkit/-/0.2.2/toolbox-testkit-0.2.2.tgz",
"integrity": "sha512-ZxrRYiJI/j5aXwcd3mQItgDl1iOJ6N26+MpIG18qVZNwKKFCTCfSTzgzRooPt8rRc1MQYTPo4c+l/QpOWuTXxA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@add-ideas/toolbox-contract": "0.2.2"
},
"bin": {
"toolbox-check": "dist/cli.js"
}
},
"node_modules/@asamuzakjp/css-color": { "node_modules/@asamuzakjp/css-color": {
"version": "5.1.11", "version": "5.1.11",
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz",
@@ -87,13 +123,13 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@babel/code-frame": { "node_modules/@babel/code-frame": {
"version": "7.29.0", "version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
"integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/helper-validator-identifier": "^7.28.5", "@babel/helper-validator-identifier": "^7.29.7",
"js-tokens": "^4.0.0", "js-tokens": "^4.0.0",
"picocolors": "^1.1.1" "picocolors": "^1.1.1"
}, },
@@ -102,9 +138,9 @@
} }
}, },
"node_modules/@babel/compat-data": { "node_modules/@babel/compat-data": {
"version": "7.29.3", "version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
"integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -112,21 +148,21 @@
} }
}, },
"node_modules/@babel/core": { "node_modules/@babel/core": {
"version": "7.29.0", "version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.29.0", "@babel/code-frame": "^7.29.7",
"@babel/generator": "^7.29.0", "@babel/generator": "^7.29.7",
"@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-compilation-targets": "^7.29.7",
"@babel/helper-module-transforms": "^7.28.6", "@babel/helper-module-transforms": "^7.29.7",
"@babel/helpers": "^7.28.6", "@babel/helpers": "^7.29.7",
"@babel/parser": "^7.29.0", "@babel/parser": "^7.29.7",
"@babel/template": "^7.28.6", "@babel/template": "^7.29.7",
"@babel/traverse": "^7.29.0", "@babel/traverse": "^7.29.7",
"@babel/types": "^7.29.0", "@babel/types": "^7.29.7",
"@jridgewell/remapping": "^2.3.5", "@jridgewell/remapping": "^2.3.5",
"convert-source-map": "^2.0.0", "convert-source-map": "^2.0.0",
"debug": "^4.1.0", "debug": "^4.1.0",
@@ -143,14 +179,14 @@
} }
}, },
"node_modules/@babel/generator": { "node_modules/@babel/generator": {
"version": "7.29.1", "version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz",
"integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/parser": "^7.29.0", "@babel/parser": "^7.29.7",
"@babel/types": "^7.29.0", "@babel/types": "^7.29.7",
"@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/gen-mapping": "^0.3.12",
"@jridgewell/trace-mapping": "^0.3.28", "@jridgewell/trace-mapping": "^0.3.28",
"jsesc": "^3.0.2" "jsesc": "^3.0.2"
@@ -160,14 +196,14 @@
} }
}, },
"node_modules/@babel/helper-compilation-targets": { "node_modules/@babel/helper-compilation-targets": {
"version": "7.28.6", "version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
"integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/compat-data": "^7.28.6", "@babel/compat-data": "^7.29.7",
"@babel/helper-validator-option": "^7.27.1", "@babel/helper-validator-option": "^7.29.7",
"browserslist": "^4.24.0", "browserslist": "^4.24.0",
"lru-cache": "^5.1.1", "lru-cache": "^5.1.1",
"semver": "^6.3.1" "semver": "^6.3.1"
@@ -187,9 +223,9 @@
} }
}, },
"node_modules/@babel/helper-globals": { "node_modules/@babel/helper-globals": {
"version": "7.28.0", "version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
"integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -197,29 +233,29 @@
} }
}, },
"node_modules/@babel/helper-module-imports": { "node_modules/@babel/helper-module-imports": {
"version": "7.28.6", "version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
"integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/traverse": "^7.28.6", "@babel/traverse": "^7.29.7",
"@babel/types": "^7.28.6" "@babel/types": "^7.29.7"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@babel/helper-module-transforms": { "node_modules/@babel/helper-module-transforms": {
"version": "7.28.6", "version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
"integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/helper-module-imports": "^7.28.6", "@babel/helper-module-imports": "^7.29.7",
"@babel/helper-validator-identifier": "^7.28.5", "@babel/helper-validator-identifier": "^7.29.7",
"@babel/traverse": "^7.28.6" "@babel/traverse": "^7.29.7"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
@@ -229,9 +265,9 @@
} }
}, },
"node_modules/@babel/helper-string-parser": { "node_modules/@babel/helper-string-parser": {
"version": "7.27.1", "version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -239,9 +275,9 @@
} }
}, },
"node_modules/@babel/helper-validator-identifier": { "node_modules/@babel/helper-validator-identifier": {
"version": "7.28.5", "version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -249,9 +285,9 @@
} }
}, },
"node_modules/@babel/helper-validator-option": { "node_modules/@babel/helper-validator-option": {
"version": "7.27.1", "version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
"integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -259,27 +295,27 @@
} }
}, },
"node_modules/@babel/helpers": { "node_modules/@babel/helpers": {
"version": "7.29.2", "version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
"integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/template": "^7.28.6", "@babel/template": "^7.29.7",
"@babel/types": "^7.29.0" "@babel/types": "^7.29.7"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@babel/parser": { "node_modules/@babel/parser": {
"version": "7.29.3", "version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
"integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/types": "^7.29.0" "@babel/types": "^7.29.7"
}, },
"bin": { "bin": {
"parser": "bin/babel-parser.js" "parser": "bin/babel-parser.js"
@@ -299,33 +335,33 @@
} }
}, },
"node_modules/@babel/template": { "node_modules/@babel/template": {
"version": "7.28.6", "version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
"integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.28.6", "@babel/code-frame": "^7.29.7",
"@babel/parser": "^7.28.6", "@babel/parser": "^7.29.7",
"@babel/types": "^7.28.6" "@babel/types": "^7.29.7"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@babel/traverse": { "node_modules/@babel/traverse": {
"version": "7.29.0", "version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz",
"integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.29.0", "@babel/code-frame": "^7.29.7",
"@babel/generator": "^7.29.0", "@babel/generator": "^7.29.7",
"@babel/helper-globals": "^7.28.0", "@babel/helper-globals": "^7.29.7",
"@babel/parser": "^7.29.0", "@babel/parser": "^7.29.7",
"@babel/template": "^7.28.6", "@babel/template": "^7.29.7",
"@babel/types": "^7.29.0", "@babel/types": "^7.29.7",
"debug": "^4.3.1" "debug": "^4.3.1"
}, },
"engines": { "engines": {
@@ -333,14 +369,14 @@
} }
}, },
"node_modules/@babel/types": { "node_modules/@babel/types": {
"version": "7.29.0", "version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
"integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/helper-string-parser": "^7.27.1", "@babel/helper-string-parser": "^7.29.7",
"@babel/helper-validator-identifier": "^7.28.5" "@babel/helper-validator-identifier": "^7.29.7"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
@@ -500,14 +536,14 @@
} }
}, },
"node_modules/@emnapi/core": { "node_modules/@emnapi/core": {
"version": "1.10.0", "version": "1.11.1",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz",
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
"@emnapi/wasi-threads": "1.2.1", "@emnapi/wasi-threads": "1.2.2",
"tslib": "^2.4.0" "tslib": "^2.4.0"
} }
}, },
@@ -520,9 +556,9 @@
"optional": true "optional": true
}, },
"node_modules/@emnapi/runtime": { "node_modules/@emnapi/runtime": {
"version": "1.10.0", "version": "1.11.1",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz",
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
@@ -539,9 +575,9 @@
"optional": true "optional": true
}, },
"node_modules/@emnapi/wasi-threads": { "node_modules/@emnapi/wasi-threads": {
"version": "1.2.1", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
@@ -1070,14 +1106,14 @@
} }
}, },
"node_modules/@napi-rs/wasm-runtime": { "node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.4", "version": "1.1.6",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz",
"integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
"@tybys/wasm-util": "^0.10.1" "@tybys/wasm-util": "^0.10.3"
}, },
"funding": { "funding": {
"type": "github", "type": "github",
@@ -1089,9 +1125,9 @@
} }
}, },
"node_modules/@oxc-project/types": { "node_modules/@oxc-project/types": {
"version": "0.130.0", "version": "0.139.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.130.0.tgz", "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz",
"integrity": "sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q==", "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
@@ -1117,9 +1153,9 @@
} }
}, },
"node_modules/@rolldown/binding-android-arm64": { "node_modules/@rolldown/binding-android-arm64": {
"version": "1.0.1", "version": "1.1.5",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz",
"integrity": "sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg==", "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1134,9 +1170,9 @@
} }
}, },
"node_modules/@rolldown/binding-darwin-arm64": { "node_modules/@rolldown/binding-darwin-arm64": {
"version": "1.0.1", "version": "1.1.5",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz",
"integrity": "sha512-cKnAhWEsV7TPcA/5EAteDp6KcJZBQ2G+BqE7zayMMi7kMvwRsbv7WT9aOnn0WNl4SKEIf43vjS31iUPu80nzXg==", "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1151,9 +1187,9 @@
} }
}, },
"node_modules/@rolldown/binding-darwin-x64": { "node_modules/@rolldown/binding-darwin-x64": {
"version": "1.0.1", "version": "1.1.5",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz",
"integrity": "sha512-YKrVwQjIRBPo+5G/u03wGjbdy4q7pyzCe93DK9VJ7zkVmeg8LJ7GbgsiHWdR4xSoe4CAXRD7Bcjgbtr64bkXNg==", "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1168,9 +1204,9 @@
} }
}, },
"node_modules/@rolldown/binding-freebsd-x64": { "node_modules/@rolldown/binding-freebsd-x64": {
"version": "1.0.1", "version": "1.1.5",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz",
"integrity": "sha512-z/oBsREo46SsFqBwYtFe0kpJeBijAT48O/WXLI4suiCLBkr03RTtTJMCzSdDd2znlh8VJizL09XVkQgk8IZonw==", "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1185,9 +1221,9 @@
} }
}, },
"node_modules/@rolldown/binding-linux-arm-gnueabihf": { "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
"version": "1.0.1", "version": "1.1.5",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz",
"integrity": "sha512-ik8q7GM11zxvYxFc2PeDcT6TBvhCQMaUxfph/M5l9sKuTs/Sjg3L+Byw0F7w0ZVLBZmx30P+gG0ECzzN+MFcmQ==", "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -1202,13 +1238,16 @@
} }
}, },
"node_modules/@rolldown/binding-linux-arm64-gnu": { "node_modules/@rolldown/binding-linux-arm64-gnu": {
"version": "1.0.1", "version": "1.1.5",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz",
"integrity": "sha512-QoSx2EkyrrdZ6kcyE8stqZ62t0Yra8Fs5ia9lOxJrh6TMQJK7gQKmscdTHf7pOXKREKrVwOtJcQG3qVSfc866A==", "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1219,13 +1258,16 @@
} }
}, },
"node_modules/@rolldown/binding-linux-arm64-musl": { "node_modules/@rolldown/binding-linux-arm64-musl": {
"version": "1.0.1", "version": "1.1.5",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz",
"integrity": "sha512-uwNwFpwKeNiZawfAWBgg0VIztPTV3ihhh1vV334h9ivnNLorxnQMU6Fz8wG1Zb4Qh9LC1/MkcyT3YlDXG3Rsgg==", "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1236,13 +1278,16 @@
} }
}, },
"node_modules/@rolldown/binding-linux-ppc64-gnu": { "node_modules/@rolldown/binding-linux-ppc64-gnu": {
"version": "1.0.1", "version": "1.1.5",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz",
"integrity": "sha512-zY1bul7OWr7DFBiJ++wofXvnr8B45ce3QsQUhKrIhXsygAh7bTkwyeM1bi1a2g5C/yC/N8TZyGDEoMfm/l9mpg==", "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1253,13 +1298,16 @@
} }
}, },
"node_modules/@rolldown/binding-linux-s390x-gnu": { "node_modules/@rolldown/binding-linux-s390x-gnu": {
"version": "1.0.1", "version": "1.1.5",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz",
"integrity": "sha512-0frlsT/f4Ft6I7SMESTKnF3cZsdicQn1dCMkF/jT9wDLE+gGoiQfv1nmT9e+s7s/fekvvy6tZM2jHvI2tkbJDQ==", "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1270,13 +1318,16 @@
} }
}, },
"node_modules/@rolldown/binding-linux-x64-gnu": { "node_modules/@rolldown/binding-linux-x64-gnu": {
"version": "1.0.1", "version": "1.1.5",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz",
"integrity": "sha512-XABVmGp9Tg0WspTVvwduTc4fpqy6JnAUrSQe6OuyqD/03nI7r0O9OWUkMIwFrjKAIqolvqoA4ZrJppgwE0Gxmw==", "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1287,13 +1338,16 @@
} }
}, },
"node_modules/@rolldown/binding-linux-x64-musl": { "node_modules/@rolldown/binding-linux-x64-musl": {
"version": "1.0.1", "version": "1.1.5",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz",
"integrity": "sha512-bV4fzswuzVcKD90o/VM6QqKxnxlDq0g2BISDLNVmxrnhpv1DDbyPhCIjYfvzYLV+MvkKKnQt2Q6AO86SEBULUQ==", "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1304,9 +1358,9 @@
} }
}, },
"node_modules/@rolldown/binding-openharmony-arm64": { "node_modules/@rolldown/binding-openharmony-arm64": {
"version": "1.0.1", "version": "1.1.5",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz",
"integrity": "sha512-/Mh0Zhq3OP7fVs0kcQHZP6lZEthMGTaSf8UBQYSFEZDWGXXlEC+nJ6EqenaK2t4LBXMe3A+K/G2BVXXdtOr4PQ==", "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1321,9 +1375,9 @@
} }
}, },
"node_modules/@rolldown/binding-wasm32-wasi": { "node_modules/@rolldown/binding-wasm32-wasi": {
"version": "1.0.1", "version": "1.1.5",
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz",
"integrity": "sha512-+1xc9X45l8ufsBAm6Gjvx2qDRIY9lTVt0cgWNcJ+1gdhXvkbxePA60yRTwSTuXL09CMhyJmjpV7E3NoyxbqFQQ==", "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==",
"cpu": [ "cpu": [
"wasm32" "wasm32"
], ],
@@ -1331,18 +1385,18 @@
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
"@emnapi/core": "1.10.0", "@emnapi/core": "1.11.1",
"@emnapi/runtime": "1.10.0", "@emnapi/runtime": "1.11.1",
"@napi-rs/wasm-runtime": "^1.1.4" "@napi-rs/wasm-runtime": "^1.1.6"
}, },
"engines": { "engines": {
"node": "^20.19.0 || >=22.12.0" "node": "^20.19.0 || >=22.12.0"
} }
}, },
"node_modules/@rolldown/binding-win32-arm64-msvc": { "node_modules/@rolldown/binding-win32-arm64-msvc": {
"version": "1.0.1", "version": "1.1.5",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz",
"integrity": "sha512-1D+UqZdfnuR+Jy1GgMJwi85bD40H21uNmOPRWQhw4oRSuolZ/B5rixZ45DK2KXOTCvmVCecauWgEhbw8bI7tOw==", "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1357,9 +1411,9 @@
} }
}, },
"node_modules/@rolldown/binding-win32-x64-msvc": { "node_modules/@rolldown/binding-win32-x64-msvc": {
"version": "1.0.1", "version": "1.1.5",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz",
"integrity": "sha512-INAycaWuhlOK3wk4mRHGsdgwYWmd9cChdPdE9bwWmy6rn9VqVNYNFGhOdXrofXUxwHIncSiPNb8tNm8knDVIeQ==", "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1373,6 +1427,13 @@
"node": "^20.19.0 || >=22.12.0" "node": "^20.19.0 || >=22.12.0"
} }
}, },
"node_modules/@rolldown/pluginutils": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
"integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
"dev": true,
"license": "MIT"
},
"node_modules/@standard-schema/spec": { "node_modules/@standard-schema/spec": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
@@ -1444,9 +1505,9 @@
} }
}, },
"node_modules/@tybys/wasm-util": { "node_modules/@tybys/wasm-util": {
"version": "0.10.2", "version": "0.10.3",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
"integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
@@ -1808,13 +1869,6 @@
} }
} }
}, },
"node_modules/@vitejs/plugin-react/node_modules/@rolldown/pluginutils": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
"integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
"dev": true,
"license": "MIT"
},
"node_modules/@vitest/expect": { "node_modules/@vitest/expect": {
"version": "4.1.6", "version": "4.1.6",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.6.tgz", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.6.tgz",
@@ -1998,9 +2052,9 @@
} }
}, },
"node_modules/baseline-browser-mapping": { "node_modules/baseline-browser-mapping": {
"version": "2.10.30", "version": "2.10.43",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.30.tgz", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.43.tgz",
"integrity": "sha512-xjOFN16Ha1+Rz4nFYKqHU/LSB+gx/Vi3yQLX7r7sAW+Wa+8hhF2h4pvqTrTMc8+WcDBEunnUurr46Jvv0jk3Vg==", "integrity": "sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
@@ -2034,9 +2088,9 @@
} }
}, },
"node_modules/browserslist": { "node_modules/browserslist": {
"version": "4.28.2", "version": "4.28.6",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.6.tgz",
"integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", "integrity": "sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -2054,10 +2108,10 @@
], ],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.10.12", "baseline-browser-mapping": "^2.10.42",
"caniuse-lite": "^1.0.30001782", "caniuse-lite": "^1.0.30001803",
"electron-to-chromium": "^1.5.328", "electron-to-chromium": "^1.5.389",
"node-releases": "^2.0.36", "node-releases": "^2.0.51",
"update-browserslist-db": "^1.2.3" "update-browserslist-db": "^1.2.3"
}, },
"bin": { "bin": {
@@ -2068,9 +2122,9 @@
} }
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001792", "version": "1.0.30001806",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001792.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz",
"integrity": "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==", "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -2217,9 +2271,9 @@
"peer": true "peer": true
}, },
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.5.357", "version": "1.5.393",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.357.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.393.tgz",
"integrity": "sha512-NHlTIQDK8fmVwHwuIzmXYEJ1Ewq3D9wDNc0cWXxDGysP6Pb21giwGNkxiTifyKy/4SoPuN5l6GLP1W9Sv7zB2g==", "integrity": "sha512-kiDJdIUawuEIcp9XoICKp1iTYDEbgguIPq526N1Q7jIQDeQ3CqoMx71025PI/7E48Ddtw2HuWsVjY7afEgNxmg==",
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
@@ -2525,7 +2579,7 @@
}, },
"node_modules/fflate": { "node_modules/fflate": {
"version": "0.8.3", "version": "0.8.3",
"resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/fflate/-/fflate-0.8.3.tgz", "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz",
"integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==",
"license": "MIT" "license": "MIT"
}, },
@@ -3176,9 +3230,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.3.11", "version": "3.3.16",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -3202,11 +3256,14 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/node-releases": { "node_modules/node-releases": {
"version": "2.0.44", "version": "2.0.51",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.44.tgz", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz",
"integrity": "sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==", "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT",
"engines": {
"node": ">=18"
}
}, },
"node_modules/obug": { "node_modules/obug": {
"version": "2.1.1", "version": "2.1.1",
@@ -3347,9 +3404,9 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/picomatch": { "node_modules/picomatch": {
"version": "4.0.4", "version": "4.0.5",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -3360,9 +3417,9 @@
} }
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.5.14", "version": "8.5.20",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.20.tgz",
"integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", "integrity": "sha512-lW616l85ucIQL+FocMmL7pQFPqBmwejrCMg+iPxyImlrANNJG9NHq/RkyCZopDhd8C3LA03PHRJDjkbGu8vvug==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -3380,7 +3437,7 @@
], ],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"nanoid": "^3.3.11", "nanoid": "^3.3.16",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
"source-map-js": "^1.2.1" "source-map-js": "^1.2.1"
}, },
@@ -3480,13 +3537,13 @@
} }
}, },
"node_modules/rolldown": { "node_modules/rolldown": {
"version": "1.0.1", "version": "1.1.5",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.1.tgz", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz",
"integrity": "sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ==", "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@oxc-project/types": "=0.130.0", "@oxc-project/types": "=0.139.0",
"@rolldown/pluginutils": "^1.0.0" "@rolldown/pluginutils": "^1.0.0"
}, },
"bin": { "bin": {
@@ -3496,30 +3553,23 @@
"node": "^20.19.0 || >=22.12.0" "node": "^20.19.0 || >=22.12.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@rolldown/binding-android-arm64": "1.0.1", "@rolldown/binding-android-arm64": "1.1.5",
"@rolldown/binding-darwin-arm64": "1.0.1", "@rolldown/binding-darwin-arm64": "1.1.5",
"@rolldown/binding-darwin-x64": "1.0.1", "@rolldown/binding-darwin-x64": "1.1.5",
"@rolldown/binding-freebsd-x64": "1.0.1", "@rolldown/binding-freebsd-x64": "1.1.5",
"@rolldown/binding-linux-arm-gnueabihf": "1.0.1", "@rolldown/binding-linux-arm-gnueabihf": "1.1.5",
"@rolldown/binding-linux-arm64-gnu": "1.0.1", "@rolldown/binding-linux-arm64-gnu": "1.1.5",
"@rolldown/binding-linux-arm64-musl": "1.0.1", "@rolldown/binding-linux-arm64-musl": "1.1.5",
"@rolldown/binding-linux-ppc64-gnu": "1.0.1", "@rolldown/binding-linux-ppc64-gnu": "1.1.5",
"@rolldown/binding-linux-s390x-gnu": "1.0.1", "@rolldown/binding-linux-s390x-gnu": "1.1.5",
"@rolldown/binding-linux-x64-gnu": "1.0.1", "@rolldown/binding-linux-x64-gnu": "1.1.5",
"@rolldown/binding-linux-x64-musl": "1.0.1", "@rolldown/binding-linux-x64-musl": "1.1.5",
"@rolldown/binding-openharmony-arm64": "1.0.1", "@rolldown/binding-openharmony-arm64": "1.1.5",
"@rolldown/binding-wasm32-wasi": "1.0.1", "@rolldown/binding-wasm32-wasi": "1.1.5",
"@rolldown/binding-win32-arm64-msvc": "1.0.1", "@rolldown/binding-win32-arm64-msvc": "1.1.5",
"@rolldown/binding-win32-x64-msvc": "1.0.1" "@rolldown/binding-win32-x64-msvc": "1.1.5"
} }
}, },
"node_modules/rolldown/node_modules/@rolldown/pluginutils": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
"integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
"dev": true,
"license": "MIT"
},
"node_modules/saxes": { "node_modules/saxes": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
@@ -3628,9 +3678,9 @@
} }
}, },
"node_modules/tinyglobby": { "node_modules/tinyglobby": {
"version": "0.2.16", "version": "0.2.17",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
"integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -3771,9 +3821,9 @@
} }
}, },
"node_modules/undici": { "node_modules/undici": {
"version": "7.25.0", "version": "7.28.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz",
"integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -3829,17 +3879,17 @@
} }
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "8.0.13", "version": "8.1.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.13.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz",
"integrity": "sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw==", "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"lightningcss": "^1.32.0", "lightningcss": "^1.32.0",
"picomatch": "^4.0.4", "picomatch": "^4.0.5",
"postcss": "^8.5.14", "postcss": "^8.5.17",
"rolldown": "1.0.1", "rolldown": "~1.1.5",
"tinyglobby": "^0.2.16" "tinyglobby": "^0.2.17"
}, },
"bin": { "bin": {
"vite": "bin/vite.js" "vite": "bin/vite.js"
@@ -3855,7 +3905,7 @@
}, },
"peerDependencies": { "peerDependencies": {
"@types/node": "^20.19.0 || >=22.12.0", "@types/node": "^20.19.0 || >=22.12.0",
"@vitejs/devtools": "^0.1.18", "@vitejs/devtools": "^0.3.0",
"esbuild": "^0.27.0 || ^0.28.0", "esbuild": "^0.27.0 || ^0.28.0",
"jiti": ">=1.21.0", "jiti": ">=1.21.0",
"less": "^4.0.0", "less": "^4.0.0",

View File

@@ -1,10 +1,13 @@
{ {
"name": "pdf-tools", "name": "pdf-tools",
"version": "0.3.2", "version": "0.4.3",
"private": true, "private": true,
"license": "AGPL-3.0-only",
"type": "module", "type": "module",
"scripts": { "scripts": {
"predev": "npm run manifest:generate",
"dev": "vite", "dev": "vite",
"prebuild": "npm run manifest:generate",
"build": "vite build", "build": "vite build",
"preview": "vite preview", "preview": "vite preview",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
@@ -12,10 +15,16 @@
"format": "prettier --write .", "format": "prettier --write .",
"format:check": "prettier --check .", "format:check": "prettier --check .",
"test": "vitest run --environment jsdom", "test": "vitest run --environment jsdom",
"test:toolbox": "toolbox-check dist",
"test:watch": "vitest --environment jsdom", "test:watch": "vitest --environment jsdom",
"check": "npm run typecheck && npm run lint && npm run test && npm run build" "manifest:generate": "node scripts/generate-toolbox-manifest.mjs",
"manifest:check": "node scripts/generate-toolbox-manifest.mjs --check",
"release:artifact": "npm run check && node scripts/package-release.mjs",
"check": "npm run manifest:check && npm run typecheck && npm run lint && npm run test && npm run build && npm run test:toolbox"
}, },
"dependencies": { "dependencies": {
"@add-ideas/toolbox-contract": "^0.2.2",
"@add-ideas/toolbox-shell-react": "^0.2.2",
"fflate": "^0.8.3", "fflate": "^0.8.3",
"pdf-lib": "^1.17.1", "pdf-lib": "^1.17.1",
"pdfjs-dist": "^5.7.284", "pdfjs-dist": "^5.7.284",
@@ -23,6 +32,7 @@
"react-dom": "^19.2.6" "react-dom": "^19.2.6"
}, },
"devDependencies": { "devDependencies": {
"@add-ideas/toolbox-testkit": "^0.2.2",
"@eslint/js": "^10.0.1", "@eslint/js": "^10.0.1",
"@testing-library/react": "^16.3.2", "@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1", "@testing-library/user-event": "^14.6.1",

View File

@@ -3,13 +3,13 @@
"short_name": "PDFTools", "short_name": "PDFTools",
"icons": [ "icons": [
{ {
"src": "/web-app-manifest-192x192.png", "src": "./web-app-manifest-192x192.png",
"sizes": "192x192", "sizes": "192x192",
"type": "image/png", "type": "image/png",
"purpose": "maskable" "purpose": "maskable"
}, },
{ {
"src": "/web-app-manifest-512x512.png", "src": "./web-app-manifest-512x512.png",
"sizes": "512x512", "sizes": "512x512",
"type": "image/png", "type": "image/png",
"purpose": "maskable" "purpose": "maskable"

32
public/toolbox-app.json Normal file
View File

@@ -0,0 +1,32 @@
{
"$schema": "https://git.add-ideas.de/zemion/toolbox-sdk/raw/branch/main/schemas/toolbox-app.v1.schema.json",
"schemaVersion": 1,
"id": "de.add-ideas.pdf-tools",
"name": "PDF Workbench",
"description": "Page-level PDF operations performed locally in the browser.",
"entry": "./",
"icon": "./favicon.svg",
"categories": ["documents", "pdf"],
"tags": ["merge", "split", "rotate", "reorder"],
"integration": {
"contextVersion": 1,
"launchModes": ["navigate", "new-tab"],
"embedding": "unsupported"
},
"requirements": {
"secureContext": true,
"workers": true,
"indexedDb": true,
"crossOriginIsolated": false
},
"privacy": {
"processing": "local",
"fileUploads": false,
"telemetry": false
},
"source": {
"repository": "https://git.add-ideas.de/zemion/pdf-tools",
"license": "AGPL-3.0-only"
},
"version": "0.4.3"
}

View File

@@ -0,0 +1,64 @@
import { readFile, writeFile } from 'node:fs/promises';
import path from 'node:path';
import process from 'node:process';
import { fileURLToPath } from 'node:url';
import { parseToolboxApp } from '@add-ideas/toolbox-contract';
import { format } from 'prettier';
const projectRoot = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
'..'
);
const packageJsonPath = path.join(projectRoot, 'package.json');
const definitionPath = path.join(
projectRoot,
'src',
'toolboxApp.definition.json'
);
const versionSourcePath = path.join(projectRoot, 'src', 'version.ts');
const manifestPath = path.join(projectRoot, 'public', 'toolbox-app.json');
const [packageJson, definition, versionSource] = await Promise.all([
readJson(packageJsonPath),
readJson(definitionPath),
readFile(versionSourcePath, 'utf8'),
]);
const sourceVersion = versionSource.match(
/APP_VERSION\s*=\s*['"]([^'"]+)['"]/
)?.[1];
if (!sourceVersion) {
throw new Error('Could not read APP_VERSION from src/version.ts.');
}
if (sourceVersion !== packageJson.version) {
throw new Error(
`Version mismatch: package.json is ${packageJson.version}, while src/version.ts is ${sourceVersion}.`
);
}
const manifest = {
$schema:
'https://git.add-ideas.de/zemion/toolbox-sdk/raw/branch/main/schemas/toolbox-app.v1.schema.json',
schemaVersion: 1,
...definition,
version: packageJson.version,
};
parseToolboxApp(manifest);
const output = await format(JSON.stringify(manifest), { parser: 'json' });
if (process.argv.includes('--check')) {
const existing = await readFile(manifestPath, 'utf8').catch(() => '');
if (existing !== output) {
throw new Error(
'public/toolbox-app.json is out of date. Run npm run manifest:generate.'
);
}
} else {
await writeFile(manifestPath, output);
}
async function readJson(filePath) {
return JSON.parse(await readFile(filePath, 'utf8'));
}

100
scripts/package-release.mjs Normal file
View File

@@ -0,0 +1,100 @@
import { createHash } from 'node:crypto';
import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import {
collectDirectoryFiles,
createDeterministicZip,
createThirdPartyLicenseBundle,
} from './release-archive.mjs';
const projectRoot = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
'..'
);
const packageJson = JSON.parse(
await readFile(path.join(projectRoot, 'package.json'), 'utf8')
);
const releaseDirectory = path.join(projectRoot, 'release');
const archiveName = `pdf-tools-${packageJson.version}.zip`;
const archivePath = path.join(releaseDirectory, archiveName);
const checksumPath = path.join(
releaseDirectory,
`pdf-tools-${packageJson.version}.sha256`
);
const force = process.argv.slice(2).includes('--force');
const unknownArguments = process.argv
.slice(2)
.filter((argument) => argument !== '--force');
if (unknownArguments.length > 0) {
throw new Error(`Unknown release argument: ${unknownArguments.join(', ')}`);
}
await mkdir(releaseDirectory, { recursive: true });
if (!force) {
await assertDoesNotExist(archivePath);
await assertDoesNotExist(checksumPath);
}
const files = await collectDirectoryFiles(path.join(projectRoot, 'dist'));
files['CHANGELOG.md'] = await readFile(path.join(projectRoot, 'CHANGELOG.md'));
files['SOURCE.md'] = Buffer.from(
[
'# Corresponding source',
'',
`The source code corresponding to PDF Tools ${packageJson.version} is available at:`,
'',
`https://git.add-ideas.de/zemion/pdf-tools/src/tag/v${packageJson.version}`,
'',
'Third-party components remain available under the separate terms reproduced in LICENSES/.',
'',
].join('\n'),
'utf8'
);
files['LICENSES/pdf-tools-LICENSE.txt'] = await readFile(
path.join(projectRoot, 'LICENSE')
);
files['LICENSES/core-js-3.49.0-LICENSE.txt'] = await readFile(
path.join(projectRoot, 'licenses', 'core-js-3.49.0-LICENSE.txt')
);
files['LICENSES/THIRD-PARTY-LICENSES.txt'] =
await createThirdPartyLicenseBundle(projectRoot, [
'@add-ideas/toolbox-contract',
'@add-ideas/toolbox-shell-react',
'@pdf-lib/standard-fonts',
'@pdf-lib/upng',
'fflate',
'pako',
'pdf-lib',
'pdfjs-dist',
'react',
'react-dom',
'scheduler',
'tslib',
]);
const archive = createDeterministicZip(files);
const checksum = createHash('sha256').update(archive).digest('hex');
const writeFlag = force ? 'w' : 'wx';
await writeFile(archivePath, archive, { flag: writeFlag });
await writeFile(checksumPath, `${checksum} ${archiveName}\n`, {
encoding: 'utf8',
flag: writeFlag,
});
console.log(`Created release/${archiveName}`);
console.log(`SHA-256 ${checksum}`);
async function assertDoesNotExist(outputPath) {
try {
await access(outputPath);
} catch (error) {
if (error && typeof error === 'object' && error.code === 'ENOENT') return;
throw error;
}
throw new Error(
`${path.relative(projectRoot, outputPath)} already exists; pass --force to replace this exact version.`
);
}

12
scripts/release-archive.d.ts vendored Normal file
View File

@@ -0,0 +1,12 @@
export function compareCodePoints(left: string, right: string): number;
export function createDeterministicZip(
files: Record<string, Uint8Array>
): Uint8Array;
export function collectDirectoryFiles(
directory: string,
prefix?: string
): Promise<Record<string, Buffer>>;
export function createThirdPartyLicenseBundle(
projectRoot: string,
packageNames: string[]
): Promise<Buffer>;

123
scripts/release-archive.mjs Normal file
View File

@@ -0,0 +1,123 @@
import { readFile, readdir } from 'node:fs/promises';
import path from 'node:path';
import { zipSync } from 'fflate';
const LICENSE_FILE = /^(?:licen[cs]e|copying|notice)(?:[._-].*)?$/iu;
export function compareCodePoints(left, right) {
return left < right ? -1 : left > right ? 1 : 0;
}
export function createDeterministicZip(files) {
const orderedFiles = Object.fromEntries(
Object.entries(files).sort(([left], [right]) =>
compareCodePoints(left, right)
)
);
return zipSync(orderedFiles, {
level: 9,
// ZIP stores a wall-clock DOS timestamp. Constructing the epoch in local
// time keeps its encoded bytes stable in every process timezone.
mtime: new Date(1980, 0, 1, 0, 0, 0),
});
}
export async function collectDirectoryFiles(directory, prefix = '') {
const result = {};
const entries = await readdir(directory, { withFileTypes: true });
entries.sort((left, right) => compareCodePoints(left.name, right.name));
for (const entry of entries) {
const absolutePath = path.join(directory, entry.name);
const relativePath = path.posix.join(prefix, entry.name);
if (entry.isDirectory()) {
Object.assign(
result,
await collectDirectoryFiles(absolutePath, relativePath)
);
} else if (entry.isFile()) {
result[relativePath] = await readFile(absolutePath);
} else {
throw new Error(
`Unsupported filesystem entry in release input: ${relativePath}`
);
}
}
return result;
}
export async function createThirdPartyLicenseBundle(projectRoot, packageNames) {
const sections = [];
for (const packageName of [...new Set(packageNames)].sort(
compareCodePoints
)) {
const packageDirectory = path.join(
projectRoot,
'node_modules',
...packageName.split('/')
);
const packageJson = JSON.parse(
await readFile(path.join(packageDirectory, 'package.json'), 'utf8')
);
const licenseFiles = await collectLicenseFiles(packageDirectory);
if (licenseFiles.length === 0) {
throw new Error(`No license text found for ${packageName}.`);
}
const contents = [];
for (const licenseFile of licenseFiles) {
const text = await readFile(
path.join(packageDirectory, ...licenseFile.split('/')),
'utf8'
);
contents.push(
`--- ${licenseFile} ---\n${text.replaceAll('\r\n', '\n').trim()}\n`
);
}
sections.push(
[
`================================================================================`,
`${packageJson.name ?? packageName}@${packageJson.version ?? 'unknown'}`,
`Declared license: ${String(packageJson.license ?? 'unspecified')}`,
`================================================================================`,
...contents,
].join('\n')
);
}
return Buffer.from(
[
'Third-party software license texts bundled with PDF Tools',
'Generated deterministically from the installed production packages.',
'',
...sections,
'',
].join('\n'),
'utf8'
);
}
async function collectLicenseFiles(directory, prefix = '') {
const matches = [];
const entries = await readdir(directory, { withFileTypes: true });
entries.sort((left, right) => compareCodePoints(left.name, right.name));
for (const entry of entries) {
if (entry.name === 'node_modules') continue;
const relativePath = path.posix.join(prefix, entry.name);
const absolutePath = path.join(directory, entry.name);
if (entry.isDirectory()) {
matches.push(...(await collectLicenseFiles(absolutePath, relativePath)));
} else if (entry.isFile() && LICENSE_FILE.test(entry.name)) {
matches.push(relativePath);
} else if (!entry.isFile()) {
throw new Error(
`Unsupported filesystem entry in license input: ${relativePath}`
);
}
}
return matches;
}

View File

@@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import Layout from './components/Layout'; import { AppShell } from '@add-ideas/toolbox-shell-react';
import FileLoader from './components/FileLoader'; import FileLoader from './components/FileLoader';
import ReorderPanel from './components/ReorderPanel'; import ReorderPanel from './components/ReorderPanel';
import ActionsPanel from './components/ActionsPanel'; import ActionsPanel from './components/ActionsPanel';
@@ -43,6 +43,7 @@ import {
createSplitResultsZip, createSplitResultsZip,
createSplitZipFilename, createSplitZipFilename,
} from './pdf/pdfZipService'; } from './pdf/pdfZipService';
import { toolboxApp } from './toolboxApp';
import { import {
createSelectionPdfName, createSelectionPdfName,
createSelectionWorkspaceName, createSelectionWorkspaceName,
@@ -1139,7 +1140,14 @@ const App: React.FC = () => {
previewVisualIndex >= 0 && previewVisualIndex < pages.length - 1; previewVisualIndex >= 0 && previewVisualIndex < pages.length - 1;
return ( return (
<Layout onOpenHelp={() => setHelpOpen(true)}> <AppShell
app={toolboxApp}
className="pdf-toolbox-shell"
helpAction={{
onClick: () => setHelpOpen(true),
title: 'Open help and keyboard shortcuts (F1 or ?)',
}}
>
<FileLoader pdf={pdf} onFilesLoaded={handleFilesLoaded} /> <FileLoader pdf={pdf} onFilesLoaded={handleFilesLoaded} />
{mergeQueueOpen && mergeQueueItems.length > 0 && ( {mergeQueueOpen && mergeQueueItems.length > 0 && (
@@ -1222,7 +1230,11 @@ const App: React.FC = () => {
{error && ( {error && (
<div <div
className="card" className="card"
style={{ border: '1px solid #fecaca', background: '#fef2f2' }} style={{
border: '1px solid var(--pdf-danger-border)',
background: 'var(--pdf-danger-surface)',
color: 'var(--pdf-danger-text)',
}}
> >
<strong>Error:</strong> {error} <strong>Error:</strong> {error}
</div> </div>
@@ -1252,7 +1264,7 @@ const App: React.FC = () => {
</ActionDialog> </ActionDialog>
<HelpDialog open={helpOpen} onClose={() => setHelpOpen(false)} /> <HelpDialog open={helpOpen} onClose={() => setHelpOpen(false)} />
</Layout> </AppShell>
); );
}; };

View File

@@ -21,18 +21,18 @@ const backgroundByVariant: Record<
NonNullable<ActionDialogAction['variant']>, NonNullable<ActionDialogAction['variant']>,
string string
> = { > = {
primary: '#2563eb', primary: 'var(--pdf-primary)',
secondary: '#e5e7eb', secondary: 'var(--pdf-control)',
danger: '#dc2626', danger: 'var(--pdf-danger-text)',
}; };
const colorByVariant: Record< const colorByVariant: Record<
NonNullable<ActionDialogAction['variant']>, NonNullable<ActionDialogAction['variant']>,
string string
> = { > = {
primary: 'white', primary: 'var(--pdf-on-primary)',
secondary: '#111827', secondary: 'var(--pdf-control-text)',
danger: 'white', danger: 'var(--pdf-on-primary)',
}; };
const ActionDialog: React.FC<ActionDialogProps> = ({ const ActionDialog: React.FC<ActionDialogProps> = ({
@@ -75,7 +75,7 @@ const ActionDialog: React.FC<ActionDialogProps> = ({
position: 'fixed', position: 'fixed',
inset: 0, inset: 0,
zIndex: 70, zIndex: 70,
background: 'rgba(15, 23, 42, 0.55)', background: 'var(--pdf-overlay)',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
@@ -86,9 +86,10 @@ const ActionDialog: React.FC<ActionDialogProps> = ({
style={{ style={{
width: '100%', width: '100%',
maxWidth: '440px', maxWidth: '440px',
background: 'white', background: 'var(--pdf-surface-raised)',
color: 'var(--pdf-text)',
borderRadius: '0.75rem', borderRadius: '0.75rem',
boxShadow: '0 20px 40px rgba(15, 23, 42, 0.35)', boxShadow: 'var(--pdf-dialog-shadow)',
padding: '1rem', padding: '1rem',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
@@ -121,8 +122,8 @@ const ActionDialog: React.FC<ActionDialogProps> = ({
borderRadius: '999px', borderRadius: '999px',
width: '1.8rem', width: '1.8rem',
height: '1.8rem', height: '1.8rem',
background: '#e5e7eb', background: 'var(--pdf-control)',
color: '#111827', color: 'var(--pdf-control-text)',
cursor: 'pointer', cursor: 'pointer',
fontSize: '1.1rem', fontSize: '1.1rem',
lineHeight: 1, lineHeight: 1,
@@ -139,7 +140,7 @@ const ActionDialog: React.FC<ActionDialogProps> = ({
<div <div
style={{ style={{
fontSize: '0.9rem', fontSize: '0.9rem',
color: '#4b5563', color: 'var(--pdf-muted)',
lineHeight: 1.45, lineHeight: 1.45,
}} }}
> >
@@ -173,9 +174,11 @@ const ActionDialog: React.FC<ActionDialogProps> = ({
borderRadius: '0.5rem', borderRadius: '0.5rem',
padding: '0.45rem 0.8rem', padding: '0.45rem 0.8rem',
background: action.disabled background: action.disabled
? '#e5e7eb' ? 'var(--pdf-control)'
: backgroundByVariant[variant], : backgroundByVariant[variant],
color: action.disabled ? '#6b7280' : colorByVariant[variant], color: action.disabled
? 'var(--pdf-muted-soft)'
: colorByVariant[variant],
cursor: action.disabled ? 'default' : 'pointer', cursor: action.disabled ? 'default' : 'pointer',
fontSize: '0.9rem', fontSize: '0.9rem',
}} }}

View File

@@ -49,7 +49,7 @@ const ActionsPanel: React.FC<ActionsPanelProps> = ({
return ( return (
<div className="card"> <div className="card">
<h2>Tools</h2> <h2>Tools</h2>
<p style={{ fontSize: '0.85rem', color: '#6b7280' }}> <p style={{ fontSize: '0.85rem', color: 'var(--pdf-muted-soft)' }}>
Use these tools on the current in-memory document (reordered, rotated, Use these tools on the current in-memory document (reordered, rotated,
with deletions). Nothing is uploaded to a server. with deletions). Nothing is uploaded to a server.
</p> </p>

View File

@@ -30,7 +30,13 @@ const FileLoader: React.FC<FileLoaderProps> = ({ pdf, onFilesLoaded }) => {
/> />
{pdf && ( {pdf && (
<div style={{ marginTop: '0.75rem', fontSize: '0.9rem' }}> <div
style={{
marginTop: '0.75rem',
fontSize: '0.9rem',
color: 'var(--pdf-muted)',
}}
>
<div> <div>
<strong>Loaded:</strong> {pdf.name} <strong>Loaded:</strong> {pdf.name}
</div> </div>

View File

@@ -1,47 +0,0 @@
import React from 'react';
import { APP_VERSION } from '../version';
interface LayoutProps {
children: React.ReactNode;
onOpenHelp?: () => void;
}
const Layout: React.FC<LayoutProps> = ({ children, onOpenHelp }) => {
return (
<div className="app-root">
<header className="app-header">
<div className="app-header-content">
<div className="app-header-title">
<span className="app-logo">📄</span>
<div>
<h1>PDF Workbench</h1>
<small>All in your browser</small>
</div>
</div>
<div className="app-header-actions">
{onOpenHelp && (
<button
type="button"
className="app-help-button"
onClick={onOpenHelp}
aria-haspopup="dialog"
title="Open help and keyboard shortcuts (F1 or ?)"
>
Help <span aria-hidden="true">?</span>
</button>
)}
<div className="app-version" title={`Version ${APP_VERSION}`}>
v{APP_VERSION}
</div>
</div>
</div>
</header>
<main className="app-main">{children}</main>
</div>
);
};
export default Layout;

View File

@@ -54,22 +54,25 @@ const MergeQueuePanel: React.FC<MergeQueuePanelProps> = ({
return ( return (
<div <div
className="card merge-queue-card" className="card merge-queue-card"
style={{ border: '1px solid #bfdbfe', background: '#eff6ff' }} style={{
border: '1px solid var(--pdf-info-border)',
background: 'var(--pdf-info-surface)',
}}
> >
<h2>Merge PDF queue</h2> <h2>Merge PDF queue</h2>
<p style={{ fontSize: '0.85rem', color: '#374151' }}> <p style={{ fontSize: '0.85rem', color: 'var(--pdf-info-text)' }}>
Add several PDFs, reorder the queue, then merge them into a new unsaved Add several PDFs, reorder the queue, then merge them into a new unsaved
workspace. Processing still happens entirely in your browser. workspace. Processing still happens entirely in your browser.
</p> </p>
{hasCurrentPdf ? ( {hasCurrentPdf ? (
<p style={{ fontSize: '0.85rem', color: '#374151' }}> <p style={{ fontSize: '0.85rem', color: 'var(--pdf-info-text)' }}>
Current workspace:{' '} Current workspace:{' '}
<strong>{currentPdfName ?? 'Untitled document'}</strong> with{' '} <strong>{currentPdfName ?? 'Untitled document'}</strong> with{' '}
{currentPageCount} {currentPageCount === 1 ? 'page' : 'pages'}. {currentPageCount} {currentPageCount === 1 ? 'page' : 'pages'}.
</p> </p>
) : ( ) : (
<p style={{ fontSize: '0.85rem', color: '#374151' }}> <p style={{ fontSize: '0.85rem', color: 'var(--pdf-info-text)' }}>
No current workspace is open. The queue will become a new workspace. No current workspace is open. The queue will become a new workspace.
</p> </p>
)} )}
@@ -122,7 +125,7 @@ const MergeQueuePanel: React.FC<MergeQueuePanelProps> = ({
))} ))}
</div> </div>
<p style={{ fontSize: '0.85rem', color: '#374151' }}> <p style={{ fontSize: '0.85rem', color: 'var(--pdf-info-text)' }}>
Queue total:{' '} Queue total:{' '}
<strong> <strong>
{items.length} {items.length === 1 ? 'PDF' : 'PDFs'} {items.length} {items.length === 1 ? 'PDF' : 'PDFs'}
@@ -186,7 +189,7 @@ const MergeQueuePanel: React.FC<MergeQueuePanelProps> = ({
fontSize: '0.85rem', fontSize: '0.85rem',
}} }}
/>{' '} />{' '}
<span style={{ color: '#6b7280' }}> <span style={{ color: 'var(--pdf-muted-soft)' }}>
(1 = before first page, {currentPageCount + 1} = after last page) (1 = before first page, {currentPageCount + 1} = after last page)
</span> </span>
</span> </span>

View File

@@ -1,11 +1,6 @@
import React, { useEffect, useRef } from 'react'; import React, { useEffect, useRef } from 'react';
import type { PdfFile } from '../pdf/pdfTypes'; import type { PdfFile } from '../pdf/pdfTypes';
import * as pdfjsLib from 'pdfjs-dist'; import { pdfjsLib } from '../pdf/pdfJs';
import pdfjsWorker from 'pdfjs-dist/build/pdf.worker?worker&url';
// pdf.js worker setup
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(pdfjsLib as any).GlobalWorkerOptions.workerSrc = pdfjsWorker;
interface PagePreviewModalProps { interface PagePreviewModalProps {
isOpen: boolean; isOpen: boolean;

View File

@@ -48,7 +48,7 @@ const CopyPagesDialog: React.FC<CopyPagesDialogProps> = ({
position: 'fixed', position: 'fixed',
inset: 0, inset: 0,
zIndex: 60, zIndex: 60,
background: 'rgba(15, 23, 42, 0.55)', background: 'var(--pdf-overlay)',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
@@ -60,9 +60,10 @@ const CopyPagesDialog: React.FC<CopyPagesDialogProps> = ({
style={{ style={{
width: '100%', width: '100%',
maxWidth: '420px', maxWidth: '420px',
background: 'white', background: 'var(--pdf-surface-raised)',
color: 'var(--pdf-text)',
borderRadius: '0.75rem', borderRadius: '0.75rem',
boxShadow: '0 20px 40px rgba(15, 23, 42, 0.35)', boxShadow: 'var(--pdf-dialog-shadow)',
padding: '1rem', padding: '1rem',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
@@ -95,8 +96,8 @@ const CopyPagesDialog: React.FC<CopyPagesDialogProps> = ({
borderRadius: '999px', borderRadius: '999px',
width: '1.8rem', width: '1.8rem',
height: '1.8rem', height: '1.8rem',
background: '#e5e7eb', background: 'var(--pdf-control)',
color: '#111827', color: 'var(--pdf-control-text)',
cursor: 'pointer', cursor: 'pointer',
fontSize: '1.1rem', fontSize: '1.1rem',
lineHeight: 1, lineHeight: 1,
@@ -114,7 +115,7 @@ const CopyPagesDialog: React.FC<CopyPagesDialogProps> = ({
style={{ style={{
margin: 0, margin: 0,
fontSize: '0.9rem', fontSize: '0.9rem',
color: '#4b5563', color: 'var(--pdf-muted)',
}} }}
> >
Copy{' '} Copy{' '}
@@ -145,7 +146,7 @@ const CopyPagesDialog: React.FC<CopyPagesDialogProps> = ({
style={{ style={{
padding: '0.45rem 0.55rem', padding: '0.45rem 0.55rem',
borderRadius: '0.5rem', borderRadius: '0.5rem',
border: '1px solid #d1d5db', border: '1px solid var(--pdf-border-strong)',
fontSize: '0.95rem', fontSize: '0.95rem',
}} }}
/> />
@@ -154,7 +155,7 @@ const CopyPagesDialog: React.FC<CopyPagesDialogProps> = ({
<div <div
style={{ style={{
fontSize: '0.8rem', fontSize: '0.8rem',
color: '#6b7280', color: 'var(--pdf-muted-soft)',
lineHeight: 1.4, lineHeight: 1.4,
}} }}
> >
@@ -166,9 +167,9 @@ const CopyPagesDialog: React.FC<CopyPagesDialogProps> = ({
<div <div
style={{ style={{
borderRadius: '0.5rem', borderRadius: '0.5rem',
background: '#fef2f2', background: 'var(--pdf-danger-surface)',
border: '1px solid #fecaca', border: '1px solid var(--pdf-danger-border)',
color: '#b91c1c', color: 'var(--pdf-danger-text)',
padding: '0.5rem', padding: '0.5rem',
fontSize: '0.85rem', fontSize: '0.85rem',
}} }}
@@ -192,8 +193,8 @@ const CopyPagesDialog: React.FC<CopyPagesDialogProps> = ({
border: 'none', border: 'none',
borderRadius: '0.5rem', borderRadius: '0.5rem',
padding: '0.45rem 0.8rem', padding: '0.45rem 0.8rem',
background: '#e5e7eb', background: 'var(--pdf-control)',
color: '#111827', color: 'var(--pdf-control-text)',
cursor: 'pointer', cursor: 'pointer',
fontSize: '0.9rem', fontSize: '0.9rem',
}} }}
@@ -207,8 +208,8 @@ const CopyPagesDialog: React.FC<CopyPagesDialogProps> = ({
border: 'none', border: 'none',
borderRadius: '0.5rem', borderRadius: '0.5rem',
padding: '0.45rem 0.8rem', padding: '0.45rem 0.8rem',
background: '#16a34a', background: 'var(--pdf-success-text)',
color: 'white', color: 'var(--pdf-on-primary)',
cursor: 'pointer', cursor: 'pointer',
fontSize: '0.9rem', fontSize: '0.9rem',
}} }}

View File

@@ -53,11 +53,11 @@ const PageCard: React.FC<PageCardProps> = ({
}) => { }) => {
const background = isDraggingCard const background = isDraggingCard
? isCopyDragging ? isCopyDragging
? '#dcfce7' ? 'var(--pdf-success-soft)'
: '#dbeafe' : 'var(--pdf-info-surface)'
: selected : selected
? '#eff6ff' ? 'var(--pdf-info-surface)'
: '#f9fafb'; : 'var(--pdf-surface-soft)';
return ( return (
<div <div
@@ -71,7 +71,7 @@ const PageCard: React.FC<PageCardProps> = ({
width: '162px', width: '162px',
padding: '0.4rem', padding: '0.4rem',
borderRadius: '0.5rem', borderRadius: '0.5rem',
border: '1px solid #e5e7eb', border: '1px solid var(--pdf-border)',
background, background,
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
@@ -91,9 +91,11 @@ const PageCard: React.FC<PageCardProps> = ({
width: '20px', width: '20px',
height: '20px', height: '20px',
borderRadius: '0.4rem', borderRadius: '0.4rem',
border: '1px solid #9ca3af', border: '1px solid var(--pdf-border-strong)',
background: selected ? '#2563eb' : 'rgba(255,255,255,0.9)', background: selected
color: selected ? 'white' : 'transparent', ? 'var(--pdf-primary)'
: 'color-mix(in srgb, var(--pdf-surface) 92%, transparent)',
color: selected ? 'var(--pdf-on-primary)' : 'transparent',
fontSize: '0.8rem', fontSize: '0.8rem',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
@@ -131,8 +133,8 @@ const PageCard: React.FC<PageCardProps> = ({
height: 'auto', height: 'auto',
objectFit: 'contain', objectFit: 'contain',
borderRadius: '0.25rem', borderRadius: '0.25rem',
border: '1px solid #e5e7eb', border: '1px solid var(--pdf-paper-border)',
background: 'white', background: 'var(--pdf-paper)',
}} }}
/> />
) : ( ) : (
@@ -141,8 +143,8 @@ const PageCard: React.FC<PageCardProps> = ({
width: '60px', width: '60px',
height: '80px', height: '80px',
borderRadius: '0.25rem', borderRadius: '0.25rem',
border: '1px dashed #d1d5db', border: '1px dashed var(--pdf-paper-border)',
background: '#f3f4f6', background: 'var(--pdf-paper-placeholder)',
}} }}
/> />
)} )}
@@ -151,7 +153,7 @@ const PageCard: React.FC<PageCardProps> = ({
<span style={{ fontSize: '0.8rem' }}> <span style={{ fontSize: '0.8rem' }}>
Page {page.sourcePageIndex + 1} Page {page.sourcePageIndex + 1}
</span> </span>
<span style={{ fontSize: '0.7rem', color: '#6b7280' }}> <span style={{ fontSize: '0.7rem', color: 'var(--pdf-muted-soft)' }}>
Pos {visualIndex + 1} · Rot {page.rotation}° Pos {visualIndex + 1} · Rot {page.rotation}°
</span> </span>
@@ -170,7 +172,8 @@ const PageCard: React.FC<PageCardProps> = ({
}} }}
style={{ style={{
...pageActionButtonStyle, ...pageActionButtonStyle,
background: '#e5e7eb', background: 'var(--pdf-control)',
color: 'var(--pdf-control-text)',
}} }}
> >
90° 90°
@@ -184,7 +187,8 @@ const PageCard: React.FC<PageCardProps> = ({
}} }}
style={{ style={{
...pageActionButtonStyle, ...pageActionButtonStyle,
background: '#e5e7eb', background: 'var(--pdf-control)',
color: 'var(--pdf-control-text)',
}} }}
> >
90° 90°
@@ -198,8 +202,8 @@ const PageCard: React.FC<PageCardProps> = ({
}} }}
style={{ style={{
...pageActionButtonStyle, ...pageActionButtonStyle,
background: '#fecaca', background: 'var(--pdf-danger-soft)',
color: '#b91c1c', color: 'var(--pdf-danger-text)',
}} }}
title="Remove this page from the exported PDF" title="Remove this page from the exported PDF"
> >

View File

@@ -53,8 +53,8 @@ const PageSelectionToolbar: React.FC<PageSelectionToolbarProps> = ({
disabled={!hasSelection} disabled={!hasSelection}
style={{ style={{
...pillButtonStyle, ...pillButtonStyle,
background: '#dcfce7', background: 'var(--pdf-success-soft)',
color: '#166534', color: 'var(--pdf-success-text)',
cursor: 'pointer', cursor: 'pointer',
}} }}
title="Copy selected pages to another position" title="Copy selected pages to another position"
@@ -69,8 +69,8 @@ const PageSelectionToolbar: React.FC<PageSelectionToolbarProps> = ({
onClick={onDeleteSelected} onClick={onDeleteSelected}
style={{ style={{
...pillButtonStyle, ...pillButtonStyle,
background: '#fee2e2', background: 'var(--pdf-danger-soft)',
color: '#b91c1c', color: 'var(--pdf-danger-text)',
cursor: 'pointer', cursor: 'pointer',
}} }}
> >
@@ -83,8 +83,8 @@ const PageSelectionToolbar: React.FC<PageSelectionToolbarProps> = ({
onClick={onSelectAll} onClick={onSelectAll}
style={{ style={{
...pillButtonStyle, ...pillButtonStyle,
background: '#8dcd8d', background: 'var(--pdf-success-border)',
color: '#111827', color: 'var(--pdf-control-text)',
cursor: 'pointer', cursor: 'pointer',
}} }}
> >
@@ -97,8 +97,10 @@ const PageSelectionToolbar: React.FC<PageSelectionToolbarProps> = ({
disabled={!hasSelection} disabled={!hasSelection}
style={{ style={{
...pillButtonStyle, ...pillButtonStyle,
background: '#e5e7eb', background: 'var(--pdf-control)',
color: hasSelection ? '#111827' : '#6b7280', color: hasSelection
? 'var(--pdf-control-text)'
: 'var(--pdf-muted-soft)',
cursor: hasSelection ? 'pointer' : 'default', cursor: hasSelection ? 'pointer' : 'default',
}} }}
> >

View File

@@ -279,13 +279,15 @@ const ReorderPanel: React.FC<ReorderPanelProps> = ({
draggingPage != null && draggingPage != null &&
selectedPageIds.length > 0 && selectedPageIds.length > 0 &&
selectedPageIds.includes(draggingPage.id); selectedPageIds.includes(draggingPage.id);
const dropIndicatorColor = isCopyDragging ? '#16a34a' : '#2563eb'; const dropIndicatorColor = isCopyDragging
? 'var(--pdf-success-text)'
: 'var(--pdf-primary)';
return ( return (
<> <>
<div className="card"> <div className="card">
<h2>Pages</h2> <h2>Pages</h2>
<p style={{ fontSize: '0.85rem', color: '#6b7280' }}> <p style={{ fontSize: '0.85rem', color: 'var(--pdf-muted-soft)' }}>
Tap/click a page to preview it. Use the checkbox to select pages Tap/click a page to preview it. Use the checkbox to select pages
(Shift for ranges). Drag to reorder; dragging a selected page moves (Shift for ranges). Drag to reorder; dragging a selected page moves
the whole selection. Hold Ctrl/ while dropping to copy instead of the whole selection. Hold Ctrl/ while dropping to copy instead of

View File

@@ -54,7 +54,7 @@ const WorkspacePanel: React.FC<WorkspacePanelProps> = ({
<div className="card"> <div className="card">
<h2>Workspace</h2> <h2>Workspace</h2>
<p style={{ fontSize: '0.85rem', color: '#6b7280' }}> <p style={{ fontSize: '0.85rem', color: 'var(--pdf-muted-soft)' }}>
Save named workspaces in this browser. PDF binaries are stored in Save named workspaces in this browser. PDF binaries are stored in
IndexedDB; nothing is uploaded. IndexedDB; nothing is uploaded.
</p> </p>
@@ -78,7 +78,7 @@ const WorkspacePanel: React.FC<WorkspacePanelProps> = ({
minWidth: 0, minWidth: 0,
padding: '0.45rem 0.55rem', padding: '0.45rem 0.55rem',
borderRadius: '0.5rem', borderRadius: '0.5rem',
border: '1px solid #d1d5db', border: '1px solid var(--pdf-border-strong)',
fontSize: '0.9rem', fontSize: '0.9rem',
}} }}
/> />
@@ -140,7 +140,7 @@ const WorkspacePanel: React.FC<WorkspacePanelProps> = ({
style={{ style={{
marginTop: '0.5rem', marginTop: '0.5rem',
fontSize: '0.8rem', fontSize: '0.8rem',
color: '#92400e', color: 'var(--pdf-warning-text)',
}} }}
> >
Unsaved workspace changes. Unsaved workspace changes.
@@ -152,7 +152,7 @@ const WorkspacePanel: React.FC<WorkspacePanelProps> = ({
style={{ style={{
marginTop: '0.5rem', marginTop: '0.5rem',
fontSize: '0.85rem', fontSize: '0.85rem',
color: '#166534', color: 'var(--pdf-success-text)',
}} }}
> >
{workspaceMessage} {workspaceMessage}
@@ -178,10 +178,12 @@ const WorkspacePanel: React.FC<WorkspacePanelProps> = ({
<div <div
key={workspace.id} key={workspace.id}
style={{ style={{
border: '1px solid #e5e7eb', border: '1px solid var(--pdf-border)',
borderRadius: '0.5rem', borderRadius: '0.5rem',
padding: '0.5rem', padding: '0.5rem',
background: active ? '#eff6ff' : '#f9fafb', background: active
? 'var(--pdf-info-surface)'
: 'var(--pdf-surface-soft)',
display: 'flex', display: 'flex',
justifyContent: 'space-between', justifyContent: 'space-between',
gap: '0.75rem', gap: '0.75rem',
@@ -193,11 +195,19 @@ const WorkspacePanel: React.FC<WorkspacePanelProps> = ({
<div style={{ fontSize: '0.9rem' }}> <div style={{ fontSize: '0.9rem' }}>
<strong>{workspace.name}</strong> <strong>{workspace.name}</strong>
{active && ( {active && (
<span style={{ color: '#2563eb' }}> · active</span> <span style={{ color: 'var(--pdf-primary)' }}>
{' '}
· active
</span>
)} )}
</div> </div>
<div style={{ fontSize: '0.75rem', color: '#6b7280' }}> <div
style={{
fontSize: '0.75rem',
color: 'var(--pdf-muted-soft)',
}}
>
{workspace.pdfName} · source pages:{' '} {workspace.pdfName} · source pages:{' '}
{workspace.sourcePageCount} · workspace pages:{' '} {workspace.sourcePageCount} · workspace pages:{' '}
{workspace.workspacePageCount} · undo:{' '} {workspace.workspacePageCount} · undo:{' '}
@@ -228,8 +238,8 @@ const WorkspacePanel: React.FC<WorkspacePanelProps> = ({
disabled={isBusy} disabled={isBusy}
onClick={() => onDeleteWorkspace(workspace.id)} onClick={() => onDeleteWorkspace(workspace.id)}
style={{ style={{
background: '#fee2e2', background: 'var(--pdf-danger-soft)',
color: '#991b1b', color: 'var(--pdf-danger-text)',
}} }}
> >
Delete Delete
@@ -261,8 +271,8 @@ const WorkspacePanel: React.FC<WorkspacePanelProps> = ({
key={entry.id} key={entry.id}
style={{ style={{
fontSize: '0.8rem', fontSize: '0.8rem',
color: '#374151', color: 'var(--pdf-muted)',
borderLeft: '3px solid #2563eb', borderLeft: '3px solid var(--pdf-primary)',
paddingLeft: '0.45rem', paddingLeft: '0.45rem',
paddingTop: '0.2rem', paddingTop: '0.2rem',
paddingBottom: '0.2rem', paddingBottom: '0.2rem',
@@ -272,7 +282,7 @@ const WorkspacePanel: React.FC<WorkspacePanelProps> = ({
Undo {history.length - index}. {entry.label} Undo {history.length - index}. {entry.label}
</strong> </strong>
<br /> <br />
<span style={{ color: '#6b7280' }}> <span style={{ color: 'var(--pdf-muted-soft)' }}>
{new Date(entry.timestamp).toLocaleString()} {new Date(entry.timestamp).toLocaleString()}
</span> </span>
</div> </div>
@@ -282,12 +292,12 @@ const WorkspacePanel: React.FC<WorkspacePanelProps> = ({
style={{ style={{
margin: '0.25rem 0', margin: '0.25rem 0',
borderRadius: '999px', borderRadius: '999px',
background: '#ecfdf5', background: 'var(--pdf-success-surface)',
color: '#166534', color: 'var(--pdf-success-text)',
fontSize: '0.8rem', fontSize: '0.8rem',
fontWeight: 600, fontWeight: 600,
alignSelf: 'flex-start', alignSelf: 'flex-start',
border: '2px solid #166534', border: '2px solid var(--pdf-success-text)',
width: '100%', width: '100%',
}} }}
></div> ></div>
@@ -300,8 +310,8 @@ const WorkspacePanel: React.FC<WorkspacePanelProps> = ({
key={entry.id} key={entry.id}
style={{ style={{
fontSize: '0.8rem', fontSize: '0.8rem',
color: '#9ca3af', color: 'var(--pdf-muted-faint)',
borderLeft: '3px solid #d1d5db', borderLeft: '3px solid var(--pdf-border-strong)',
paddingLeft: '0.45rem', paddingLeft: '0.45rem',
paddingTop: '0.2rem', paddingTop: '0.2rem',
paddingBottom: '0.2rem', paddingBottom: '0.2rem',
@@ -312,7 +322,7 @@ const WorkspacePanel: React.FC<WorkspacePanelProps> = ({
Redo {index + 1}. {entry.label} Redo {index + 1}. {entry.label}
</strong> </strong>
<br /> <br />
<span style={{ color: '#9ca3af' }}> <span style={{ color: 'var(--pdf-muted-faint)' }}>
{new Date(entry.timestamp).toLocaleString()} {new Date(entry.timestamp).toLocaleString()}
</span> </span>
</div> </div>

View File

@@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom/client'; import ReactDOM from 'react-dom/client';
import App from './App'; import App from './App';
import '@add-ideas/toolbox-shell-react/styles.css';
import './styles.css'; import './styles.css';
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(

9
src/pdf/pdfJs.ts Normal file
View File

@@ -0,0 +1,9 @@
import './uint8ArrayToHexPolyfill';
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.mjs';
import pdfjsWorker from './pdfjsWorker?worker&url';
// pdf.js worker setup for Vite.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(pdfjsLib as any).GlobalWorkerOptions.workerSrc = pdfjsWorker;
export { pdfjsLib };

View File

@@ -1,9 +1,4 @@
import * as pdfjsLib from 'pdfjs-dist'; import { pdfjsLib } from './pdfJs';
import pdfjsWorker from 'pdfjs-dist/build/pdf.worker?worker&url';
// pdf.js worker setup for Vite
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(pdfjsLib as any).GlobalWorkerOptions.workerSrc = pdfjsWorker;
type RotationsMap = Record<number, number>; // key: 0-based page index, value: degrees type RotationsMap = Record<number, number>; // key: 0-based page index, value: degrees

2
src/pdf/pdfjsWorker.ts Normal file
View File

@@ -0,0 +1,2 @@
import './uint8ArrayToHexPolyfill';
import 'pdfjs-dist/legacy/build/pdf.worker.mjs';

View File

@@ -0,0 +1,22 @@
type Uint8ArrayWithToHex = Uint8Array & {
toHex?: () => string;
};
const uint8ArrayPrototype = Uint8Array.prototype as Uint8ArrayWithToHex;
if (typeof uint8ArrayPrototype.toHex !== 'function') {
Object.defineProperty(Uint8Array.prototype, 'toHex', {
value: function toHex(this: Uint8Array): string {
const hex = new Array<string>(this.length);
for (let index = 0; index < this.length; index += 1) {
const value = this[index].toString(16);
hex[index] = value.length === 1 ? `0${value}` : value;
}
return hex.join('');
},
writable: true,
configurable: true,
});
}

View File

@@ -12,89 +12,30 @@ body {
BlinkMacSystemFont, BlinkMacSystemFont,
'Segoe UI', 'Segoe UI',
sans-serif; sans-serif;
background-color: #f3f4f6; background-color: var(--toolbox-background);
color: #111827; color: var(--toolbox-text);
} }
#root { #root {
min-height: 100vh; min-height: 100vh;
} }
.app-shell { .pdf-toolbox-shell .toolbox-shell__main .card {
display: flex; border: 1px solid var(--pdf-border);
min-height: 100vh; background: var(--pdf-surface);
}
.app-sidebar {
width: 260px;
background: #111827;
color: #e5e7eb;
padding: 1rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.app-sidebar h1 {
font-size: 1.1rem;
margin: 0;
}
.app-sidebar small {
color: #9ca3af;
}
.app-nav {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.app-nav button {
width: 100%;
text-align: left;
padding: 0.4rem 0.6rem;
border-radius: 0.375rem;
border: none;
background: transparent;
color: #d1d5db;
cursor: pointer;
font-size: 0.9rem;
}
.app-nav button.active {
background: #374151;
color: #f9fafb;
}
.app-nav button:disabled {
opacity: 0.5;
cursor: default;
}
.app-main {
flex: 1;
padding: 1rem 1.5rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.card {
background: white;
border-radius: 0.75rem; border-radius: 0.75rem;
padding: 1rem; padding: 1rem;
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.12); box-shadow: var(--pdf-card-shadow);
} }
.card h2 { .pdf-toolbox-shell .toolbox-shell__main .card h2 {
margin-top: 0; margin-top: 0;
font-size: 1rem; font-size: 1rem;
} }
button.primary { .pdf-toolbox-shell .toolbox-shell__main button.primary {
background: #2563eb; background: var(--pdf-primary);
color: white; color: var(--pdf-on-primary);
border-radius: 0.5rem; border-radius: 0.5rem;
padding: 0.45rem 0.9rem; padding: 0.45rem 0.9rem;
border: none; border: none;
@@ -102,14 +43,14 @@ button.primary {
font-size: 0.9rem; font-size: 0.9rem;
} }
button.primary:disabled { .pdf-toolbox-shell .toolbox-shell__main button.primary:disabled {
opacity: 0.6; opacity: 0.6;
cursor: default; cursor: default;
} }
button.secondary { .pdf-toolbox-shell .toolbox-shell__main button.secondary {
background: #e5e7eb; background: var(--pdf-control);
color: #111827; color: var(--pdf-control-text);
border-radius: 0.5rem; border-radius: 0.5rem;
padding: 0.45rem 0.9rem; padding: 0.45rem 0.9rem;
border: none; border: none;
@@ -117,121 +58,175 @@ button.secondary {
font-size: 0.9rem; font-size: 0.9rem;
} }
.button-row { .pdf-toolbox-shell .toolbox-shell__main .button-row {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 0.5rem; gap: 0.5rem;
margin-top: 0.5rem; margin-top: 0.5rem;
} }
.app-root { .pdf-toolbox-shell {
min-height: 100vh; --pdf-surface: var(--toolbox-surface);
background-color: #f3f4f6; --pdf-surface-soft: #f9fafb;
--pdf-surface-raised: #ffffff;
--pdf-text: var(--toolbox-text);
--pdf-muted: #4b5563;
--pdf-muted-soft: #6b7280;
--pdf-muted-faint: #9ca3af;
--pdf-border: #e5e7eb;
--pdf-border-strong: #d1d5db;
--pdf-primary: #2563eb;
--pdf-on-primary: #ffffff;
--pdf-control: #e5e7eb;
--pdf-control-text: #111827;
--pdf-info-surface: #eff6ff;
--pdf-info-border: #bfdbfe;
--pdf-info-text: #1e3a8a;
--pdf-success-surface: #ecfdf5;
--pdf-success-soft: #dcfce7;
--pdf-success-border: #86efac;
--pdf-success-text: #166534;
--pdf-warning-surface: #fff7ed;
--pdf-warning-border: #fed7aa;
--pdf-warning-text: #9a3412;
--pdf-danger-surface: #fef2f2;
--pdf-danger-soft: #fee2e2;
--pdf-danger-border: #fecaca;
--pdf-danger-text: #b91c1c;
--pdf-overlay: rgba(15, 23, 42, 0.62);
--pdf-dialog-shadow: 0 20px 40px rgba(15, 23, 42, 0.35);
--pdf-card-shadow: 0 1px 3px rgba(15, 23, 42, 0.12);
--pdf-paper: #ffffff;
--pdf-paper-placeholder: #f3f4f6;
--pdf-paper-border: #d1d5db;
--pdf-paper-text: #111827;
} }
.app-header { .pdf-toolbox-shell[data-toolbox-theme='dark'] {
position: sticky; --pdf-surface-soft: #20283a;
top: 0; --pdf-surface-raised: #1b2334;
z-index: 10; --pdf-muted: #b6bfd2;
background: #111827; --pdf-muted-soft: #a9b3ca;
color: #e5e7eb; --pdf-muted-faint: #8792aa;
padding: 0.6rem 1rem; --pdf-border: #303a50;
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.4); --pdf-border-strong: #465169;
--pdf-primary: #8798f5;
--pdf-on-primary: #101728;
--pdf-control: #293247;
--pdf-control-text: #edf1fb;
--pdf-info-surface: #182946;
--pdf-info-border: #345a8c;
--pdf-info-text: #c6dcff;
--pdf-success-surface: #12382e;
--pdf-success-soft: #174436;
--pdf-success-border: #33765d;
--pdf-success-text: #a7e8cf;
--pdf-warning-surface: #402814;
--pdf-warning-border: #80522d;
--pdf-warning-text: #ffd2a6;
--pdf-danger-surface: #421f29;
--pdf-danger-soft: #4f2530;
--pdf-danger-border: #81394b;
--pdf-danger-text: #ffc0cc;
--pdf-overlay: rgba(2, 6, 16, 0.76);
--pdf-dialog-shadow: 0 24px 52px rgba(0, 0, 0, 0.5);
--pdf-card-shadow: 0 2px 7px rgba(0, 0, 0, 0.26);
} }
.app-header-title { @media (prefers-color-scheme: dark) {
display: flex; .pdf-toolbox-shell[data-toolbox-theme='system'] {
align-items: center; --pdf-surface-soft: #20283a;
gap: 0.6rem; --pdf-surface-raised: #1b2334;
--pdf-muted: #b6bfd2;
--pdf-muted-soft: #a9b3ca;
--pdf-muted-faint: #8792aa;
--pdf-border: #303a50;
--pdf-border-strong: #465169;
--pdf-primary: #8798f5;
--pdf-on-primary: #101728;
--pdf-control: #293247;
--pdf-control-text: #edf1fb;
--pdf-info-surface: #182946;
--pdf-info-border: #345a8c;
--pdf-info-text: #c6dcff;
--pdf-success-surface: #12382e;
--pdf-success-soft: #174436;
--pdf-success-border: #33765d;
--pdf-success-text: #a7e8cf;
--pdf-warning-surface: #402814;
--pdf-warning-border: #80522d;
--pdf-warning-text: #ffd2a6;
--pdf-danger-surface: #421f29;
--pdf-danger-soft: #4f2530;
--pdf-danger-border: #81394b;
--pdf-danger-text: #ffc0cc;
--pdf-overlay: rgba(2, 6, 16, 0.76);
--pdf-dialog-shadow: 0 24px 52px rgba(0, 0, 0, 0.5);
--pdf-card-shadow: 0 2px 7px rgba(0, 0, 0, 0.26);
}
} }
.app-header h1 { .pdf-toolbox-shell .toolbox-shell__main {
font-size: 1rem;
margin: 0;
}
.app-header small {
color: #9ca3af;
font-size: 0.8rem;
}
.app-logo {
font-size: 1.4rem;
}
.app-main {
padding: 0.75rem; padding: 0.75rem;
max-width: 900px; max-width: 90rem;
margin: 0 auto;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.75rem; gap: 0.75rem;
} }
/* Make cards full-width on mobile */ .pdf-toolbox-shell .toolbox-shell__main input,
.card { .pdf-toolbox-shell .toolbox-shell__main select,
.pdf-toolbox-shell .toolbox-shell__main textarea {
border-color: var(--pdf-border-strong);
background: var(--pdf-surface);
color: var(--pdf-text);
}
.pdf-toolbox-shell .toolbox-shell__main input::placeholder,
.pdf-toolbox-shell .toolbox-shell__main textarea::placeholder {
color: var(--pdf-muted-soft);
}
.pdf-toolbox-shell .toolbox-shell__main a {
color: var(--toolbox-accent);
}
.pdf-toolbox-shell .toolbox-shell__main a:hover {
color: var(--toolbox-accent-hover);
}
.pdf-toolbox-shell .toolbox-shell__main button:disabled {
opacity: 0.58;
}
.pdf-toolbox-shell .toolbox-shell__main button:focus-visible,
.pdf-toolbox-shell .toolbox-shell__main a:focus-visible,
.pdf-toolbox-shell .toolbox-shell__main input:focus-visible,
.pdf-toolbox-shell .toolbox-shell__main select:focus-visible {
outline: 3px solid color-mix(in srgb, var(--toolbox-focus) 72%, transparent);
outline-offset: 2px;
}
.pdf-toolbox-shell .toolbox-shell__main .card {
width: 100%; width: 100%;
} }
.download-link { .pdf-toolbox-shell .toolbox-shell__main .download-link {
display: inline-block; display: inline-block;
margin: 0.15rem 0; margin: 0.15rem 0;
font-size: 0.85rem; font-size: 0.85rem;
} }
.card hr { .pdf-toolbox-shell .toolbox-shell__main .card hr {
border: none; border: none;
border-top: 1px solid #e5e7eb; border-top: 1px solid var(--pdf-border);
}
.app-header-content {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
}
.app-version {
flex: 0 0 auto;
border-radius: 999px;
background: #374151;
color: #d1d5db;
padding: 0.4rem 0.7rem;
font-size: 0.85rem;
font-weight: 500;
line-height: 1;
white-space: nowrap;
}
.app-header-actions {
display: flex;
align-items: center;
gap: 0.5rem;
}
.app-help-button {
border: 1px solid #4b5563;
border-radius: 999px;
background: transparent;
color: #e5e7eb;
padding: 0.35rem 0.65rem;
font-size: 0.85rem;
font-weight: 500;
cursor: pointer;
line-height: 1;
}
.app-help-button:hover,
.app-help-button:focus-visible {
background: #374151;
outline: none;
} }
.help-dialog-backdrop { .help-dialog-backdrop {
position: fixed; position: fixed;
inset: 0; inset: 0;
z-index: 80; z-index: 80;
background: rgba(15, 23, 42, 0.65); background: var(--pdf-overlay);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -242,9 +237,9 @@ button.secondary {
width: min(920px, 100%); width: min(920px, 100%);
max-height: min(88vh, 760px); max-height: min(88vh, 760px);
overflow: auto; overflow: auto;
background: #ffffff; background: var(--pdf-surface-raised);
border-radius: 0.9rem; border-radius: 0.9rem;
box-shadow: 0 24px 60px rgba(15, 23, 42, 0.4); box-shadow: var(--pdf-dialog-shadow);
} }
.help-dialog-header { .help-dialog-header {
@@ -256,8 +251,8 @@ button.secondary {
align-items: flex-start; align-items: flex-start;
gap: 1rem; gap: 1rem;
padding: 1rem; padding: 1rem;
background: #ffffff; background: var(--pdf-surface-raised);
border-bottom: 1px solid #e5e7eb; border-bottom: 1px solid var(--pdf-border);
} }
.help-dialog-header h2 { .help-dialog-header h2 {
@@ -267,7 +262,7 @@ button.secondary {
.help-dialog-header p { .help-dialog-header p {
margin: 0.35rem 0 0; margin: 0.35rem 0 0;
color: #4b5563; color: var(--pdf-muted);
font-size: 0.9rem; font-size: 0.9rem;
line-height: 1.45; line-height: 1.45;
} }
@@ -278,8 +273,8 @@ button.secondary {
border-radius: 999px; border-radius: 999px;
width: 2rem; width: 2rem;
height: 2rem; height: 2rem;
background: #e5e7eb; background: var(--pdf-control);
color: #111827; color: var(--pdf-control-text);
cursor: pointer; cursor: pointer;
font-size: 1.25rem; font-size: 1.25rem;
line-height: 1; line-height: 1;
@@ -293,10 +288,10 @@ button.secondary {
} }
.help-section { .help-section {
border: 1px solid #e5e7eb; border: 1px solid var(--pdf-border);
border-radius: 0.75rem; border-radius: 0.75rem;
padding: 0.9rem; padding: 0.9rem;
background: #f9fafb; background: var(--pdf-surface-soft);
} }
.help-section h3 { .help-section h3 {
@@ -312,8 +307,8 @@ button.secondary {
.help-step { .help-step {
border-radius: 0.65rem; border-radius: 0.65rem;
background: #ffffff; background: var(--pdf-surface);
border: 1px solid #e5e7eb; border: 1px solid var(--pdf-border);
padding: 0.75rem; padding: 0.75rem;
} }
@@ -325,7 +320,7 @@ button.secondary {
.help-step p, .help-step p,
.help-note { .help-note {
margin: 0.35rem 0 0; margin: 0.35rem 0 0;
color: #4b5563; color: var(--pdf-muted);
font-size: 0.85rem; font-size: 0.85rem;
line-height: 1.45; line-height: 1.45;
} }
@@ -341,18 +336,18 @@ button.secondary {
display: inline-flex; display: inline-flex;
justify-content: center; justify-content: center;
border-radius: 0.4rem; border-radius: 0.4rem;
border: 1px solid #d1d5db; border: 1px solid var(--pdf-border-strong);
background: #ffffff; background: var(--pdf-surface);
padding: 0.2rem 0.45rem; padding: 0.2rem 0.45rem;
font-size: 0.78rem; font-size: 0.78rem;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
color: #111827; color: var(--pdf-text);
box-shadow: inset 0 -1px 0 #d1d5db; box-shadow: inset 0 -1px 0 var(--pdf-border-strong);
white-space: nowrap; white-space: nowrap;
} }
.shortcut-grid span { .shortcut-grid span {
color: #4b5563; color: var(--pdf-muted);
font-size: 0.85rem; font-size: 0.85rem;
line-height: 1.35; line-height: 1.35;
} }
@@ -369,8 +364,8 @@ button.secondary {
} }
.help-concepts dl > div { .help-concepts dl > div {
background: #ffffff; background: var(--pdf-surface);
border: 1px solid #e5e7eb; border: 1px solid var(--pdf-border);
border-radius: 0.65rem; border-radius: 0.65rem;
padding: 0.75rem; padding: 0.75rem;
} }
@@ -383,7 +378,7 @@ button.secondary {
.help-concepts dd { .help-concepts dd {
margin: 0; margin: 0;
color: #4b5563; color: var(--pdf-muted);
font-size: 0.82rem; font-size: 0.82rem;
line-height: 1.4; line-height: 1.4;
} }
@@ -393,10 +388,6 @@ button.secondary {
.help-concepts dl { .help-concepts dl {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.app-header-content {
align-items: flex-start;
}
} }
.merge-queue-list { .merge-queue-list {
@@ -411,16 +402,16 @@ button.secondary {
grid-template-columns: auto 1fr auto; grid-template-columns: auto 1fr auto;
align-items: center; align-items: center;
gap: 0.75rem; gap: 0.75rem;
border: 1px solid #bfdbfe; border: 1px solid var(--pdf-info-border);
border-radius: 0.75rem; border-radius: 0.75rem;
background: rgba(255, 255, 255, 0.8); background: color-mix(in srgb, var(--pdf-surface) 86%, transparent);
padding: 0.6rem; padding: 0.6rem;
} }
.merge-queue-order { .merge-queue-order {
border-radius: 999px; border-radius: 999px;
background: #dbeafe; background: var(--pdf-info-surface);
color: #1e3a8a; color: var(--pdf-info-text);
font-size: 0.8rem; font-size: 0.8rem;
font-weight: 700; font-weight: 700;
padding: 0.2rem 0.5rem; padding: 0.2rem 0.5rem;
@@ -439,7 +430,7 @@ button.secondary {
} }
.merge-queue-details span { .merge-queue-details span {
color: #4b5563; color: var(--pdf-muted);
font-size: 0.8rem; font-size: 0.8rem;
} }
@@ -471,10 +462,10 @@ button.secondary {
.merge-warning { .merge-warning {
margin: 0.75rem 0 0; margin: 0.75rem 0 0;
border: 1px solid #fed7aa; border: 1px solid var(--pdf-warning-border);
border-radius: 0.5rem; border-radius: 0.5rem;
background: #fff7ed; background: var(--pdf-warning-surface);
color: #9a3412; color: var(--pdf-warning-text);
padding: 0.55rem 0.65rem; padding: 0.55rem 0.65rem;
font-size: 0.85rem; font-size: 0.85rem;
} }

View File

@@ -0,0 +1,29 @@
{
"id": "de.add-ideas.pdf-tools",
"name": "PDF Workbench",
"description": "Page-level PDF operations performed locally in the browser.",
"entry": "./",
"icon": "./favicon.svg",
"categories": ["documents", "pdf"],
"tags": ["merge", "split", "rotate", "reorder"],
"integration": {
"contextVersion": 1,
"launchModes": ["navigate", "new-tab"],
"embedding": "unsupported"
},
"requirements": {
"secureContext": true,
"workers": true,
"indexedDb": true,
"crossOriginIsolated": false
},
"privacy": {
"processing": "local",
"fileUploads": false,
"telemetry": false
},
"source": {
"repository": "https://git.add-ideas.de/zemion/pdf-tools",
"license": "AGPL-3.0-only"
}
}

17
src/toolboxApp.test.ts Normal file
View File

@@ -0,0 +1,17 @@
import { describe, expect, it } from 'vitest';
import { parseToolboxApp } from '@add-ideas/toolbox-contract';
import generatedManifest from '../public/toolbox-app.json';
import { toolboxApp } from './toolboxApp';
describe('toolbox app definition', () => {
it('matches the generated public manifest', () => {
expect(toolboxApp).toEqual(parseToolboxApp(generatedManifest));
});
it('uses repository metadata for the shared source control', () => {
expect(toolboxApp.source?.repository).toBe(
'https://git.add-ideas.de/zemion/pdf-tools'
);
expect(toolboxApp.actions).toBeUndefined();
});
});

9
src/toolboxApp.ts Normal file
View File

@@ -0,0 +1,9 @@
import { parseToolboxApp } from '@add-ideas/toolbox-contract';
import definition from './toolboxApp.definition.json';
import { APP_VERSION } from './version';
export const toolboxApp = parseToolboxApp({
schemaVersion: 1,
...definition,
version: APP_VERSION,
});

View File

@@ -1 +1 @@
export const APP_VERSION = '0.3.2'; export const APP_VERSION = '0.4.3';

View File

@@ -0,0 +1,13 @@
import { readFile } from 'node:fs/promises';
import { describe, expect, it } from 'vitest';
describe('Toolbox layout styles', () => {
it('keeps the PDF workspace aligned to the 90rem shell width', async () => {
const stylesheet = await readFile('src/styles.css', 'utf8');
const mainRule = stylesheet.match(
/\.pdf-toolbox-shell \.toolbox-shell__main\s*\{(?<body>[^}]*)\}/u
);
expect(mainRule?.groups?.body).toMatch(/\bmax-width:\s*90rem\s*;/u);
});
});

View File

@@ -0,0 +1,96 @@
import { createHash } from 'node:crypto';
import { spawnSync } from 'node:child_process';
import {
mkdir,
mkdtemp,
readFile,
rm,
symlink,
writeFile,
} from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { describe, expect, it } from 'vitest';
import {
compareCodePoints,
collectDirectoryFiles,
createThirdPartyLicenseBundle,
} from '../scripts/release-archive.mjs';
describe('release archive helpers', () => {
it('sorts archive paths by code point', () => {
expect(['z', 'a', 'ä'].sort(compareCodePoints)).toEqual(['a', 'z', 'ä']);
});
it('creates identical ZIP bytes in different process timezones', () => {
const hashes = ['UTC', 'Europe/Berlin', 'America/New_York'].map(
(timezone) => {
const script = `
import { createHash } from 'node:crypto';
import { createDeterministicZip } from './scripts/release-archive.mjs';
const archive = createDeterministicZip({
'z.txt': Buffer.from('last'),
'a.txt': Buffer.from('first'),
});
process.stdout.write(createHash('sha256').update(archive).digest('hex'));
`;
const result = spawnSync(
process.execPath,
['--input-type=module', '--eval', script],
{
cwd: process.cwd(),
encoding: 'utf8',
env: { ...process.env, TZ: timezone },
}
);
expect(result.status, result.stderr).toBe(0);
return result.stdout;
}
);
expect(new Set(hashes)).toHaveLength(1);
});
it('collects installed runtime license and notice texts', async () => {
const bundle = await createThirdPartyLicenseBundle(process.cwd(), [
'react',
'pdfjs-dist',
'@add-ideas/toolbox-contract',
]);
const text = bundle.toString('utf8');
expect(text).toContain('react@');
expect(text).toContain('pdfjs-dist@');
expect(text).toContain('@add-ideas/toolbox-contract@');
expect(createHash('sha256').update(bundle).digest('hex')).toMatch(
/^[a-f0-9]{64}$/
);
});
it('rejects symbolic links in release input', async () => {
const temporaryDirectory = await mkdtemp(
path.join(os.tmpdir(), 'pdf-tools-release-')
);
try {
const outsideFile = path.join(temporaryDirectory, 'outside.txt');
const inputDirectory = path.join(temporaryDirectory, 'input');
await writeFile(outsideFile, 'private');
await mkdir(inputDirectory);
await symlink(outsideFile, path.join(inputDirectory, 'linked.txt'));
await expect(collectDirectoryFiles(inputDirectory)).rejects.toThrow(
'Unsupported filesystem entry in release input: linked.txt'
);
} finally {
await rm(temporaryDirectory, { recursive: true, force: true });
}
});
it('carries the embedded core-js license used by the PDF.js legacy build', async () => {
const license = await readFile(
'licenses/core-js-3.49.0-LICENSE.txt',
'utf8'
);
expect(license).toContain('core-js 3.49.0');
expect(license).toContain('CoreJS Company');
expect(license).toContain('Permission is hereby granted');
});
});

View File

@@ -2,9 +2,10 @@ import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react'; import react from '@vitejs/plugin-react';
export default defineConfig({ export default defineConfig({
base: './',
plugins: [react()], plugins: [react()],
server: { server: {
host: true, host: true,
allowedHosts: ['pdftools.add-ideas.de'], // ← ADD THIS allowedHosts: ['pdftools.add-ideas.de'],
}, },
}); });