freeze v0.2.0

This commit is contained in:
2026-05-16 20:07:52 +02:00
parent afeb46a210
commit bdbb6c0a1c
9 changed files with 857 additions and 161 deletions

21
CHANGELOG.md Normal file
View File

@@ -0,0 +1,21 @@
# Changelog
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.
## 0.2.0 — Browser-only PDF workspace baseline
### Added
- Browser-only PDF loading and processing.
- Visual page workspace with thumbnails, page preview, reordering, selection, rotation, deletion, duplication, extraction, split, merge, and export.
- Named local workspaces persisted in IndexedDB, including the PDF binary and workspace state.
- Undo/redo command history with labels, timestamps, payload snapshots, and visible redo entries.
- Reset and delete-workspace confirmation dialogs.
- In-app Help/Tutorial dialog and keyboard shortcut overview.
- Administrator-focused README for static self-hosting.
### Notes
This release is the baseline for the next refactoring phase. The goal of the upcoming internal changes is to preserve behavior while extracting workspace state, thumbnail handling, generated object URLs, UI subcomponents, tests, and linting into clearer modules.

339
README
View File

@@ -1,81 +1,312 @@
# PDF Workbench # pdf-tools / PDF Workbench
Browser-only PDF tools for quick page-level editing. Processing happens completely locally in the browser; files are never uploaded to a server. `pdf-tools` is a self-hostable, browser-only PDF workbench for fast page-level PDF operations. It is built for situations where uploading PDFs to a third-party service is undesirable, but users still need simple, visual tools to split, merge, reorder, rotate, duplicate, delete, extract, and export pages.
Current hosted version: <https://pdftools.add-ideas.de>
Current baseline: **v0.2.0 — Browser-only PDF workspace baseline**. See [`CHANGELOG.md`](CHANGELOG.md) for the release notes and milestone history.
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.
## Purpose
Many everyday PDF tasks are not full document-authoring tasks. They are page-workbench tasks:
- remove pages before sending a document;
- rotate scanned pages;
- split a PDF into single-page files;
- merge another PDF into the current document;
- extract a subset of pages;
- reorder pages visually;
- keep a local working state while experimenting;
- undo and redo page operations before exporting.
`pdf-tools` focuses on this page-level workflow. It is intentionally not a full PDF editor, OCR solution, digital-signature workflow, DMS, or Adobe Acrobat replacement.
## Where the project comes from
The project started as a pragmatic, browser-only PDF helper for quick administrative and document-preparation tasks. The guiding idea is simple: many users need small PDF operations, but sensitive or internal documents should not have to leave the browser just to remove, rotate, split, or rearrange pages.
This makes the project especially useful for self-hosted environments, public-sector settings, universities, small organizations, and internal tools where operational simplicity and document locality matter.
## Distinguishing features
- **Browser-only processing**: PDF files are processed locally in the browser. The hosting server does not receive the selected PDFs.
- **Static self-hosting**: The production build can be served by any static web server or reverse proxy.
- **Visual page workspace**: Users work with page thumbnails, drag-and-drop ordering, selection, page preview, and page-level actions.
- **Named local workspaces**: Workspaces can be saved in the browser with the PDF binary and editing state stored in IndexedDB.
- **Undo/redo command history**: Workspace operations are recorded as commands with label, timestamp, and payload. The history view shows undo and redo states.
- **Progressive thumbnails**: Thumbnails are generated progressively so the UI becomes useful before all pages have finished rendering.
- **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.
- **Merge choices**: Loading another PDF can replace the current document, append pages, or insert pages at a chosen position.
- **In-app help**: The app includes a Help/Tutorial dialog with keyboard shortcuts and workflow explanations.
## Current features ## Current features
- load a PDF in the browser ### File and workspace handling
- generate page thumbnails progressively
- reorder pages via drag and drop
- select pages, including Shift range selection
- rotate pages clockwise/counter-clockwise
- delete pages from the working document
- preview pages in an overlay
- flip through preview pages with buttons or arrow keys
- merge a second PDF by replacing, appending, or inserting at a chosen position
- extract selected pages into a new PDF
- export the current reordered/rotated document
- split into single-page PDFs
## Keyboard shortcuts - Load a local PDF file.
- Save a named workspace in the browser.
- Restore saved workspaces from IndexedDB.
- Reset the active workspace, with a save prompt for unsaved changes.
- Delete saved workspaces after confirmation.
- Store workspace history and redo history.
- `CtrlA` / `⌘A`: select all pages ### Page operations
- `Delete` / `Backspace`: delete selected pages
- `Esc`: clear the current selection - Generate page thumbnails in the browser.
- Preview overlay: `←` / `→` flip pages, `Esc` closes the overlay - Reorder pages with drag and drop.
- Select individual pages.
- Select page ranges with Shift-click.
- Drag a selected page to move the whole selection.
- Duplicate/copy selected pages into a chosen position.
- Rotate pages clockwise and counter-clockwise.
- Delete one page or all selected pages after confirmation.
- Preview pages in a modal overlay.
- Flip through preview pages with buttons or arrow keys.
### Export tools
- Export the current reordered/rotated/duplicated/deleted workspace as a new PDF.
- Extract selected pages into a new PDF.
- Split the source PDF into single-page PDFs.
- Merge another PDF by replacing, appending, or inserting it into the current workspace.
### Keyboard shortcuts
| Shortcut | Action |
| --- | --- |
| `F1` / `?` | Open in-app help and tutorial |
| `Ctrl`/`⌘` + `A` | Select all pages |
| `Delete` / `Backspace` | Delete selected pages after confirmation |
| `Esc` | Clear the current selection or close an open dialog |
| `Ctrl`/`⌘` + `Z` | Undo |
| `Ctrl`/`⌘` + `Shift` + `Z` | Redo |
| `Ctrl`/`⌘` + `Y` | Redo |
| `←` / `→` in preview | Move to previous / next page |
| `Esc` in preview | Close preview |
Keyboard shortcuts are ignored while typing in form fields. Keyboard shortcuts are ignored while typing in form fields.
## Current implementation focus ## In-app documentation concept
The project is currently optimized around page-level PDF work: split, merge, reorder, rotate, preview, select, delete, extract, and export. Deep content-stream editing is intentionally out of scope for now. The app includes a Help/Tutorial dialog reachable from the header via **Help ?**, `F1`, or `?`.
Recommended structure for in-app documentation:
1. **Quick tutorial**: short task-oriented steps from loading a PDF to exporting.
2. **Keyboard shortcuts**: a compact reference for power users.
3. **Concepts**: explain the difference between a PDF file, a workspace, command history, and exported output.
4. **Privacy model**: state clearly that processing is browser-local and workspaces are saved in local browser storage.
5. **Roadmap hints**: link users to the README or project repository for planned features, instead of overloading the app UI.
This keeps the app useful for first-time users without turning the main interface into a manual.
## Administrator notes
### Deployment model
`pdf-tools` is deployed as a static web application:
```text
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/`.
### Build from source
```bash
npm ci
npm run build
```
The production build is written to `dist/`.
To preview the production build locally:
```bash
npm run preview
```
For development:
```bash
npm run dev
```
### Static hosting
Any static hosting setup should work, for example:
- nginx;
- Apache httpd;
- Caddy;
- Traefik in front of a static file container;
- GitLab Pages or another static publishing target;
- a minimal container serving `dist/`.
A typical nginx location for a single-page app looks like this:
```nginx
location / {
try_files $uri $uri/ /index.html;
}
```
### Reverse proxy considerations
For production, serve the built app via HTTPS. The reverse proxy only sees requests for static app assets. It should not receive the user's PDF files, because the files are opened through browser APIs and processed locally.
For the Vite development server, `vite.config.ts` can restrict allowed hosts. The current development configuration includes `pdftools.add-ideas.de` as an allowed host for the dev server. This is not required for a normal production static build.
### Content Security Policy
A strict CSP is possible, but it must account for browser-local rendering details. Thumbnails use data URLs, downloads use blob URLs, and pdf.js uses a worker.
A starting point for testing could be:
```http
Content-Security-Policy: default-src 'self'; script-src 'self'; worker-src 'self' blob:; img-src 'self' data: blob:; style-src 'self' 'unsafe-inline'; object-src 'none'; base-uri 'self'; form-action 'none'
```
Adjust this to your hosting environment and test PDF loading, thumbnail rendering, preview, and downloads before enforcing it broadly.
### Storage and persistence
Saved workspaces are stored in the user's browser using IndexedDB. A workspace may include:
- the source PDF binary;
- workspace name and metadata;
- page order and duplicated page references;
- rotations;
- selected pages;
- undo and redo command history.
This has several operational consequences:
- Workspaces are local to the browser profile and device.
- Clearing browser data can delete saved workspaces.
- Server backups do not include user workspaces.
- Different users on different devices do not share workspaces through the server.
- Browser storage quotas apply.
### Privacy and logging
The server should only log asset requests for the app itself. It should not log PDF filenames or contents unless the hosting environment adds custom client-side telemetry or upload logic. Avoid adding analytics that could weaken the project's privacy model.
### Browser support
The app expects modern browser APIs, including:
- File API;
- Blob and object URLs;
- IndexedDB;
- Web Workers;
- Canvas;
- modern JavaScript modules.
Use current versions of Chromium, Firefox, Safari, or Edge.
## Versioning and release baseline
The application version shown in the header is defined in `src/version.ts`. The package version in `package.json` and the displayed app version should be kept in sync for releases.
The current development baseline is:
```text
v0.2.0 — Browser-only PDF workspace baseline
```
## Project structure
```text
src/
App.tsx Main application state and orchestration
components/
ActionDialog.tsx Reusable confirmation/action dialog
ActionsPanel.tsx Export, extract, and split actions
FileLoader.tsx PDF file loading
HelpDialog.tsx In-app tutorial and shortcut reference
Layout.tsx Application shell/header
PagePreviewModal.tsx Large page preview with navigation
ReorderPanel.tsx Page grid, selection, drag/drop, copy/delete/rotate
WorkspacePanel.tsx Workspace save/load/reset and undo/redo history
pdf/
pdfService.ts pdf-lib operations: load, merge, split, export
pdfThumbnailService.ts pdf.js thumbnail rendering
pdfTypes.ts PDF-related types
workspace/
workspaceCommands.ts Command model for undo/redo
workspaceDb.ts IndexedDB persistence
workspaceTypes.ts Workspace data model
styles.css Global styles
version.ts App version displayed in the header
```
## Roadmap ## Roadmap
### Milestone 1: Fast preview and thumbnails ### Milestone 1: Fast preview and thumbnails
- [x] Remove unused `PageList` component - [x] Remove unused legacy page list view.
- [x] Bound thumbnail generation by width and height - [x] Bound thumbnail generation by width and height.
- [x] Display thumbnails progressively - [x] Display thumbnails progressively.
- [x] Add preview page flipping - [x] Add preview page flipping.
- [x] Attach preview controls to the modal container - [x] Attach preview controls to the modal container.
- [x] Add first keyboard shortcuts - [x] Add first keyboard shortcuts.
- [x] Cache thumbnails by page and rotation - [x] Cache thumbnails by page and rotation.
- [x] Regenerate only changed rotated thumbnails - [x] Regenerate only changed rotated thumbnails.
- [x] Show software version number - [x] Show software version number.
### Milestone 2: Real page workspace ### Milestone 2: Real page workspace
- [x] Introduce stable page references instead of only original page indices - [x] Introduce stable page references instead of only original page indices.
- [x] Support duplicate selected pages - [x] Support duplicate selected pages.
- [x] Save / reload the last state from storage - [x] Save and reload the last state from browser storage.
- [x] Support workspaces - [x] Support named workspaces.
- [x] Reset workspace - [x] Store PDF binaries directly in IndexedDB.
- [ ] Extract selection as a new active workspace - [x] Reset workspace.
- [x] Add command history as a foundation for undo/redo - [x] Add command history as a foundation for undo/redo.
- [x] Add undo/redo - [x] Add undo/redo.
- [ ] maybe smaller undo / redo footprint - [x] Display undo/redo history with redo entries visually separated.
- [ ] Add grid/list view toggle - [ ] 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 ### Milestone 3: Better merge and mobile handling
- [ ] Add a full multi-file merge queue - [ ] Add a full multi-file merge queue.
- [ ] Support drag-and-drop of PDFs into the page grid at the hovered position - [ ] Support drag-and-drop of PDFs into the page grid at the hovered position.
- [ ] Add custom long-press drag on mobile - [ ] Add custom long-press drag on mobile.
- [ ] Consolidate actions into a toolbar - [ ] Consolidate frequently used actions into a toolbar.
### Milestone 4: Structural PDF editing ### Milestone 4: Structural PDF editing
- [ ] Metadata editing - [ ] Metadata editing.
- [ ] Crop pages - [ ] Crop pages.
- [ ] Add tools directly in the preview overlay - [ ] Add tools directly in the preview overlay.
- [ ] Read/fill/flatten forms - [ ] Read/fill/flatten forms.
- [ ] Read bookmarks, then evaluate bookmark editing - [ ] Read bookmarks, then evaluate bookmark editing.
- [ ] Read annotations, then evaluate annotation writing - [ ] Read annotations, then evaluate annotation writing.
### Milestone 5: Export and power tools ### Milestone 5: Export and power tools
- [ ] Basic text extraction - [ ] Basic text extraction.
- [ ] ZIP export for split results - [ ] ZIP export for split results.
- [ ] Optimize/compress MVP - [ ] Optimize/compress MVP.
- [ ] Carefully scoped encrypted PDF handling - [ ] 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
GPL-3.0. See `LICENSE`.

