diff --git a/CHANGELOG.md b/CHANGELOG.md index a2a58c6..b77d570 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,12 @@ 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. -## Unreleased +## 0.3.0 — Selection workspace and maintenance release ### Added +- Added “Open selection as workspace” to create a new unsaved active workspace from the selected pages in current visual order. +- Added selection-workspace helper tests for visual-order selection and derived naming. - Added TypeScript type-check, ESLint, Prettier, and aggregate `check` scripts. - Added ESLint flat config with TypeScript, React Hooks, React Refresh, browser, and Node config support. - Added Prettier configuration and ignore file. @@ -18,6 +20,8 @@ The project follows a pragmatic versioning scheme while the app is still below ` ### Changed +- Bumped the app/package version to `0.3.0`. +- Switched to `@vitejs/plugin-react`. - Marked the package as an ES module package to remove the Vite CJS Node API deprecation warning during local tooling runs. - Ran Prettier across the project after adding the formatting configuration. - Split the former monolithic `ReorderPanel` into focused page-workspace components: `PageGrid`, `PageCard`, `PageSelectionToolbar`, `DropIndicator`, and `CopyPagesDialog`. @@ -30,6 +34,7 @@ The project follows a pragmatic versioning scheme while the app is still below ` ### Fixed +- Renamed Prettier config files to `.prettierrc.json` and `.prettierignore` so Prettier picks them up automatically. - Fixed existing `tsc --noEmit` failures for Vite worker URL imports and `Uint8Array`/`BlobPart` PDF byte handling. - Removed a duplicate copy-dialog validation error assignment in `ReorderPanel`. - Rotated thumbnails from loaded/saved workspaces are now regenerated from the actual current page rotation instead of relying only on rotation changes after load. diff --git a/README b/README index 0631671..b84a1c7 100644 --- a/README +++ b/README @@ -4,7 +4,7 @@ Current hosted version: -Current baseline: **v0.2.0 — Browser-only PDF workspace baseline**. See [`CHANGELOG.md`](CHANGELOG.md) for the release notes and milestone history. +Current release: **v0.3.0 — Selection workspace and maintenance release**. See [`CHANGELOG.md`](CHANGELOG.md) for 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. @@ -70,6 +70,7 @@ This makes the project especially useful for self-hosted environments, public-se - Export the current reordered/rotated/duplicated/deleted workspace as a new PDF. - Extract selected pages into a new PDF. +- Open selected pages as a new active workspace for continued editing. - Split the source PDF into single-page PDFs. - Merge another PDF by replacing, appending, or inserting it into the current workspace. @@ -226,10 +227,10 @@ The application version shown in the header is defined in `src/version.ts`. The The current development baseline is: ```text -v0.2.0 — Browser-only PDF workspace baseline +v0.3.0 — Selection workspace and maintenance release ``` -This baseline is preserved through a staged refactoring path. Workspace state, thumbnail handling, generated download URLs, page-grid components, tests, type-checking, linting, and formatting are now separated enough to support the next feature phase without turning `App.tsx` back into a monolith. +This release preserves the browser-only workspace baseline and adds the first post-refactor feature: opening selected pages as a new active workspace. 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. ## Project structure @@ -282,7 +283,7 @@ src/ - [x] Add command history as a foundation for undo/redo. - [x] Add undo/redo. - [x] Display undo/redo history with redo entries visually separated. -- [ ] Extract selection as a new active workspace. +- [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. diff --git a/package-lock.json b/package-lock.json index 185edff..92d6ec2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,25 +1,25 @@ { "name": "pdf-tools", - "version": "0.2.1", + "version": "0.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pdf-tools", - "version": "0.2.1", + "version": "0.2.2", "dependencies": { "pdf-lib": "^1.17.1", - "pdfjs-dist": "^4.6.82", - "react": "^18.3.1", - "react-dom": "^18.3.1" + "pdfjs-dist": "^5.7.284", + "react": "^19.2.6", + "react-dom": "^19.2.6" }, "devDependencies": { "@eslint/js": "^10.0.1", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", "@types/node": "^25.8.0", - "@types/react": "^18.3.11", - "@types/react-dom": "^18.3.2", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.2", "eslint": "^10.4.0", "eslint-config-prettier": "^10.1.8", @@ -28,7 +28,7 @@ "globals": "^17.6.0", "jsdom": "^29.1.1", "prettier": "^3.8.3", - "typescript": "^5.6.3", + "typescript": "^6.0.3", "typescript-eslint": "^8.59.3", "vite": "^8.0.13", "vitest": "^4.1.6" @@ -822,9 +822,9 @@ } }, "node_modules/@napi-rs/canvas": { - "version": "0.1.82", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.82.tgz", - "integrity": "sha512-FGjyUBoF0sl1EenSiE4UV2WYu76q6F9GSYedq5EiOCOyGYoQ/Owulcv6rd7v/tWOpljDDtefXXIaOCJrVKem4w==", + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.100.tgz", + "integrity": "sha512-xglYA6q3XO5P3BNJYxVZ1IV7DLVjp1Py6nwag88YntrS+3vKHyYcMqXVS4ZztJmwz2uGvz1FWhI/4LgbR5uQDA==", "license": "MIT", "optional": true, "workspaces": [ @@ -833,23 +833,28 @@ "engines": { "node": ">= 10" }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, "optionalDependencies": { - "@napi-rs/canvas-android-arm64": "0.1.82", - "@napi-rs/canvas-darwin-arm64": "0.1.82", - "@napi-rs/canvas-darwin-x64": "0.1.82", - "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.82", - "@napi-rs/canvas-linux-arm64-gnu": "0.1.82", - "@napi-rs/canvas-linux-arm64-musl": "0.1.82", - "@napi-rs/canvas-linux-riscv64-gnu": "0.1.82", - "@napi-rs/canvas-linux-x64-gnu": "0.1.82", - "@napi-rs/canvas-linux-x64-musl": "0.1.82", - "@napi-rs/canvas-win32-x64-msvc": "0.1.82" + "@napi-rs/canvas-android-arm64": "0.1.100", + "@napi-rs/canvas-darwin-arm64": "0.1.100", + "@napi-rs/canvas-darwin-x64": "0.1.100", + "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.100", + "@napi-rs/canvas-linux-arm64-gnu": "0.1.100", + "@napi-rs/canvas-linux-arm64-musl": "0.1.100", + "@napi-rs/canvas-linux-riscv64-gnu": "0.1.100", + "@napi-rs/canvas-linux-x64-gnu": "0.1.100", + "@napi-rs/canvas-linux-x64-musl": "0.1.100", + "@napi-rs/canvas-win32-arm64-msvc": "0.1.100", + "@napi-rs/canvas-win32-x64-msvc": "0.1.100" } }, "node_modules/@napi-rs/canvas-android-arm64": { - "version": "0.1.82", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.82.tgz", - "integrity": "sha512-bvZhN0iI54ouaQOrgJV96H2q7J3ZoufnHf4E1fUaERwW29Rz4rgicohnAg4venwBJZYjGl5Yl3CGmlAl1LZowQ==", + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.100.tgz", + "integrity": "sha512-hjhCKhntPv9+t4ckHymdx0phYNcVW+GKQR6Lzw2zE+pOVjOplSmtx9nNNknTjbEDLcuLZqA1y8ufKg1XfgftzQ==", "cpu": [ "arm64" ], @@ -860,12 +865,16 @@ ], "engines": { "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" } }, "node_modules/@napi-rs/canvas-darwin-arm64": { - "version": "0.1.82", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.82.tgz", - "integrity": "sha512-InuBHKCyuFqhNwNr4gpqazo5Xp6ltKflqOLiROn4hqAS8u21xAHyYCJRgHwd+a5NKmutFTaRWeUIT/vxWbU/iw==", + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.100.tgz", + "integrity": "sha512-2PcswRaC7Ly645DGt88///zuFDhJxJYdKAs1uU3mfk1atYkXufgcgLfBpk6Tm12nCQBaNt1wpybuPZ4qOhTo8A==", "cpu": [ "arm64" ], @@ -876,12 +885,16 @@ ], "engines": { "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" } }, "node_modules/@napi-rs/canvas-darwin-x64": { - "version": "0.1.82", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.82.tgz", - "integrity": "sha512-aQGV5Ynn96onSXcuvYb2y7TRXD/t4CL2EGmnGqvLyeJX1JLSNisKQlWN/1bPDDXymZYSdUqbXehj5qzBlOx+RQ==", + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.100.tgz", + "integrity": "sha512-ePNZtj7pNIva/siZMg+HmbeozkIjqUIYdoymH8HaA3qK7LfzFN4WMBM8G6HQ9ZC+H3+Dnn5pqtiXpgLykaPOhw==", "cpu": [ "x64" ], @@ -892,12 +905,16 @@ ], "engines": { "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" } }, "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { - "version": "0.1.82", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.82.tgz", - "integrity": "sha512-YIUpmHWeHGGRhWitT1KJkgj/JPXPfc9ox8oUoyaGPxolLGPp5AxJkq8wIg8CdFGtutget968dtwmx71m8o3h5g==", + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.100.tgz", + "integrity": "sha512-d5cDB48oWFGU8/XPhUOFAlySgb/VAu7D+s8fi55K1Pcfg8aPplHWqMgibhVLU8ky7Pyg/fuiVLz4Nf3JrSTuUA==", "cpu": [ "arm" ], @@ -908,12 +925,16 @@ ], "engines": { "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" } }, "node_modules/@napi-rs/canvas-linux-arm64-gnu": { - "version": "0.1.82", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.82.tgz", - "integrity": "sha512-AwLzwLBgmvk7kWeUgItOUor/QyG31xqtD26w1tLpf4yE0hiXTGp23yc669aawjB6FzgIkjh1NKaNS52B7/qEBQ==", + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.100.tgz", + "integrity": "sha512-rDxgxRu69RvDlX/bh9o22DxLsGr8EqsNgotL9+RwQE1S0b0cqeatqsw6aW45mukm0B42DIAaAacKaYQ8cqS1nw==", "cpu": [ "arm64" ], @@ -924,12 +945,16 @@ ], "engines": { "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" } }, "node_modules/@napi-rs/canvas-linux-arm64-musl": { - "version": "0.1.82", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.82.tgz", - "integrity": "sha512-moZWuqepAwWBffdF4JDadt8TgBD02iMhG6I1FHZf8xO20AsIp9rB+p0B8Zma2h2vAF/YMjeFCDmW5un6+zZz9g==", + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.100.tgz", + "integrity": "sha512-K3mDW66N+xT2/V439u1alFANiBUjdEx2gLiNYnCmUsva5jZMxWTjafBYwTzYK+EMFMHrUoabuU+T1BIP5CgbYQ==", "cpu": [ "arm64" ], @@ -940,12 +965,16 @@ ], "engines": { "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" } }, "node_modules/@napi-rs/canvas-linux-riscv64-gnu": { - "version": "0.1.82", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.82.tgz", - "integrity": "sha512-w9++2df2kG9eC9LWYIHIlMLuhIrKGQYfUxs97CwgxYjITeFakIRazI9LYWgVzEc98QZ9x9GQvlicFsrROV59MQ==", + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.100.tgz", + "integrity": "sha512-mooqUBTIsccZpnoQC4NgrC1v6C1vof39etLNMnBwCY+p0gajWJvAHLGQ6g/gGyS5YrpDW+GefSN4+Cvcr08UWw==", "cpu": [ "riscv64" ], @@ -956,12 +985,16 @@ ], "engines": { "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" } }, "node_modules/@napi-rs/canvas-linux-x64-gnu": { - "version": "0.1.82", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.82.tgz", - "integrity": "sha512-lZulOPwrRi6hEg/17CaqdwWEUfOlIJuhXxincx1aVzsVOCmyHf+xFq4i6liJl1P+x2v6Iz2Z/H5zHvXJCC7Bwg==", + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.100.tgz", + "integrity": "sha512-1eCvkDCazm7FFhsT7DfGOdSaHgZVK3bt/dSBl5EWHOWmnz+I7j8tPseJqqD81NF+MH21jKUK4wQSDjN0mdhnTg==", "cpu": [ "x64" ], @@ -972,12 +1005,16 @@ ], "engines": { "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" } }, "node_modules/@napi-rs/canvas-linux-x64-musl": { - "version": "0.1.82", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.82.tgz", - "integrity": "sha512-Be9Wf5RTv1w6GXlTph55K3PH3vsAh1Ax4T1FQY1UYM0QfD0yrwGdnJ8/fhqw7dEgMjd59zIbjJQC8C3msbGn5g==", + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.100.tgz", + "integrity": "sha512-20arT6lnI19S68qNlii73TSEDbECNgzMz2EpldC1V3mZFuRkeujXkcebRk0LRJe9SEUAooYiLokfMViY8IX7yA==", "cpu": [ "x64" ], @@ -988,12 +1025,36 @@ ], "engines": { "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-win32-arm64-msvc": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-0.1.100.tgz", + "integrity": "sha512-DZFFT1wIAg37LJw37yhMRFfjATd3vTQzjZ1Yki8u2vhO6Hi5VE6BVaGQ1aaDu7xb4iMErz+9EOwjpS7xcxFeBw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" } }, "node_modules/@napi-rs/canvas-win32-x64-msvc": { - "version": "0.1.82", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.82.tgz", - "integrity": "sha512-LN/i8VrvxTDmEEK1c10z2cdOTkWT76LlTGtyZe5Kr1sqoSomKeExAjbilnu1+oee5lZUgS5yfZ2LNlVhCeARuw==", + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.100.tgz", + "integrity": "sha512-MyT1j3mHC2+Lu4pBi9mKyMJhtP6U7k7EldY7sj/uS5gJA65gTXt8MefJQXLJo5d/vZbuWmfxzkEUNc/urV3pHA==", "cpu": [ "x64" ], @@ -1004,6 +1065,10 @@ ], "engines": { "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" } }, "node_modules/@napi-rs/wasm-runtime": { @@ -1456,34 +1521,26 @@ "undici-types": ">=7.24.0 <7.24.7" } }, - "node_modules/@types/prop-types": { - "version": "15.7.15", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", - "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/react": { - "version": "18.3.27", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", - "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@types/prop-types": "*", "csstype": "^3.2.2" } }, "node_modules/@types/react-dom": { - "version": "18.3.7", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", - "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "dev": true, "license": "MIT", "peer": true, "peerDependencies": { - "@types/react": "^18.0.0" + "@types/react": "^19.2.0" } }, "node_modules/@typescript-eslint/eslint-plugin": { @@ -2662,6 +2719,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, "license": "MIT" }, "node_modules/jsdom": { @@ -3054,18 +3112,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, "node_modules/lru-cache": { "version": "11.3.6", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz", @@ -3279,15 +3325,15 @@ } }, "node_modules/pdfjs-dist": { - "version": "4.10.38", - "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-4.10.38.tgz", - "integrity": "sha512-/Y3fcFrXEAsMjJXeL9J8+ZG9U01LbuWaYypvDW2ycW1jL269L3js3DVBjDJ0Up9Np1uqDXsDrRihHANhZOlwdQ==", + "version": "5.7.284", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.7.284.tgz", + "integrity": "sha512-h4EdYQczmGhbOlqc3PPZwxevn7ApdWPbovAuWXOB/DjIyigSnwfy2oze7c6mRcSr9XgLp3eN3EeL4DyySTPMFw==", "license": "Apache-2.0", "engines": { - "node": ">=20" + "node": ">=22.13.0 || >=24" }, "optionalDependencies": { - "@napi-rs/canvas": "^0.1.65" + "@napi-rs/canvas": "^0.1.100" } }, "node_modules/picocolors": { @@ -3392,30 +3438,26 @@ } }, "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", + "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", "license": "MIT", "peer": true, - "dependencies": { - "loose-envify": "^1.1.0" - }, "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", + "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", "license": "MIT", "peer": true, "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" + "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^18.3.1" + "react": "^19.2.6" } }, "node_modules/react-is": { @@ -3490,13 +3532,10 @@ } }, "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - } + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" }, "node_modules/semver": { "version": "6.3.1", @@ -3692,9 +3731,9 @@ } }, "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, "license": "Apache-2.0", "peer": true, diff --git a/package.json b/package.json index c08dd00..768ca2b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pdf-tools", - "version": "0.2.1", + "version": "0.3.0", "private": true, "type": "module", "scripts": { @@ -17,17 +17,17 @@ }, "dependencies": { "pdf-lib": "^1.17.1", - "pdfjs-dist": "^4.6.82", - "react": "^18.3.1", - "react-dom": "^18.3.1" + "pdfjs-dist": "^5.7.284", + "react": "^19.2.6", + "react-dom": "^19.2.6" }, "devDependencies": { "@eslint/js": "^10.0.1", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", "@types/node": "^25.8.0", - "@types/react": "^18.3.11", - "@types/react-dom": "^18.3.2", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.2", "eslint": "^10.4.0", "eslint-config-prettier": "^10.1.8", @@ -36,7 +36,7 @@ "globals": "^17.6.0", "jsdom": "^29.1.1", "prettier": "^3.8.3", - "typescript": "^5.6.3", + "typescript": "^6.0.3", "typescript-eslint": "^8.59.3", "vite": "^8.0.13", "vitest": "^4.1.6" diff --git a/pdf-tools-v0.3.0.zip b/pdf-tools-v0.3.0.zip new file mode 100644 index 0000000..3a49d7c Binary files /dev/null and b/pdf-tools-v0.3.0.zip differ diff --git a/pdf-tools.zip b/pdf-tools.zip deleted file mode 100644 index b93bc3e..0000000 Binary files a/pdf-tools.zip and /dev/null differ diff --git a/src/App.tsx b/src/App.tsx index 24e7ae9..63d2d65 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -17,6 +17,7 @@ import type { import { createInitialPageRefs, createPageRefId, + createPdfId, createWorkspaceId, defaultWorkspaceNameFromPdfName, normalizeRotation, @@ -37,6 +38,11 @@ import { } from "./pdf/pdfService"; import { usePdfThumbnails } from "./pdf/usePdfThumbnails"; import { usePdfGeneratedOutputs } from "./hooks/usePdfGeneratedOutputs"; +import { + createSelectionPdfName, + createSelectionWorkspaceName, + getSelectedPagesInVisualOrder, +} from './workspace/workspaceSelection'; function isEditableKeyboardTarget(target: EventTarget | null): boolean { if (!(target instanceof HTMLElement)) return false; @@ -127,6 +133,21 @@ const App: React.FC = () => { onError: handleThumbnailError, }); + const closeActionDialog = useCallback(() => { + setActionDialog(null); + }, []); + + const openActionDialog = useCallback( + (dialog: { + title: string; + content: React.ReactNode; + actions: ActionDialogAction[]; + }) => { + setActionDialog(dialog); + }, + [] + ); + const refreshWorkspaces = async () => { try { const summaries = await listWorkspaces(); @@ -668,7 +689,41 @@ const App: React.FC = () => { setLastSelectedVisualIndex(null); }; - const handleDeleteSelected = () => { + const performDeleteSelected = useCallback( + (pageIdsToDelete: string[]) => { + if (pageIdsToDelete.length === 0) return; + + const before = getCurrentCommandState(); + const selectedSet = new Set(pageIdsToDelete); + + executeWorkspaceCommand( + createWorkspaceCommand({ + type: 'pages.delete', + label: + pageIdsToDelete.length === 1 + ? 'Deleted selected page' + : `Deleted ${pageIdsToDelete.length} selected pages`, + before, + after: { + pages: pages.filter((page) => !selectedSet.has(page.id)), + selectedPageIds: [], + lastSelectedVisualIndex: null, + }, + details: { + count: pageIdsToDelete.length, + }, + }) + ); + }, + [ + createWorkspaceCommand, + executeWorkspaceCommand, + getCurrentCommandState, + pages, + ] + ); + + const handleDeleteSelected = useCallback(() => { if (selectedPageIds.length === 0) return; const idsToDelete = [...selectedPageIds]; @@ -706,33 +761,12 @@ const App: React.FC = () => { }, ], }); - }; - - const performDeleteSelected = (pageIdsToDelete: string[]) => { - if (pageIdsToDelete.length === 0) return; - - const before = getCurrentCommandState(); - const selectedSet = new Set(pageIdsToDelete); - - executeWorkspaceCommand( - createWorkspaceCommand({ - type: "pages.delete", - label: - pageIdsToDelete.length === 1 - ? "Deleted selected page" - : `Deleted ${pageIdsToDelete.length} selected pages`, - before, - after: { - pages: pages.filter((page) => !selectedSet.has(page.id)), - selectedPageIds: [], - lastSelectedVisualIndex: null, - }, - details: { - count: pageIdsToDelete.length, - }, - }), - ); - }; + }, [ + closeActionDialog, + openActionDialog, + performDeleteSelected, + selectedPageIds, + ]); const handleCopyPagesToSlot = (pageIds: string[], insertSlot: number) => { if (!pdf || pageIds.length === 0) return; @@ -780,18 +814,6 @@ const App: React.FC = () => { ); }; - const closeActionDialog = () => { - setActionDialog(null); - }; - - const openActionDialog = (dialog: { - title: string; - content: React.ReactNode; - actions: ActionDialogAction[]; - }) => { - setActionDialog(dialog); - }; - const handleOpenPreview = (pageId: string) => { setPreviewPageId(pageId); }; @@ -909,8 +931,13 @@ const App: React.FC = () => { setIsBusy(true); try { - const selectedSet = new Set(selectedPageIds); - const selectedPages = pages.filter((page) => selectedSet.has(page.id)); + const selectedPages = getSelectedPagesInVisualOrder( + pages, + selectedPageIds + ); + + if (selectedPages.length === 0) return; + const blob = await exportPages(pdf, selectedPages); const base = pdf.name.replace(/\.pdf$/i, ""); const filename = `${base}_selected.pdf`; @@ -923,6 +950,117 @@ const App: React.FC = () => { } }; + + const performOpenSelectionAsWorkspace = async () => { + if (!pdf || selectedPageIds.length === 0) return; + + const selectedPages = getSelectedPagesInVisualOrder(pages, selectedPageIds); + if (selectedPages.length === 0) return; + + setError(null); + setIsBusy(true); + + try { + const selectedPageCount = selectedPages.length; + const blob = await exportPages(pdf, selectedPages); + const arrayBuffer = await blob.arrayBuffer(); + const doc = await PDFDocument.load(arrayBuffer); + const pdfName = createSelectionPdfName(pdf.name, selectedPageCount); + const workspaceName = createSelectionWorkspaceName( + pdf.name, + selectedPageCount + ); + + const extractedPdf: PdfFile = { + id: createPdfId(), + name: pdfName, + doc, + pageCount: doc.getPageCount(), + arrayBuffer, + }; + + setPdf(extractedPdf); + replaceWorkspaceState({ + pages: createInitialPageRefs(extractedPdf.pageCount), + selectedPageIds: [], + lastSelectedVisualIndex: null, + history: [], + redoHistory: [], + dirty: true, + message: `Created a new workspace from ${selectedPageCount} selected ${ + selectedPageCount === 1 ? "page" : "pages" + }.`, + }); + + setActiveWorkspaceId(null); + setWorkspaceName(workspaceName); + setPreviewPageId(null); + clearGeneratedOutputs(); + clearThumbnailCache(); + } catch (e) { + console.error(e); + setError("Error while opening selection as a new workspace."); + } finally { + setIsBusy(false); + } + }; + + const handleOpenSelectionAsWorkspace = () => { + if (!pdf || selectedPageIds.length === 0) return; + + const selectedPages = getSelectedPagesInVisualOrder(pages, selectedPageIds); + if (selectedPages.length === 0) return; + + if (!workspaceDirty) { + void performOpenSelectionAsWorkspace(); + return; + } + + openActionDialog({ + title: "Open selection as new workspace?", + content: ( + <> +

