diff --git a/e2e/pdf-splitter.spec.ts b/e2e/pdf-splitter.spec.ts
new file mode 100644
index 0000000..b7a4eae
--- /dev/null
+++ b/e2e/pdf-splitter.spec.ts
@@ -0,0 +1,66 @@
+import { test, expect } from '@playwright/test';
+
+test('pdf splitter: tool page loads with drop zone', async ({ page }) => {
+ await page.goto('/#/tools/pdf-splitter');
+ await expect(page.locator('.tool-layout')).toBeVisible();
+ await expect(page.locator('.pdf-drop-zone')).toBeVisible();
+});
+
+test('pdf splitter: drop zone contains upload hint text', async ({ page }) => {
+ await page.goto('/#/tools/pdf-splitter');
+ await expect(page.locator('.pdf-drop-zone')).toContainText('Drop a PDF here');
+ await expect(page.locator('.pdf-drop-zone')).toContainText('100% in-browser');
+});
+
+test('pdf splitter: appears on dev page', async ({ page }) => {
+ await page.goto('/#/dev');
+ await expect(page.getByText('PDF Page Splitter')).toBeVisible();
+});
+
+test('pdf splitter: shows error for non-PDF file', async ({ page }) => {
+ await page.goto('/#/tools/pdf-splitter');
+
+ // Inject a non-PDF file via the hidden file input.
+ const fileInput = page.locator('input[type="file"]');
+ await fileInput.setInputFiles({
+ name: 'notes.txt',
+ mimeType: 'text/plain',
+ buffer: Buffer.from('hello world'),
+ });
+
+ await expect(page.locator('.tool-message--error')).toBeVisible();
+ await expect(page.locator('.tool-message--error')).toContainText(/pdf/i);
+});
+
+test('pdf splitter: processes a minimal valid PDF', async ({ page }) => {
+ await page.goto('/#/tools/pdf-splitter');
+
+ // Minimal valid 1-page PDF (uncompressed, hand-crafted).
+ const minimalPdf = `%PDF-1.4
+1 0 obj<>endobj
+2 0 obj<>endobj
+3 0 obj<>>>endobj
+xref
+0 4
+0000000000 65535 f
+0000000009 00000 n
+0000000058 00000 n
+0000000115 00000 n
+trailer<>
+startxref
+217
+%%EOF`;
+
+ const fileInput = page.locator('input[type="file"]');
+ await fileInput.setInputFiles({
+ name: 'test.pdf',
+ mimeType: 'application/pdf',
+ buffer: Buffer.from(minimalPdf),
+ });
+
+ // Should show at least one page card (the split result)
+ // The loading message or page grid should appear β wait for either the grid or an error.
+ await expect(
+ page.locator('.pdf-page-grid, .tool-message--error'),
+ ).toBeVisible({ timeout: 15000 });
+});
diff --git a/e2e/search.spec.ts b/e2e/search.spec.ts
index e5249f9..5b4bc9f 100644
--- a/e2e/search.spec.ts
+++ b/e2e/search.spec.ts
@@ -37,7 +37,7 @@ test('search: Escape clears query and shows all tools', async ({ page }) => {
await page.locator(SEARCH).press('Escape');
await expect(page.locator(SEARCH)).toHaveValue('');
- await expect(page.locator('.tool-card')).toHaveCount(15);
+ await expect(page.locator(".tool-card")).toHaveCount(10); // PAGE_SIZE
});
test('search: result count shown while filtering', async ({ page }) => {
diff --git a/e2e/smoke.spec.ts b/e2e/smoke.spec.ts
index e53cdd7..ff4f515 100644
--- a/e2e/smoke.spec.ts
+++ b/e2e/smoke.spec.ts
@@ -16,6 +16,7 @@ const TOOL_IDS = [
'music-box-designer',
'tuner',
'jsonpath',
+ 'pdf-splitter',
];
test('home page loads and shows tool cards', async ({ page }) => {
@@ -244,6 +245,17 @@ test('music page shows tuner card', async ({ page }) => {
await expect(page.getByText('Tuner')).toBeVisible();
});
+test('pdf splitter renders drop zone', async ({ page }) => {
+ await page.goto('/#/tools/pdf-splitter');
+ await expect(page.locator('.tool-layout')).toBeVisible();
+ await expect(page.locator('.pdf-drop-zone')).toBeVisible();
+});
+
+test('dev page shows pdf splitter card', async ({ page }) => {
+ await page.goto('/#/dev');
+ await expect(page.getByText('PDF Page Splitter')).toBeVisible();
+});
+
for (const id of TOOL_IDS) {
test(`tool page loads: /tools/${id}`, async ({ page }) => {
// App uses HashRouter, so tool routes are under /#/tools/:id
diff --git a/package-lock.json b/package-lock.json
index 4885a5a..c773472 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,7 +15,10 @@
"@codemirror/view": "^6.40.0",
"codemirror": "^6.0.2",
"jsonpath-plus": "^10.4.0",
+ "jszip": "^3.10.1",
"papaparse": "^5.5.3",
+ "pdf-lib": "^1.17.1",
+ "pdfjs-dist": "^5.5.207",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-router-dom": "^7.13.1",
@@ -1408,6 +1411,274 @@
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
"license": "MIT"
},
+ "node_modules/@napi-rs/canvas": {
+ "version": "0.1.97",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.97.tgz",
+ "integrity": "sha512-8cFniXvrIEnVwuNSRCW9wirRZbHvrD3JVujdS2P5n5xiJZNZMOZcfOvJ1pb66c7jXMKHHglJEDVJGbm8XWFcXQ==",
+ "license": "MIT",
+ "optional": true,
+ "workspaces": [
+ "e2e/*"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ },
+ "optionalDependencies": {
+ "@napi-rs/canvas-android-arm64": "0.1.97",
+ "@napi-rs/canvas-darwin-arm64": "0.1.97",
+ "@napi-rs/canvas-darwin-x64": "0.1.97",
+ "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.97",
+ "@napi-rs/canvas-linux-arm64-gnu": "0.1.97",
+ "@napi-rs/canvas-linux-arm64-musl": "0.1.97",
+ "@napi-rs/canvas-linux-riscv64-gnu": "0.1.97",
+ "@napi-rs/canvas-linux-x64-gnu": "0.1.97",
+ "@napi-rs/canvas-linux-x64-musl": "0.1.97",
+ "@napi-rs/canvas-win32-arm64-msvc": "0.1.97",
+ "@napi-rs/canvas-win32-x64-msvc": "0.1.97"
+ }
+ },
+ "node_modules/@napi-rs/canvas-android-arm64": {
+ "version": "0.1.97",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.97.tgz",
+ "integrity": "sha512-V1c/WVw+NzH8vk7ZK/O8/nyBSCQimU8sfMsB/9qeSvdkGKNU7+mxy/bIF0gTgeBFmHpj30S4E9WHMSrxXGQuVQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-darwin-arm64": {
+ "version": "0.1.97",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.97.tgz",
+ "integrity": "sha512-ok+SCEF4YejcxuJ9Rm+WWunHHpf2HmiPxfz6z1a/NFQECGXtsY7A4B8XocK1LmT1D7P174MzwPF9Wy3AUAwEPw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-darwin-x64": {
+ "version": "0.1.97",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.97.tgz",
+ "integrity": "sha512-PUP6e6/UGlclUvAQNnuXCcnkpdUou6VYZfQOQxExLp86epOylmiwLkqXIvpFmjoTEDmPmXrI+coL/9EFU1gKPA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": {
+ "version": "0.1.97",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.97.tgz",
+ "integrity": "sha512-XyXH2L/cic8eTNtbrXCcvqHtMX/nEOxN18+7rMrAM2XtLYC/EB5s0wnO1FsLMWmK+04ZSLN9FBGipo7kpIkcOw==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-linux-arm64-gnu": {
+ "version": "0.1.97",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.97.tgz",
+ "integrity": "sha512-Kuq/M3djq0K8ktgz6nPlK7Ne5d4uWeDxPpyKWOjWDK2RIOhHVtLtyLiJw2fuldw7Vn4mhw05EZXCEr4Q76rs9w==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-linux-arm64-musl": {
+ "version": "0.1.97",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.97.tgz",
+ "integrity": "sha512-kKmSkQVnWeqg7qdsiXvYxKhAFuHz3tkBjW/zyQv5YKUPhotpaVhpBGv5LqCngzyuRV85SXoe+OFj+Tv0a0QXkQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-linux-riscv64-gnu": {
+ "version": "0.1.97",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.97.tgz",
+ "integrity": "sha512-Jc7I3A51jnEOIAXeLsN/M/+Z28LUeakcsXs07FLq9prXc0eYOtVwsDEv913Gr+06IRo34gJJVgT0TXvmz+N2VA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-linux-x64-gnu": {
+ "version": "0.1.97",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.97.tgz",
+ "integrity": "sha512-iDUBe7AilfuBSRbSa8/IGX38Mf+iCSBqoVKLSQ5XaY2JLOaqz1TVyPFEyIck7wT6mRQhQt5sN6ogfjIDfi74tg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-linux-x64-musl": {
+ "version": "0.1.97",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.97.tgz",
+ "integrity": "sha512-AKLFd/v0Z5fvgqBDqhvqtAdx+fHMJ5t9JcUNKq4FIZ5WH+iegGm8HPdj00NFlCSnm83Fp3Ln8I2f7uq1aIiWaA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-win32-arm64-msvc": {
+ "version": "0.1.97",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-0.1.97.tgz",
+ "integrity": "sha512-u883Yr6A6fO7Vpsy9YE4FVCIxzzo5sO+7pIUjjoDLjS3vQaNMkVzx5bdIpEL+ob+gU88WDK4VcxYMZ6nmnoX9A==",
+ "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.97",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.97.tgz",
+ "integrity": "sha512-sWtD2EE3fV0IzN+iiQUqr/Q1SwqWhs2O1FKItFlxtdDkikpEj5g7DKQpY3x55H/MAOnL8iomnlk3mcEeGiUMoQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@pdf-lib/standard-fonts": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz",
+ "integrity": "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==",
+ "license": "MIT",
+ "dependencies": {
+ "pako": "^1.0.6"
+ }
+ },
+ "node_modules/@pdf-lib/upng": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@pdf-lib/upng/-/upng-1.0.1.tgz",
+ "integrity": "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==",
+ "license": "MIT",
+ "dependencies": {
+ "pako": "^1.0.10"
+ }
+ },
"node_modules/@playwright/test": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz",
@@ -2610,6 +2881,12 @@
"url": "https://opencollective.com/express"
}
},
+ "node_modules/core-util-is": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
+ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
+ "license": "MIT"
+ },
"node_modules/crelt": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
@@ -3239,6 +3516,12 @@
"node": ">= 4"
}
},
+ "node_modules/immediate": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
+ "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
+ "license": "MIT"
+ },
"node_modules/import-fresh": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
@@ -3266,6 +3549,12 @@
"node": ">=0.8.19"
}
},
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -3296,6 +3585,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+ "license": "MIT"
+ },
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -3438,6 +3733,18 @@
"node": ">=18.0.0"
}
},
+ "node_modules/jszip": {
+ "version": "3.10.1",
+ "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
+ "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
+ "license": "(MIT OR GPL-3.0-or-later)",
+ "dependencies": {
+ "lie": "~3.3.0",
+ "pako": "~1.0.2",
+ "readable-stream": "~2.3.6",
+ "setimmediate": "^1.0.5"
+ }
+ },
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -3462,6 +3769,15 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/lie": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
+ "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
+ "license": "MIT",
+ "dependencies": {
+ "immediate": "~3.0.5"
+ }
+ },
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@@ -3558,6 +3874,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/node-readable-to-web-readable-stream": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/node-readable-to-web-readable-stream/-/node-readable-to-web-readable-stream-0.4.2.tgz",
+ "integrity": "sha512-/cMZNI34v//jUTrI+UIo4ieHAB5EZRY/+7OmXZgBxaWBMcW2tGdceIw06RFxWxrKZ5Jp3sI2i5TsRo+CBhtVLQ==",
+ "license": "MIT",
+ "optional": true
+ },
"node_modules/node-releases": {
"version": "2.0.36",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz",
@@ -3626,6 +3949,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/pako": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
+ "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
+ "license": "(MIT AND Zlib)"
+ },
"node_modules/papaparse": {
"version": "5.5.3",
"resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.3.tgz",
@@ -3685,6 +4014,31 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/pdf-lib": {
+ "version": "1.17.1",
+ "resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz",
+ "integrity": "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==",
+ "license": "MIT",
+ "dependencies": {
+ "@pdf-lib/standard-fonts": "^1.0.0",
+ "@pdf-lib/upng": "^1.0.1",
+ "pako": "^1.0.11",
+ "tslib": "^1.11.1"
+ }
+ },
+ "node_modules/pdfjs-dist": {
+ "version": "5.5.207",
+ "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.5.207.tgz",
+ "integrity": "sha512-WMqqw06w1vUt9ZfT0gOFhMf3wHsWhaCrxGrckGs5Cci6ybDW87IvPaOd2pnBwT6BJuP/CzXDZxjFgmSULLdsdw==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=20.19.0 || >=22.13.0 || >=24"
+ },
+ "optionalDependencies": {
+ "@napi-rs/canvas": "^0.1.95",
+ "node-readable-to-web-readable-stream": "^0.4.2"
+ }
+ },
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -3791,6 +4145,12 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/process-nextick-args": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
+ "license": "MIT"
+ },
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -3870,6 +4230,21 @@
"react-dom": ">=18"
}
},
+ "node_modules/readable-stream": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+ "license": "MIT",
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
"node_modules/require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
@@ -3935,6 +4310,12 @@
"fsevents": "~2.3.2"
}
},
+ "node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "license": "MIT"
+ },
"node_modules/saxes": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
@@ -3970,6 +4351,12 @@
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
"license": "MIT"
},
+ "node_modules/setimmediate": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
+ "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
+ "license": "MIT"
+ },
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -4036,6 +4423,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
"node_modules/strip-json-comments": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
@@ -4178,6 +4574,12 @@
"typescript": ">=4.8.4"
}
},
+ "node_modules/tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
+ "license": "0BSD"
+ },
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -4287,6 +4689,12 @@
"punycode": "^2.1.0"
}
},
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "license": "MIT"
+ },
"node_modules/vite": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
diff --git a/package.json b/package.json
index 62ddd85..f5872d8 100644
--- a/package.json
+++ b/package.json
@@ -20,7 +20,10 @@
"@codemirror/view": "^6.40.0",
"codemirror": "^6.0.2",
"jsonpath-plus": "^10.4.0",
+ "jszip": "^3.10.1",
"papaparse": "^5.5.3",
+ "pdf-lib": "^1.17.1",
+ "pdfjs-dist": "^5.5.207",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-router-dom": "^7.13.1",
diff --git a/src/App.tsx b/src/App.tsx
index eafb210..193053e 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -7,6 +7,8 @@ import { Link, Navigate, Route, Routes, useLocation, useNavigate, useParams, use
import { TOOLS, type ToolMeta } from './registry/tools';
import { ToolCard } from './components/ToolCard';
import { ToolPage } from './components/ToolPage';
+import { Pagination } from './components/Pagination';
+import { usePagination } from './hooks/usePagination';
const Base64Tool = lazy(() => import('./tools/base64/Base64Tool').then(m => ({ default: m.Base64Tool })));
const JsonFormatterTool = lazy(() => import('./tools/json-formatter/JsonFormatterTool').then(m => ({ default: m.JsonFormatterTool })));
@@ -23,6 +25,7 @@ const MetronomeTool = lazy(() => import('./tools/metronome/MetronomeTool').then(
const MusicBoxDesignerTool = lazy(() => import('./tools/music-box-designer/MusicBoxDesignerTool').then(m => ({ default: m.MusicBoxDesignerTool })));
const TunerTool = lazy(() => import('./tools/tuner/TunerTool').then(m => ({ default: m.TunerTool })));
const JsonPathTool = lazy(() => import('./tools/jsonpath/JsonPathTool').then(m => ({ default: m.JsonPathTool })));
+const PdfSplitterTool = lazy(() => import('./tools/pdf-splitter/PdfSplitterTool').then(m => ({ default: m.PdfSplitterTool })));
const SettingsPage = lazy(() => import('./tools/settings/SettingsPage').then(m => ({ default: m.SettingsPage })));
function renderTool(id: string) {
@@ -42,6 +45,7 @@ function renderTool(id: string) {
case 'music-box-designer': return
Tool not found.
; } } @@ -62,6 +66,7 @@ function HomePage() { const [searchParams] = useSearchParams(); const query = (searchParams.get('q') ?? '').slice(0, 80); const filtered = filterTools(TOOLS, query); + const { currentPage, totalPages, pagedItems, setPage } = usePagination(filtered); return (JSON, YAML, CSV, and more (in-browser).
Browser-based music tools β no plugins needed.
Tools for working with text and patterns.
Encode, decode, and hash data in-browser.
Handy utilities for developers.