241
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "pdf-workbench", "name": "pdf-tools",
"version": "0.1.0", "version": "0.2.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "pdf-workbench", "name": "pdf-tools",
"version": "0.1.0", "version": "0.2.0",
"dependencies": { "dependencies": {
"pdf-lib": "^1.17.1", "pdf-lib": "^1.17.1",
"pdfjs-dist": "^4.6.82", "pdfjs-dist": "^4.6.82",
@@ -623,9 +623,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@rollup/rollup-android-arm-eabi": { "node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.53.3", "version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz",
"integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -637,9 +637,9 @@
] ]
}, },
"node_modules/@rollup/rollup-android-arm64": { "node_modules/@rollup/rollup-android-arm64": {
"version": "4.53.3", "version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz",
"integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -651,9 +651,9 @@
] ]
}, },
"node_modules/@rollup/rollup-darwin-arm64": { "node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.53.3", "version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz",
"integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -665,9 +665,9 @@
] ]
}, },
"node_modules/@rollup/rollup-darwin-x64": { "node_modules/@rollup/rollup-darwin-x64": {
"version": "4.53.3", "version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz",
"integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -679,9 +679,9 @@
] ]
}, },
"node_modules/@rollup/rollup-freebsd-arm64": { "node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.53.3", "version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz",
"integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -693,9 +693,9 @@
] ]
}, },
"node_modules/@rollup/rollup-freebsd-x64": { "node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.53.3", "version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz",
"integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -707,9 +707,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm-gnueabihf": { "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.53.3", "version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz",
"integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -721,9 +721,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm-musleabihf": { "node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.53.3", "version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz",
"integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -735,9 +735,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm64-gnu": { "node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.53.3", "version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz",
"integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -749,9 +749,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm64-musl": { "node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.53.3", "version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz",
"integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -763,9 +763,23 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-loong64-gnu": { "node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.53.3", "version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz",
"integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-loong64-musl": {
"version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz",
"integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==",
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
@@ -777,9 +791,23 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-ppc64-gnu": { "node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.53.3", "version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz",
"integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-ppc64-musl": {
"version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz",
"integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@@ -791,9 +819,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-riscv64-gnu": { "node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.53.3", "version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz",
"integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@@ -805,9 +833,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-riscv64-musl": { "node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.53.3", "version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz",
"integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@@ -819,9 +847,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-s390x-gnu": { "node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.53.3", "version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz",
"integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
@@ -833,9 +861,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-x64-gnu": { "node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.53.3", "version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz",
"integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -847,9 +875,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-x64-musl": { "node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.53.3", "version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz",
"integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -860,10 +888,24 @@
"linux" "linux"
] ]
}, },
"node_modules/@rollup/rollup-openbsd-x64": {
"version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz",
"integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
]
},
"node_modules/@rollup/rollup-openharmony-arm64": { "node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.53.3", "version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz",
"integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -875,9 +917,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-arm64-msvc": { "node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.53.3", "version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz",
"integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -889,9 +931,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-ia32-msvc": { "node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.53.3", "version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz",
"integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@@ -903,9 +945,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-x64-gnu": { "node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.53.3", "version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz",
"integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -917,9 +959,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-x64-msvc": { "node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.53.3", "version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz",
"integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1342,9 +1384,9 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.5.6", "version": "8.5.14",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -1397,9 +1439,9 @@
} }
}, },
"node_modules/rollup": { "node_modules/rollup": {
"version": "4.53.3", "version": "4.60.4",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz",
"integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -1413,28 +1455,31 @@
"npm": ">=8.0.0" "npm": ">=8.0.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.53.3", "@rollup/rollup-android-arm-eabi": "4.60.4",
"@rollup/rollup-android-arm64": "4.53.3", "@rollup/rollup-android-arm64": "4.60.4",
"@rollup/rollup-darwin-arm64": "4.53.3", "@rollup/rollup-darwin-arm64": "4.60.4",
"@rollup/rollup-darwin-x64": "4.53.3", "@rollup/rollup-darwin-x64": "4.60.4",
"@rollup/rollup-freebsd-arm64": "4.53.3", "@rollup/rollup-freebsd-arm64": "4.60.4",
"@rollup/rollup-freebsd-x64": "4.53.3", "@rollup/rollup-freebsd-x64": "4.60.4",
"@rollup/rollup-linux-arm-gnueabihf": "4.53.3", "@rollup/rollup-linux-arm-gnueabihf": "4.60.4",
"@rollup/rollup-linux-arm-musleabihf": "4.53.3", "@rollup/rollup-linux-arm-musleabihf": "4.60.4",
"@rollup/rollup-linux-arm64-gnu": "4.53.3", "@rollup/rollup-linux-arm64-gnu": "4.60.4",
"@rollup/rollup-linux-arm64-musl": "4.53.3", "@rollup/rollup-linux-arm64-musl": "4.60.4",
"@rollup/rollup-linux-loong64-gnu": "4.53.3", "@rollup/rollup-linux-loong64-gnu": "4.60.4",
"@rollup/rollup-linux-ppc64-gnu": "4.53.3", "@rollup/rollup-linux-loong64-musl": "4.60.4",
"@rollup/rollup-linux-riscv64-gnu": "4.53.3", "@rollup/rollup-linux-ppc64-gnu": "4.60.4",
"@rollup/rollup-linux-riscv64-musl": "4.53.3", "@rollup/rollup-linux-ppc64-musl": "4.60.4",
"@rollup/rollup-linux-s390x-gnu": "4.53.3", "@rollup/rollup-linux-riscv64-gnu": "4.60.4",
"@rollup/rollup-linux-x64-gnu": "4.53.3", "@rollup/rollup-linux-riscv64-musl": "4.60.4",
"@rollup/rollup-linux-x64-musl": "4.53.3", "@rollup/rollup-linux-s390x-gnu": "4.60.4",
"@rollup/rollup-openharmony-arm64": "4.53.3", "@rollup/rollup-linux-x64-gnu": "4.60.4",
"@rollup/rollup-win32-arm64-msvc": "4.53.3", "@rollup/rollup-linux-x64-musl": "4.60.4",
"@rollup/rollup-win32-ia32-msvc": "4.53.3", "@rollup/rollup-openbsd-x64": "4.60.4",
"@rollup/rollup-win32-x64-gnu": "4.53.3", "@rollup/rollup-openharmony-arm64": "4.60.4",
"@rollup/rollup-win32-x64-msvc": "4.53.3", "@rollup/rollup-win32-arm64-msvc": "4.60.4",
"@rollup/rollup-win32-ia32-msvc": "4.60.4",
"@rollup/rollup-win32-x64-gnu": "4.60.4",
"@rollup/rollup-win32-x64-msvc": "4.60.4",
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },

View File

@@ -1,6 +1,6 @@
{ {
"name": "pdf-workbench", "name": "pdf-tools",
"version": "0.1.0", "version": "0.2.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
@@ -20,5 +20,6 @@
"@vitejs/plugin-react-swc": "^3.7.0", "@vitejs/plugin-react-swc": "^3.7.0",
"typescript": "^5.6.3", "typescript": "^5.6.3",
"vite": "^5.4.10" "vite": "^5.4.10"
} },
"description": "Browser-only, self-hostable PDF workbench for page-level PDF operations."
} }

View File

@@ -8,6 +8,7 @@ import WorkspacePanel from './components/WorkspacePanel';
import ActionDialog, { import ActionDialog, {
type ActionDialogAction, type ActionDialogAction,
} from './components/ActionDialog'; } from './components/ActionDialog';
import HelpDialog from './components/HelpDialog';
import { PDFDocument } from 'pdf-lib'; import { PDFDocument } from 'pdf-lib';
import type { import type {
StoredWorkspace, StoredWorkspace,
@@ -105,6 +106,7 @@ const App: React.FC = () => {
content: React.ReactNode; content: React.ReactNode;
actions: ActionDialogAction[]; actions: ActionDialogAction[];
} | null>(null); } | null>(null);
const [helpOpen, setHelpOpen] = useState(false);
const [pdf, setPdf] = useState<PdfFile | null>(null); const [pdf, setPdf] = useState<PdfFile | null>(null);
const [isBusy, setIsBusy] = useState(false); const [isBusy, setIsBusy] = useState(false);
@@ -785,6 +787,23 @@ const App: React.FC = () => {
const hasPdf = !!pdf; const hasPdf = !!pdf;
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (isEditableKeyboardTarget(e.target)) return;
if (e.key === 'F1' || e.key === '?') {
e.preventDefault();
setHelpOpen(true);
}
};
window.addEventListener('keydown', handleKeyDown);
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
}, []);
// === UI interactions === // === UI interactions ===
const handleRotatePageClockwise = (pageId: string) => { const handleRotatePageClockwise = (pageId: string) => {
const before = getCurrentCommandState(); const before = getCurrentCommandState();
@@ -1257,7 +1276,7 @@ const App: React.FC = () => {
previewVisualIndex >= 0 && previewVisualIndex < pages.length - 1; previewVisualIndex >= 0 && previewVisualIndex < pages.length - 1;
return ( return (
<Layout> <Layout onOpenHelp={() => setHelpOpen(true)}>
<FileLoader pdf={pdf} onFileLoaded={handleFileLoaded} /> <FileLoader pdf={pdf} onFileLoaded={handleFileLoaded} />
<WorkspacePanel <WorkspacePanel
@@ -1446,6 +1465,8 @@ const App: React.FC = () => {
> >
{actionDialog?.content} {actionDialog?.content}
</ActionDialog> </ActionDialog>
<HelpDialog open={helpOpen} onClose={() => setHelpOpen(false)} />
</Layout> </Layout>
); );
}; };

View File

@@ -0,0 +1,166 @@
import React, { useEffect } from 'react';
interface HelpDialogProps {
open: boolean;
onClose: () => void;
}
const shortcuts = [
{ keys: 'F1 / ?', description: 'Open this help and tutorial dialog' },
{ keys: 'Ctrl/⌘ + A', description: 'Select all pages in the current workspace' },
{ keys: 'Delete / Backspace', description: 'Delete the selected pages after confirmation' },
{ keys: 'Esc', description: 'Clear the page selection or close an open dialog' },
{ keys: 'Ctrl/⌘ + Z', description: 'Undo the latest workspace command' },
{ keys: 'Ctrl/⌘ + Shift + Z', description: 'Redo the next workspace command' },
{ keys: 'Ctrl/⌘ + Y', description: 'Redo the next workspace command' },
{ keys: '← / → in preview', description: 'Move to the previous or next page in the preview overlay' },
];
const tutorialSteps = [
{
title: '1. Open a PDF or load a workspace',
body: 'Start by selecting a local PDF file. If you saved workspaces before, you can restore one from browser storage instead.',
},
{
title: '2. Arrange pages visually',
body: 'Drag page cards to reorder them. Rotate single pages, open the large preview with a click, or remove pages you do not want in the export.',
},
{
title: '3. Select, copy, and delete pages',
body: 'Use the checkbox on a page card to select it. Shift-click extends a range. Dragging a selected page moves the whole selection; the copy controls duplicate selected pages into a chosen slot.',
},
{
title: '4. Save your workspace or export a PDF',
body: 'Saving a workspace keeps the current working state in this browser. Exporting creates a new PDF file for download.',
},
{
title: '5. Use history deliberately',
body: 'Each workspace operation is stored as a command with label and timestamp. Undo and redo walk through that command history.',
},
];
const HelpDialog: React.FC<HelpDialogProps> = ({ open, onClose }) => {
useEffect(() => {
if (!open) return;
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key !== 'Escape') return;
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
onClose();
};
window.addEventListener('keydown', handleKeyDown, { capture: true });
return () => {
window.removeEventListener('keydown', handleKeyDown, { capture: true });
};
}, [open, onClose]);
if (!open) return null;
return (
<div
className="help-dialog-backdrop"
role="dialog"
aria-modal="true"
aria-labelledby="help-dialog-title"
onPointerDown={(e) => {
if (e.target === e.currentTarget) {
onClose();
}
}}
>
<div className="help-dialog-panel">
<div className="help-dialog-header">
<div>
<h2 id="help-dialog-title">Help & tutorial</h2>
<p>
PDF Workbench is a browser-only page workspace. Use it to quickly
rearrange, split, merge, rotate, duplicate, and export PDFs without
uploading documents to a server.
</p>
</div>
<button
type="button"
className="help-close-button"
onClick={onClose}
aria-label="Close help"
title="Close help (Esc)"
>
×
</button>
</div>
<div className="help-dialog-content">
<section className="help-section">
<h3>Quick tutorial</h3>
<div className="help-step-list">
{tutorialSteps.map((step) => (
<article key={step.title} className="help-step">
<h4>{step.title}</h4>
<p>{step.body}</p>
</article>
))}
</div>
</section>
<section className="help-section">
<h3>Keyboard shortcuts</h3>
<div className="shortcut-grid">
{shortcuts.map((shortcut) => (
<React.Fragment key={shortcut.keys}>
<kbd>{shortcut.keys}</kbd>
<span>{shortcut.description}</span>
</React.Fragment>
))}
</div>
<p className="help-note">
Shortcuts are ignored while typing in text fields or other editable
controls.
</p>
</section>
<section className="help-section help-concepts">
<h3>Important concepts</h3>
<dl>
<div>
<dt>Browser-only processing</dt>
<dd>
PDF operations run in your browser. A self-hosted server only
delivers the static app files.
</dd>
</div>
<div>
<dt>Workspace</dt>
<dd>
A named local editing state, including the PDF binary, page
order, rotations, selection, and command history.
</dd>
</div>
<div>
<dt>Export</dt>
<dd>
A generated PDF download. Exported files are separate from
saved workspaces.
</dd>
</div>
<div>
<dt>History</dt>
<dd>
Undoable commands show what changed, when it changed, and
where the current point in history is.
</dd>
</div>
</dl>
</section>
</div>
</div>
</div>
);
};
export default HelpDialog;

View File

@@ -3,9 +3,10 @@ import { APP_VERSION } from '../version';
interface LayoutProps { interface LayoutProps {
children: React.ReactNode; children: React.ReactNode;
onOpenHelp?: () => void;
} }
const Layout: React.FC<LayoutProps> = ({ children }) => { const Layout: React.FC<LayoutProps> = ({ children, onOpenHelp }) => {
return ( return (
<div className="app-root"> <div className="app-root">
<header className="app-header"> <header className="app-header">
@@ -18,8 +19,22 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
</div> </div>
</div> </div>
<div className="app-version" title={`Version ${APP_VERSION}`}> <div className="app-header-actions">
v{APP_VERSION} {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>
</div> </div>
</header> </header>

View File

@@ -198,3 +198,199 @@ button.secondary {
line-height: 1; line-height: 1;
white-space: nowrap; 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 {
position: fixed;
inset: 0;
z-index: 80;
background: rgba(15, 23, 42, 0.65);
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
}
.help-dialog-panel {
width: min(920px, 100%);
max-height: min(88vh, 760px);
overflow: auto;
background: #ffffff;
border-radius: 0.9rem;
box-shadow: 0 24px 60px rgba(15, 23, 42, 0.4);
}
.help-dialog-header {
position: sticky;
top: 0;
z-index: 1;
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 1rem;
padding: 1rem;
background: #ffffff;
border-bottom: 1px solid #e5e7eb;
}
.help-dialog-header h2 {
margin: 0;
font-size: 1.1rem;
}
.help-dialog-header p {
margin: 0.35rem 0 0;
color: #4b5563;
font-size: 0.9rem;
line-height: 1.45;
}
.help-close-button {
flex: 0 0 auto;
border: none;
border-radius: 999px;
width: 2rem;
height: 2rem;
background: #e5e7eb;
color: #111827;
cursor: pointer;
font-size: 1.25rem;
line-height: 1;
}
.help-dialog-content {
display: grid;
grid-template-columns: minmax(0, 1.2fr) minmax(260px, 0.8fr);
gap: 1rem;
padding: 1rem;
}
.help-section {
border: 1px solid #e5e7eb;
border-radius: 0.75rem;
padding: 0.9rem;
background: #f9fafb;
}
.help-section h3 {
margin: 0 0 0.75rem;
font-size: 0.95rem;
}
.help-step-list {
display: flex;
flex-direction: column;
gap: 0.65rem;
}
.help-step {
border-radius: 0.65rem;
background: #ffffff;
border: 1px solid #e5e7eb;
padding: 0.75rem;
}
.help-step h4 {
margin: 0;
font-size: 0.9rem;
}
.help-step p,
.help-note {
margin: 0.35rem 0 0;
color: #4b5563;
font-size: 0.85rem;
line-height: 1.45;
}
.shortcut-grid {
display: grid;
grid-template-columns: max-content minmax(0, 1fr);
gap: 0.45rem 0.75rem;
align-items: center;
}
.shortcut-grid kbd {
display: inline-flex;
justify-content: center;
border-radius: 0.4rem;
border: 1px solid #d1d5db;
background: #ffffff;
padding: 0.2rem 0.45rem;
font-size: 0.78rem;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
color: #111827;
box-shadow: inset 0 -1px 0 #d1d5db;
white-space: nowrap;
}
.shortcut-grid span {
color: #4b5563;
font-size: 0.85rem;
line-height: 1.35;
}
.help-concepts {
grid-column: 1 / -1;
}
.help-concepts dl {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 0.75rem;
margin: 0;
}
.help-concepts dl > div {
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 0.65rem;
padding: 0.75rem;
}
.help-concepts dt {
font-weight: 700;
font-size: 0.85rem;
margin-bottom: 0.3rem;
}
.help-concepts dd {
margin: 0;
color: #4b5563;
font-size: 0.82rem;
line-height: 1.4;
}
@media (max-width: 760px) {
.help-dialog-content,
.help-concepts dl {
grid-template-columns: 1fr;
}
.app-header-content {
align-items: flex-start;
}
}

View File

@@ -1 +1 @@
export const APP_VERSION = '0.1.3'; export const APP_VERSION = '0.2.0';