+ This will replace the current in-memory workspace with a new + workspace built from {selectedPages.length}{' '} + {selectedPages.length === 1 ? "selected page" : "selected pages"}. +

+

+ The current workspace has unsaved changes. Do you want to save it + before opening the selection as a new workspace? +

+ + ), + actions: [ + { + label: "Cancel", + variant: "secondary", + onClick: closeActionDialog, + }, + { + label: "Open without saving", + variant: "danger", + onClick: () => { + closeActionDialog(); + void performOpenSelectionAsWorkspace(); + }, + }, + { + label: "Save and open", + variant: "primary", + autoFocus: true, + onClick: async () => { + closeActionDialog(); + const saved = await handleSaveWorkspace(); + if (saved) { + await performOpenSelectionAsWorkspace(); + } + }, + }, + ], + }); + }; + const handleExportReordered = async () => { if (!pdf || pages.length === 0) return; setError(null); @@ -1102,6 +1240,7 @@ const App: React.FC = () => { selectedCount={selectedPageIds.length} onSplit={handleSplit} onExtractSelected={handleExtractSelected} + onOpenSelectionAsWorkspace={handleOpenSelectionAsWorkspace} onExportReordered={handleExportReordered} splitDownloads={splitDownloads} subsetDownload={subsetDownload} diff --git a/src/components/ActionsPanel.tsx b/src/components/ActionsPanel.tsx index da69a23..ab0cf59 100644 --- a/src/components/ActionsPanel.tsx +++ b/src/components/ActionsPanel.tsx @@ -12,6 +12,7 @@ interface ActionsPanelProps { onSplit: () => void; onExtractSelected: () => void; + onOpenSelectionAsWorkspace: () => void; onExportReordered: () => void; splitDownloads: SplitPdfDownload[]; @@ -25,6 +26,7 @@ const ActionsPanel: React.FC = ({ selectedCount, onSplit, onExtractSelected, + onOpenSelectionAsWorkspace, onExportReordered, splitDownloads, subsetDownload, @@ -37,6 +39,11 @@ const ActionsPanel: React.FC = ({ onExtractSelected(); }; + const handleOpenSelectionAsWorkspaceClick = () => { + if (selectedCount === 0) return; + onOpenSelectionAsWorkspace(); + }; + return (

Tools

@@ -72,6 +79,20 @@ const ActionsPanel: React.FC = ({ 📤 Extract selected ({selectedCount}) + +