From af57ae02dd3c280a759d41d88053d7d8ca4e604b Mon Sep 17 00:00:00 2001 From: Reece Browne <74901996+reecebrowne@users.noreply.github.com> Date: Mon, 13 Oct 2025 15:03:07 +0100 Subject: [PATCH] Feature/v2/viewer tabs (#4646) # Description of Changes --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: James Brunton Co-authored-by: Claude --- frontend/package-lock.json | 273 +++++++++--------- frontend/package.json | 36 +-- .../fileEditor/FileEditorThumbnail.tsx | 7 + frontend/src/components/layout/Workbench.tsx | 19 +- .../components/shared/FileDropdownMenu.tsx | 78 +++++ .../src/components/shared/TopControls.tsx | 47 ++- .../src/components/viewer/EmbedPdfViewer.tsx | 51 +++- .../src/components/viewer/LocalEmbedPDF.tsx | 93 +++--- .../viewer/LocalEmbedPDFWithAnnotations.tsx | 6 +- .../components/viewer/ThumbnailSidebar.tsx | 14 +- frontend/src/components/viewer/Viewer.tsx | 2 + frontend/src/contexts/ViewerContext.tsx | 9 + frontend/src/index.css | 9 + frontend/src/tools/Sign.tsx | 29 +- frontend/src/utils/signatureFlattening.ts | 11 +- 15 files changed, 435 insertions(+), 249 deletions(-) create mode 100644 frontend/src/components/shared/FileDropdownMenu.tsx diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 51e500cb7..e74347837 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -10,24 +10,24 @@ "license": "SEE LICENSE IN https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/refs/heads/main/proprietary/LICENSE", "dependencies": { "@atlaskit/pragmatic-drag-and-drop": "^1.7.7", - "@embedpdf/core": "^1.3.1", - "@embedpdf/engines": "^1.3.1", - "@embedpdf/plugin-annotation": "^1.3.1", - "@embedpdf/plugin-export": "^1.3.1", - "@embedpdf/plugin-history": "^1.3.1", - "@embedpdf/plugin-interaction-manager": "^1.3.1", - "@embedpdf/plugin-loader": "^1.3.1", - "@embedpdf/plugin-pan": "^1.3.1", - "@embedpdf/plugin-render": "^1.3.1", - "@embedpdf/plugin-rotate": "^1.3.1", - "@embedpdf/plugin-scroll": "^1.3.1", - "@embedpdf/plugin-search": "^1.3.1", - "@embedpdf/plugin-selection": "^1.3.1", - "@embedpdf/plugin-spread": "^1.3.1", - "@embedpdf/plugin-thumbnail": "^1.3.1", - "@embedpdf/plugin-tiling": "^1.3.1", - "@embedpdf/plugin-viewport": "^1.3.1", - "@embedpdf/plugin-zoom": "^1.3.1", + "@embedpdf/core": "^1.3.14", + "@embedpdf/engines": "^1.3.14", + "@embedpdf/plugin-annotation": "^1.3.14", + "@embedpdf/plugin-export": "^1.3.14", + "@embedpdf/plugin-history": "^1.3.14", + "@embedpdf/plugin-interaction-manager": "^1.3.14", + "@embedpdf/plugin-loader": "^1.3.14", + "@embedpdf/plugin-pan": "^1.3.14", + "@embedpdf/plugin-render": "^1.3.14", + "@embedpdf/plugin-rotate": "^1.3.14", + "@embedpdf/plugin-scroll": "^1.3.14", + "@embedpdf/plugin-search": "^1.3.14", + "@embedpdf/plugin-selection": "^1.3.14", + "@embedpdf/plugin-spread": "^1.3.14", + "@embedpdf/plugin-thumbnail": "^1.3.14", + "@embedpdf/plugin-tiling": "^1.3.14", + "@embedpdf/plugin-viewport": "^1.3.14", + "@embedpdf/plugin-zoom": "^1.3.14", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", "@iconify/react": "^6.0.2", @@ -497,13 +497,13 @@ } }, "node_modules/@embedpdf/core": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@embedpdf/core/-/core-1.3.1.tgz", - "integrity": "sha512-2Az6trhiMMBIv+GFvV8H8UOS1gwQn7NK0KaJMcdsZbUHYLO0P95aVd6Pi/GRzEH4XyF51TDIoTOAUtf07TQ5dQ==", + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@embedpdf/core/-/core-1.3.14.tgz", + "integrity": "sha512-lE/vfhA53CxamaCfGWEibrEPr+JeZT42QCF+cOELUwv4+Zt6b+IE6+4wsznx/8wjjJYwllXJ3GJ/un1UzTqARw==", "license": "MIT", "dependencies": { - "@embedpdf/engines": "1.3.1", - "@embedpdf/models": "1.3.1" + "@embedpdf/engines": "1.3.14", + "@embedpdf/models": "1.3.14" }, "peerDependencies": { "preact": "^10.26.4", @@ -513,13 +513,13 @@ } }, "node_modules/@embedpdf/engines": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@embedpdf/engines/-/engines-1.3.1.tgz", - "integrity": "sha512-G3pI+18la7spviUMuA5s9/hV95jlfkA2+CNxqlHBO5ocw3641E3d36Lv+mx+6yU7k0B5vEOQPZDGRMg7KFziBQ==", + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@embedpdf/engines/-/engines-1.3.14.tgz", + "integrity": "sha512-+/FPW2gAzj2lQYvsMH/Oj9+MEXgkyEuyYDC+HFkltTuXvmiP2S/3BD0YslZDX9K4BzcmMxnWB+BiQpNJokbDVg==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.3.1", - "@embedpdf/pdfium": "1.3.1" + "@embedpdf/models": "1.3.14", + "@embedpdf/pdfium": "1.3.14" }, "peerDependencies": { "preact": "^10.26.4", @@ -529,31 +529,31 @@ } }, "node_modules/@embedpdf/models": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@embedpdf/models/-/models-1.3.1.tgz", - "integrity": "sha512-OzmO1rQAuOP/Y3aYXmW21dPNAx49olhr9ZO2hDdI0fbNBHTVGxnaKqOISxVmUz7TmhTwVBljERACnaA8Ib4b4Q==", + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@embedpdf/models/-/models-1.3.14.tgz", + "integrity": "sha512-BujY4bmr8b2DQdoZkOge03SzoRVoWxzfIQATLSPPtp4WiFh1U4BPp6cADlGuCwGkp6zBcH/aM4h8PwwA75d/eg==", "license": "MIT" }, "node_modules/@embedpdf/pdfium": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@embedpdf/pdfium/-/pdfium-1.3.1.tgz", - "integrity": "sha512-qYGSS5ntz6DSY9Cxw/aigvHqGB+AKJLEcymNTZOL0GdlBzZpL++dOIYNEYHO2Tm/lOQVpE7I0e+Xh2TvD8O1zQ==", + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@embedpdf/pdfium/-/pdfium-1.3.14.tgz", + "integrity": "sha512-TQMZabXzHmzvvfPwopubFcYgQuYV7POvMgjICYu3Pgfn3sgr+UdIUh3aNXR/COcl3q8sXPMFQ2GDuyOHR9QQnA==", "license": "MIT" }, "node_modules/@embedpdf/plugin-annotation": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-annotation/-/plugin-annotation-1.3.1.tgz", - "integrity": "sha512-mmePRYYBB8v8NIZ95XVfFkpyQ2QiKIGdWyvrPeJXSbL3/K6d6ix+o/jHBVvBWyTsQzdIlzs+FW8+iT0M1zkEow==", + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-annotation/-/plugin-annotation-1.3.14.tgz", + "integrity": "sha512-JJYqEWwUKCdBZsXCDq/CW96p3pVLn8N+XZ4W3OyL7djI2fvYC9x6ys9m82vwlSathAVOxk1D7xXiY8AzJQVF0Q==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.3.1", - "@embedpdf/utils": "1.3.1" + "@embedpdf/models": "1.3.14", + "@embedpdf/utils": "1.3.14" }, "peerDependencies": { - "@embedpdf/core": "1.3.1", - "@embedpdf/plugin-history": "1.3.1", - "@embedpdf/plugin-interaction-manager": "1.3.1", - "@embedpdf/plugin-selection": "1.3.1", + "@embedpdf/core": "1.3.14", + "@embedpdf/plugin-history": "1.3.14", + "@embedpdf/plugin-interaction-manager": "1.3.14", + "@embedpdf/plugin-selection": "1.3.14", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -561,15 +561,15 @@ } }, "node_modules/@embedpdf/plugin-export": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-export/-/plugin-export-1.3.1.tgz", - "integrity": "sha512-reb03vNPFP5GuIAFExMcuYBVYu/deVO2v8EoCwRZ/lzzYMORIkJjpNWDQPo9VfyGBh1x4/o3CHvxisU1Y1tDLg==", + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-export/-/plugin-export-1.3.14.tgz", + "integrity": "sha512-fMGp2YxvI4uTRIViUKxfnJts2Jw/vktEM45XUNGNSjT/kAW6znVNgdceYjpK++xU8CGs2grAQ1i5UvMd3aRNDA==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.3.1" + "@embedpdf/models": "1.3.14" }, "peerDependencies": { - "@embedpdf/core": "1.3.1", + "@embedpdf/core": "1.3.14", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -577,15 +577,15 @@ } }, "node_modules/@embedpdf/plugin-history": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-history/-/plugin-history-1.3.1.tgz", - "integrity": "sha512-HrPkWQmAk08mbHiOcIN4htVq5KJMqI9zSjAqaYQEhV/TugeHfWVpK+xMst/PzuFb14HWgk5gWXjtV5E4SDlw9w==", + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-history/-/plugin-history-1.3.14.tgz", + "integrity": "sha512-77hnNLp0W0FHw8lT7SeqzCgp8bOClfeOAPZdcInu/jPDhVASUGYbtE/0fkLhiaqPH7kyMirNCLif4sF6n4b5vg==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.3.1" + "@embedpdf/models": "1.3.14" }, "peerDependencies": { - "@embedpdf/core": "1.3.1", + "@embedpdf/core": "1.3.14", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -593,15 +593,15 @@ } }, "node_modules/@embedpdf/plugin-interaction-manager": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-interaction-manager/-/plugin-interaction-manager-1.3.1.tgz", - "integrity": "sha512-8h3y5a9tQ1fZlc4mP1/+XKyuHWwcQEm9AujKxy+6f6omtCBzpnKrH95bURgYOzQEBGY7d5C3HvG6JOlh0o1x3A==", + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-interaction-manager/-/plugin-interaction-manager-1.3.14.tgz", + "integrity": "sha512-nR0ZxNoTQtGqOHhweFh6QJ+nUJ4S4Ag1wWur6vAUAi8U95HUOfZhOEa0polZo0zR9WmmblGqRWjFM+mVSOoi1w==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.3.1" + "@embedpdf/models": "1.3.14" }, "peerDependencies": { - "@embedpdf/core": "1.3.1", + "@embedpdf/core": "1.3.14", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -609,15 +609,15 @@ } }, "node_modules/@embedpdf/plugin-loader": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-loader/-/plugin-loader-1.3.1.tgz", - "integrity": "sha512-NjNmA7TOs3E/zwb9I+YohzyGkxq8y5NUGu0MKgh2g41lZoFvyqTAjFPar+RjEiLX8iiJiwNZswyJsNrytmS3Xg==", + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-loader/-/plugin-loader-1.3.14.tgz", + "integrity": "sha512-KoJX1MacEWE2DrO1OeZeG/Ehz76//u+ida/xb4r9BfwqAp5TfYlksq09cOvcF8LMW5FY4pbAL+AHKI1Hjz+HNA==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.3.1" + "@embedpdf/models": "1.3.14" }, "peerDependencies": { - "@embedpdf/core": "1.3.1", + "@embedpdf/core": "1.3.14", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -625,17 +625,17 @@ } }, "node_modules/@embedpdf/plugin-pan": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-pan/-/plugin-pan-1.3.1.tgz", - "integrity": "sha512-lF1gkz/a77G3+Rr8MOefkGnPJ1i5xWnClXm2ZzYAl7PbOScp59/PaP7qeU7eMPC4FHQM81ZhCgVYGXogbaB8ww==", + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-pan/-/plugin-pan-1.3.14.tgz", + "integrity": "sha512-7EG+I5nn8yDCV8pT4x/g5mv7zJli2t3wPrh6Kt8uIpUorPHNb6J0Z67gl0uc/8rEasNzuKOuT0er46Y6/UYLzQ==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.3.1" + "@embedpdf/models": "1.3.14" }, "peerDependencies": { - "@embedpdf/core": "1.3.1", - "@embedpdf/plugin-interaction-manager": "1.3.1", - "@embedpdf/plugin-viewport": "1.3.1", + "@embedpdf/core": "1.3.14", + "@embedpdf/plugin-interaction-manager": "1.3.14", + "@embedpdf/plugin-viewport": "1.3.14", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -643,15 +643,15 @@ } }, "node_modules/@embedpdf/plugin-render": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-render/-/plugin-render-1.3.1.tgz", - "integrity": "sha512-c9oH097e1CVUpYF9RgZRfV/7XCJ0pf+svdT1wyM2MbWby06ti20oCwT9wf7BLY0hPQ7+eO3wunr1I1/y3MnVrw==", + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-render/-/plugin-render-1.3.14.tgz", + "integrity": "sha512-IPj7GCQXJBsY++JaU+z7y+FwX5NaDBj4YYV6hsHNtSGf42Y1AdlwJzDYetivG2bA84xmk7KgD1X2Y3eIFBhjwA==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.3.1" + "@embedpdf/models": "1.3.14" }, "peerDependencies": { - "@embedpdf/core": "1.3.1", + "@embedpdf/core": "1.3.14", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -659,15 +659,15 @@ } }, "node_modules/@embedpdf/plugin-rotate": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-rotate/-/plugin-rotate-1.3.1.tgz", - "integrity": "sha512-mRAlIW7IZAnCyDuYqN13yDc6yoNIYLUB4uYTUAR7vTIt021C8H5jDHk9TmLwcH0tQ8/R3yHuDm/XPAe0zfs81g==", + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-rotate/-/plugin-rotate-1.3.14.tgz", + "integrity": "sha512-OroEm11x/fPPXI9C0X+nm9LOjwaI0MvsToZRH+HpV60/FbQeOJvt6D8wThCDVLK95Na6A+JeYIMEu+Hiix7H+A==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.3.1" + "@embedpdf/models": "1.3.14" }, "peerDependencies": { - "@embedpdf/core": "1.3.1", + "@embedpdf/core": "1.3.14", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -675,16 +675,16 @@ } }, "node_modules/@embedpdf/plugin-scroll": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-scroll/-/plugin-scroll-1.3.1.tgz", - "integrity": "sha512-mDvK3DyBZC8/8pOEdJsWtSjCmV2ZuZJJ6xfspJpsaDVywo1Vq6M55BtKThkhqED6mqbFWTN9rP9cbWG8KDBWVA==", + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-scroll/-/plugin-scroll-1.3.14.tgz", + "integrity": "sha512-fQbt7OlRMLQJMuZj/Bzh0qpRxMw1ld5Qe/OTw8N54b/plljnFA52joE7cITl3H03huWWyHS3NKOScbw7f34dog==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.3.1" + "@embedpdf/models": "1.3.14" }, "peerDependencies": { - "@embedpdf/core": "1.3.1", - "@embedpdf/plugin-viewport": "1.3.1", + "@embedpdf/core": "1.3.14", + "@embedpdf/plugin-viewport": "1.3.14", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -692,16 +692,16 @@ } }, "node_modules/@embedpdf/plugin-search": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-search/-/plugin-search-1.3.1.tgz", - "integrity": "sha512-SLwYPQg1NJWytq2sd4MnWFmRVGgzwbohBedB2kH0ALsvdnoRYqgjR5HqAsKgoRJO/pphQhHlk3L1gLW62r6hqQ==", + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-search/-/plugin-search-1.3.14.tgz", + "integrity": "sha512-tlZEgR2tG+GSNnh2u1SjCxhUHfTDgcr38sE/xRK1bRLDGPZWlr6Ln7qP7JSWqeYBGni75sGrj0iZqcZbPWyJag==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.3.1" + "@embedpdf/models": "1.3.14" }, "peerDependencies": { - "@embedpdf/core": "1.3.1", - "@embedpdf/plugin-loader": "1.3.1", + "@embedpdf/core": "1.3.14", + "@embedpdf/plugin-loader": "1.3.14", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -709,17 +709,17 @@ } }, "node_modules/@embedpdf/plugin-selection": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-selection/-/plugin-selection-1.3.1.tgz", - "integrity": "sha512-yef2XB/zR7zjyeUB3Ul0SbTcXqu5isR0GtINkFwL7bJMok6HpYNDnMXSuo55BaxI0dOCnnCSZfoRkAgosnZ1uQ==", + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-selection/-/plugin-selection-1.3.14.tgz", + "integrity": "sha512-EXENuaAsse3rT6cjA1nYzyrNvoy62ojJl28wblCng6zcs3HSlGPemIQZAvaYKPUxoY608M+6nKlcMQ5neRnk/A==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.3.1" + "@embedpdf/models": "1.3.14" }, "peerDependencies": { - "@embedpdf/core": "1.3.1", - "@embedpdf/plugin-interaction-manager": "1.3.1", - "@embedpdf/plugin-viewport": "1.3.1", + "@embedpdf/core": "1.3.14", + "@embedpdf/plugin-interaction-manager": "1.3.14", + "@embedpdf/plugin-viewport": "1.3.14", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -727,16 +727,16 @@ } }, "node_modules/@embedpdf/plugin-spread": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-spread/-/plugin-spread-1.3.1.tgz", - "integrity": "sha512-RJ/kgJsFRdtWlPMXTW1feUSb6WHIvxtNRLgqzX8dlFIoyc4oZex2Vw+URo/VZuWSe/NvCIihQ20rkNAQJMnNMQ==", + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-spread/-/plugin-spread-1.3.14.tgz", + "integrity": "sha512-DVlk6tDgUoDRkp2S4Jc3LrRTuf4DPMlph9vywJw5z6Qpbh0vgcMnObg896/S0Eu5FgACNAj0WGcXpLrcrn5b9Q==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.3.1" + "@embedpdf/models": "1.3.14" }, "peerDependencies": { - "@embedpdf/core": "1.3.1", - "@embedpdf/plugin-loader": "1.3.1", + "@embedpdf/core": "1.3.14", + "@embedpdf/plugin-loader": "1.3.14", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -744,34 +744,35 @@ } }, "node_modules/@embedpdf/plugin-thumbnail": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-thumbnail/-/plugin-thumbnail-1.3.1.tgz", - "integrity": "sha512-xv96ESa7JgD5z+TzcOK18/u0gq3d9v7QPv2wpr0ZhcnwLwf4sH0eUJZIsv7z7DMOpBNz7o7jJbrtxDUdCEHGhg==", + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-thumbnail/-/plugin-thumbnail-1.3.14.tgz", + "integrity": "sha512-cnwb5dG8Jph8XSArys1WFCQ6kK2R5FKoO0B5mDrHFv9Fcm2pKszlmZC/NDoskX4pgNUgSnwhI1X3cP37ebF9Ng==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.3.1" + "@embedpdf/models": "1.3.14" }, "peerDependencies": { - "@embedpdf/core": "1.3.1", - "@embedpdf/plugin-render": "1.3.1", + "@embedpdf/core": "1.3.14", + "@embedpdf/plugin-render": "1.3.14", "preact": "^10.26.4", "react": ">=16.8.0", - "react-dom": ">=16.8.0" + "react-dom": ">=16.8.0", + "vue": ">=3.2.0" } }, "node_modules/@embedpdf/plugin-tiling": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-tiling/-/plugin-tiling-1.3.1.tgz", - "integrity": "sha512-Q8RF80fb6y9GDAKwvgsu0BsWJlQuhNCtSKWwp3YcZJtIBFm94DVcg0zTgvDmE9/WNOmn4Z1Edt86usmYauHolw==", + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-tiling/-/plugin-tiling-1.3.14.tgz", + "integrity": "sha512-SaCTo2LdZwGeE6jCqkwJxvwt8YKbsI3QGxa9S7Ez+5OcBchlhHeTfLQswcErDQ3WH2p8WHtGuucAcOLrVVOm0A==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.3.1" + "@embedpdf/models": "1.3.14" }, "peerDependencies": { - "@embedpdf/core": "1.3.1", - "@embedpdf/plugin-render": "1.3.1", - "@embedpdf/plugin-scroll": "1.3.1", - "@embedpdf/plugin-viewport": "1.3.1", + "@embedpdf/core": "1.3.14", + "@embedpdf/plugin-render": "1.3.14", + "@embedpdf/plugin-scroll": "1.3.14", + "@embedpdf/plugin-viewport": "1.3.14", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -779,15 +780,15 @@ } }, "node_modules/@embedpdf/plugin-viewport": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-viewport/-/plugin-viewport-1.3.1.tgz", - "integrity": "sha512-gzosrWL18ZhN175Kxocf/p7uqYBhNHvEuV1CpJQmN7ys48aew6Qq8z7MjAsCnJBANXk/8syNdo3qWwBriyjQNg==", + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-viewport/-/plugin-viewport-1.3.14.tgz", + "integrity": "sha512-mfJ7EbbU68eKk6oFvQ4ozGJNpxUxWbjQ5Gm3uuB+Gj5/tWgBocBOX36k/9LgivEEeX7g2S0tOgyErljApmH8Vg==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.3.1" + "@embedpdf/models": "1.3.14" }, "peerDependencies": { - "@embedpdf/core": "1.3.1", + "@embedpdf/core": "1.3.14", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -795,19 +796,19 @@ } }, "node_modules/@embedpdf/plugin-zoom": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-zoom/-/plugin-zoom-1.3.1.tgz", - "integrity": "sha512-3GXpgv6XmZiQnjaPbsxblTqUn84ALFiyONh2gwrEU9apB6STT3TQiY0QRindwrUXdQLpCSjRSB9PpDBCtTww7w==", + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-zoom/-/plugin-zoom-1.3.14.tgz", + "integrity": "sha512-/N5tyMk+8OzhObrS3O9yPkcmX8EPiuTo+WaT2QCVSmIUqKnOO4AnKpHJ6Vl0uVhcuXHCMwLucZKyhJ7tRqavwg==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.3.1", + "@embedpdf/models": "1.3.14", "hammerjs": "^2.0.8" }, "peerDependencies": { - "@embedpdf/core": "1.3.1", - "@embedpdf/plugin-interaction-manager": "1.3.1", - "@embedpdf/plugin-scroll": "1.3.1", - "@embedpdf/plugin-viewport": "1.3.1", + "@embedpdf/core": "1.3.14", + "@embedpdf/plugin-interaction-manager": "1.3.14", + "@embedpdf/plugin-scroll": "1.3.14", + "@embedpdf/plugin-viewport": "1.3.14", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -815,9 +816,9 @@ } }, "node_modules/@embedpdf/utils": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@embedpdf/utils/-/utils-1.3.1.tgz", - "integrity": "sha512-6trYysnggwCCTB2q7cX6tkOTbZJNtt2YYZohPCmh0yaDpkfNSgwDwD0jCLtEU2UZLQoH4+2GvNo+4xe+KAGlIQ==", + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@embedpdf/utils/-/utils-1.3.14.tgz", + "integrity": "sha512-gxEJD12nageCMqAjdbicNfDQolXU3nvnV0EX96OdZITRNj0Q1tisutVYoaxcCiJu3vvIEOzipjsAnQOubbFCEA==", "license": "MIT", "peerDependencies": { "preact": "^10.26.4", diff --git a/frontend/package.json b/frontend/package.json index 4eb01f202..f11184471 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,24 +6,24 @@ "proxy": "http://localhost:8080", "dependencies": { "@atlaskit/pragmatic-drag-and-drop": "^1.7.7", - "@embedpdf/core": "^1.3.1", - "@embedpdf/engines": "^1.3.1", - "@embedpdf/plugin-annotation": "^1.3.1", - "@embedpdf/plugin-export": "^1.3.1", - "@embedpdf/plugin-history": "^1.3.1", - "@embedpdf/plugin-interaction-manager": "^1.3.1", - "@embedpdf/plugin-loader": "^1.3.1", - "@embedpdf/plugin-pan": "^1.3.1", - "@embedpdf/plugin-render": "^1.3.1", - "@embedpdf/plugin-rotate": "^1.3.1", - "@embedpdf/plugin-scroll": "^1.3.1", - "@embedpdf/plugin-search": "^1.3.1", - "@embedpdf/plugin-selection": "^1.3.1", - "@embedpdf/plugin-spread": "^1.3.1", - "@embedpdf/plugin-thumbnail": "^1.3.1", - "@embedpdf/plugin-tiling": "^1.3.1", - "@embedpdf/plugin-viewport": "^1.3.1", - "@embedpdf/plugin-zoom": "^1.3.1", + "@embedpdf/core": "^1.3.14", + "@embedpdf/engines": "^1.3.14", + "@embedpdf/plugin-annotation": "^1.3.14", + "@embedpdf/plugin-export": "^1.3.14", + "@embedpdf/plugin-history": "^1.3.14", + "@embedpdf/plugin-interaction-manager": "^1.3.14", + "@embedpdf/plugin-loader": "^1.3.14", + "@embedpdf/plugin-pan": "^1.3.14", + "@embedpdf/plugin-render": "^1.3.14", + "@embedpdf/plugin-rotate": "^1.3.14", + "@embedpdf/plugin-scroll": "^1.3.14", + "@embedpdf/plugin-search": "^1.3.14", + "@embedpdf/plugin-selection": "^1.3.14", + "@embedpdf/plugin-spread": "^1.3.14", + "@embedpdf/plugin-thumbnail": "^1.3.14", + "@embedpdf/plugin-tiling": "^1.3.14", + "@embedpdf/plugin-viewport": "^1.3.14", + "@embedpdf/plugin-zoom": "^1.3.14", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", "@iconify/react": "^6.0.2", diff --git a/frontend/src/components/fileEditor/FileEditorThumbnail.tsx b/frontend/src/components/fileEditor/FileEditorThumbnail.tsx index 8159e30a9..bf3ddd885 100644 --- a/frontend/src/components/fileEditor/FileEditorThumbnail.tsx +++ b/frontend/src/components/fileEditor/FileEditorThumbnail.tsx @@ -45,6 +45,7 @@ const FileEditorThumbnail = ({ selectedFiles, onToggleFile, onCloseFile, + onViewFile, _onSetStatus, onReorderFiles, onDownloadFile, @@ -205,6 +206,11 @@ const FileEditorThumbnail = ({ onToggleFile(file.id); }; + const handleCardDoubleClick = () => { + if (!isSupported) return; + onViewFile(file.id); + }; + // ---- Style helpers ---- const getHeaderClassName = () => { if (hasError) return styles.headerError; @@ -226,6 +232,7 @@ const FileEditorThumbnail = ({ role="listitem" aria-selected={isSelected} onClick={handleCardClick} + onDoubleClick={handleCardDoubleClick} > {/* Header bar */}
{ setPreviewFile(null); const previousMode = sessionStorage.getItem('previousMode'); @@ -95,6 +100,8 @@ export default function Workbench() { setSidebarsVisible={setSidebarsVisible} previewFile={previewFile} onClose={handlePreviewClose} + activeFileIndex={activeFileIndex} + setActiveFileIndex={setActiveFileIndex} /> ); @@ -150,6 +157,12 @@ export default function Workbench() { { + const stub = selectors.getStirlingFileStub(f.fileId); + return { fileId: f.fileId, name: f.name, versionNumber: stub?.versionNumber }; + })} + currentFileIndex={activeFileIndex} + onFileSelect={setActiveFileIndex} /> )} @@ -161,7 +174,7 @@ export default function Workbench() { className="flex-1 min-h-0 relative z-10 workbench-scrollable " style={{ transition: 'opacity 0.15s ease-in-out', - paddingTop: activeFiles.length > 0 ? '3.5rem' : '0', + paddingTop: currentView === 'viewer' ? '0' : (activeFiles.length > 0 ? '3.5rem' : '0'), }} > {renderMainContent()} diff --git a/frontend/src/components/shared/FileDropdownMenu.tsx b/frontend/src/components/shared/FileDropdownMenu.tsx new file mode 100644 index 000000000..b03995fc7 --- /dev/null +++ b/frontend/src/components/shared/FileDropdownMenu.tsx @@ -0,0 +1,78 @@ +import React from 'react'; +import { Menu, Loader, Group, Text } from '@mantine/core'; +import VisibilityIcon from '@mui/icons-material/Visibility'; +import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; +import FitText from './FitText'; + +interface FileDropdownMenuProps { + displayName: string; + activeFiles: Array<{ fileId: string; name: string; versionNumber?: number }>; + currentFileIndex: number; + onFileSelect?: (index: number) => void; + switchingTo?: string | null; + viewOptionStyle: React.CSSProperties; + pillRef?: React.RefObject; +} + +export const FileDropdownMenu: React.FC = ({ + displayName, + activeFiles, + currentFileIndex, + onFileSelect, + switchingTo, + viewOptionStyle, +}) => { + return ( + + +
+ {switchingTo === "viewer" ? ( + + ) : ( + + )} + + +
+
+ + {activeFiles.map((file, index) => { + const itemName = file?.name || 'Untitled'; + const isActive = index === currentFileIndex; + return ( + { + e.stopPropagation(); + onFileSelect?.(index); + }} + className="viewer-file-tab" + {...(isActive && { 'data-active': true })} + style={{ + justifyContent: 'flex-start', + }} + > + +
+ +
+ {file.versionNumber && file.versionNumber > 1 && ( + + v{file.versionNumber} + + )} +
+
+ ); + })} +
+
+ ); +}; diff --git a/frontend/src/components/shared/TopControls.tsx b/frontend/src/components/shared/TopControls.tsx index 8edae6e72..73ae39311 100644 --- a/frontend/src/components/shared/TopControls.tsx +++ b/frontend/src/components/shared/TopControls.tsx @@ -6,9 +6,10 @@ import VisibilityIcon from "@mui/icons-material/Visibility"; import EditNoteIcon from "@mui/icons-material/EditNote"; import FolderIcon from "@mui/icons-material/Folder"; import { WorkbenchType, isValidWorkbench } from '../../types/workbench'; +import { FileDropdownMenu } from './FileDropdownMenu'; -const viewOptionStyle = { +const viewOptionStyle: React.CSSProperties = { display: 'inline-flex', flexDirection: 'row', alignItems: 'center', @@ -19,16 +20,38 @@ const viewOptionStyle = { // Build view options showing text always -const createViewOptions = (currentView: WorkbenchType, switchingTo: WorkbenchType | null) => { +const createViewOptions = ( + currentView: WorkbenchType, + switchingTo: WorkbenchType | null, + activeFiles: Array<{ fileId: string; name: string; versionNumber?: number }>, + currentFileIndex: number, + onFileSelect?: (index: number) => void +) => { + const currentFile = activeFiles[currentFileIndex]; + const isInViewer = currentView === 'viewer'; + const fileName = currentFile?.name || ''; + const displayName = isInViewer && fileName ? fileName : 'Viewer'; + const hasMultipleFiles = activeFiles.length > 1; + const showDropdown = isInViewer && hasMultipleFiles; + const viewerOption = { - label: ( -
+ label: showDropdown ? ( + + ) : ( +
{switchingTo === "viewer" ? ( ) : ( )} - Viewer + {displayName}
), value: "viewer", @@ -36,7 +59,7 @@ const createViewOptions = (currentView: WorkbenchType, switchingTo: WorkbenchTyp const pageEditorOption = { label: ( -
+
{currentView === "pageEditor" ? ( <> {switchingTo === "pageEditor" ? : } @@ -55,7 +78,7 @@ const createViewOptions = (currentView: WorkbenchType, switchingTo: WorkbenchTyp const fileEditorOption = { label: ( -
+
{currentView === "fileEditor" ? ( <> {switchingTo === "fileEditor" ? : } @@ -83,12 +106,18 @@ const createViewOptions = (currentView: WorkbenchType, switchingTo: WorkbenchTyp interface TopControlsProps { currentView: WorkbenchType; setCurrentView: (view: WorkbenchType) => void; + activeFiles?: Array<{ fileId: string; name: string; versionNumber?: number }>; + currentFileIndex?: number; + onFileSelect?: (index: number) => void; } const TopControls = ({ currentView, setCurrentView, - }: TopControlsProps) => { + activeFiles = [], + currentFileIndex = 0, + onFileSelect, +}: TopControlsProps) => { const { isRainbowMode } = useRainbowThemeContext(); const [switchingTo, setSwitchingTo] = useState(null); @@ -118,7 +147,7 @@ const TopControls = ({
void; onClose?: () => void; previewFile?: File | null; + activeFileIndex?: number; + setActiveFileIndex?: (index: number) => void; } const EmbedPdfViewerContent = ({ @@ -27,9 +28,9 @@ const EmbedPdfViewerContent = ({ setSidebarsVisible: _setSidebarsVisible, onClose, previewFile, + activeFileIndex: externalActiveFileIndex, + setActiveFileIndex: externalSetActiveFileIndex, }: EmbedPdfViewerProps) => { - const theme = useMantineTheme(); - const { colorScheme: _colorScheme } = useMantineColorScheme(); const viewerRef = React.useRef(null); const [isViewerHovered, setIsViewerHovered] = React.useState(false); @@ -52,10 +53,11 @@ const EmbedPdfViewerContent = ({ const { signatureApiRef, historyApiRef } = useSignature(); // Get current file from FileContext - const { selectors } = useFileState(); + const { selectors, state } = useFileState(); const { actions } = useFileActions(); const activeFiles = selectors.getFiles(); const activeFileIds = activeFiles.map(f => f.fileId); + const selectedFileIds = state.ui.selectedFileIds; // Navigation guard for unsaved changes const { setHasUnsavedChanges, registerUnsavedChangesChecker, unregisterUnsavedChangesChecker } = useNavigationGuard(); @@ -67,15 +69,40 @@ const EmbedPdfViewerContent = ({ // Enable annotations when: in sign mode, OR annotation mode is active, OR we want to show existing annotations const shouldEnableAnnotations = isSignatureMode || isAnnotationMode || isAnnotationsVisible; + // Track which file tab is active + const [internalActiveFileIndex, setInternalActiveFileIndex] = useState(0); + const activeFileIndex = externalActiveFileIndex ?? internalActiveFileIndex; + const setActiveFileIndex = externalSetActiveFileIndex ?? setInternalActiveFileIndex; + const hasInitializedFromSelection = useRef(false); + + // When viewer opens with a selected file, switch to that file + useEffect(() => { + if (!hasInitializedFromSelection.current && selectedFileIds.length > 0 && activeFiles.length > 0) { + const selectedFileId = selectedFileIds[0]; + const index = activeFiles.findIndex(f => f.fileId === selectedFileId); + if (index !== -1 && index !== activeFileIndex) { + setActiveFileIndex(index); + } + hasInitializedFromSelection.current = true; + } + }, [selectedFileIds, activeFiles, activeFileIndex]); + + // Reset active tab if it's out of bounds + useEffect(() => { + if (activeFileIndex >= activeFiles.length && activeFiles.length > 0) { + setActiveFileIndex(0); + } + }, [activeFiles.length, activeFileIndex]); + // Determine which file to display const currentFile = React.useMemo(() => { if (previewFile) { return previewFile; } else if (activeFiles.length > 0) { - return activeFiles[0]; // Use first file for simplicity + return activeFiles[activeFileIndex] || activeFiles[0]; } return null; - }, [previewFile, activeFiles]); + }, [previewFile, activeFiles, activeFileIndex]); // Get file with URL for rendering const fileWithUrl = useFileWithUrl(currentFile); @@ -244,15 +271,6 @@ const EmbedPdfViewerContent = ({ ) : ( <> - {/* Tabs for multiple files */} - {activeFiles.length > 1 && !previewFile && ( - - - Multiple files loaded - showing first file for now - - - )} - {/* EmbedPDF Viewer */} {/* Navigation Warning Modal */} diff --git a/frontend/src/components/viewer/LocalEmbedPDF.tsx b/frontend/src/components/viewer/LocalEmbedPDF.tsx index a7fe26d6e..cc3550dd8 100644 --- a/frontend/src/components/viewer/LocalEmbedPDF.tsx +++ b/frontend/src/components/viewer/LocalEmbedPDF.tsx @@ -18,7 +18,6 @@ import { SearchPluginPackage } from '@embedpdf/plugin-search/react'; import { ThumbnailPluginPackage } from '@embedpdf/plugin-thumbnail/react'; import { RotatePluginPackage, Rotate } from '@embedpdf/plugin-rotate/react'; import { ExportPluginPackage } from '@embedpdf/plugin-export/react'; -import { Rotation } from '@embedpdf/models'; // Import annotation plugins import { HistoryPluginPackage } from '@embedpdf/plugin-history/react'; @@ -67,6 +66,10 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, onSignatur const plugins = useMemo(() => { if (!pdfUrl) return []; + // Calculate 3.5rem in pixels dynamically based on root font size + const rootFontSize = parseFloat(getComputedStyle(document.documentElement).fontSize); + const viewportGap = rootFontSize * 3.5; + return [ createPluginRegistration(LoaderPluginPackage, { loadingOptions: { @@ -78,7 +81,7 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, onSignatur }, }), createPluginRegistration(ViewportPluginPackage, { - viewportGap: 10, + viewportGap, }), createPluginRegistration(ScrollPluginPackage, { strategy: ScrollStrategy.Vertical, @@ -134,9 +137,7 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, onSignatur createPluginRegistration(ThumbnailPluginPackage), // Register rotate plugin - createPluginRegistration(RotatePluginPackage, { - defaultRotation: Rotation.Degree0, // Start with no rotation - }), + createPluginRegistration(RotatePluginPackage), // Register export plugin for downloading PDFs createPluginRegistration(ExportPluginPackage, { @@ -288,48 +289,50 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, onSignatur }} > ( - - -
e.preventDefault()} - onDrop={(e) => e.preventDefault()} - onDragOver={(e) => e.preventDefault()} - > - {/* High-resolution tile layer */} - + renderPage={({ document, width, height, pageIndex, scale, rotation }) => { + return ( + + +
e.preventDefault()} + onDrop={(e) => e.preventDefault()} + onDragOver={(e) => e.preventDefault()} + > + {/* High-resolution tile layer */} + - {/* Search highlight layer */} - + {/* Search highlight layer */} + - {/* Selection layer for text interaction */} - - {/* Annotation layer for signatures (only when enabled) */} - {enableAnnotations && ( - - )} -
-
-
- )} + {/* Selection layer for text interaction */} + + {/* Annotation layer for signatures (only when enabled) */} + {enableAnnotations && ( + + )} +
+
+
+ ); + }} /> diff --git a/frontend/src/components/viewer/LocalEmbedPDFWithAnnotations.tsx b/frontend/src/components/viewer/LocalEmbedPDFWithAnnotations.tsx index af60d8fa8..25efcb7dd 100644 --- a/frontend/src/components/viewer/LocalEmbedPDFWithAnnotations.tsx +++ b/frontend/src/components/viewer/LocalEmbedPDFWithAnnotations.tsx @@ -64,6 +64,10 @@ export function LocalEmbedPDFWithAnnotations({ const plugins = useMemo(() => { if (!pdfUrl) return []; + // Calculate 3.5rem in pixels dynamically based on root font size + const rootFontSize = parseFloat(getComputedStyle(document.documentElement).fontSize); + const viewportGap = rootFontSize * 3.5; + return [ createPluginRegistration(LoaderPluginPackage, { loadingOptions: { @@ -75,7 +79,7 @@ export function LocalEmbedPDFWithAnnotations({ }, }), createPluginRegistration(ViewportPluginPackage, { - viewportGap: 10, + viewportGap, }), createPluginRegistration(ScrollPluginPackage, { strategy: ScrollStrategy.Vertical, diff --git a/frontend/src/components/viewer/ThumbnailSidebar.tsx b/frontend/src/components/viewer/ThumbnailSidebar.tsx index 6c3422d36..e6c515941 100644 --- a/frontend/src/components/viewer/ThumbnailSidebar.tsx +++ b/frontend/src/components/viewer/ThumbnailSidebar.tsx @@ -5,15 +5,27 @@ import { useViewer } from '../../contexts/ViewerContext'; interface ThumbnailSidebarProps { visible: boolean; onToggle: () => void; + activeFileIndex?: number; } -export function ThumbnailSidebar({ visible, onToggle: _onToggle }: ThumbnailSidebarProps) { +export function ThumbnailSidebar({ visible, onToggle: _onToggle, activeFileIndex }: ThumbnailSidebarProps) { const { getScrollState, scrollActions, getThumbnailAPI } = useViewer(); const [thumbnails, setThumbnails] = useState<{ [key: number]: string }>({}); const scrollState = getScrollState(); const thumbnailAPI = getThumbnailAPI(); + // Clear thumbnails when active file changes + useEffect(() => { + // Revoke old blob URLs to prevent memory leaks + Object.values(thumbnails).forEach((thumbUrl) => { + if (typeof thumbUrl === 'string' && thumbUrl.startsWith('blob:')) { + URL.revokeObjectURL(thumbUrl); + } + }); + setThumbnails({}); + }, [activeFileIndex]); + // Clear thumbnails when sidebar closes and revoke blob URLs to prevent memory leaks useEffect(() => { if (!visible) { diff --git a/frontend/src/components/viewer/Viewer.tsx b/frontend/src/components/viewer/Viewer.tsx index ea2403fd0..dab9ff826 100644 --- a/frontend/src/components/viewer/Viewer.tsx +++ b/frontend/src/components/viewer/Viewer.tsx @@ -5,6 +5,8 @@ export interface ViewerProps { setSidebarsVisible: (v: boolean) => void; onClose?: () => void; previewFile?: File | null; + activeFileIndex?: number; + setActiveFileIndex?: (index: number) => void; } const Viewer = (props: ViewerProps) => { diff --git a/frontend/src/contexts/ViewerContext.tsx b/frontend/src/contexts/ViewerContext.tsx index 895776f96..e3aa94487 100644 --- a/frontend/src/contexts/ViewerContext.tsx +++ b/frontend/src/contexts/ViewerContext.tsx @@ -132,6 +132,10 @@ interface ViewerContextType { setAnnotationMode: (enabled: boolean) => void; toggleAnnotationMode: () => void; + // Active file index for multi-file viewing + activeFileIndex: number; + setActiveFileIndex: (index: number) => void; + // State getters - read current state from bridges getScrollState: () => ScrollState; getZoomState: () => ZoomState; @@ -219,6 +223,7 @@ export const ViewerProvider: React.FC = ({ children }) => { const [isThumbnailSidebarVisible, setIsThumbnailSidebarVisible] = useState(false); const [isAnnotationsVisible, setIsAnnotationsVisible] = useState(true); const [isAnnotationMode, setIsAnnotationModeState] = useState(false); + const [activeFileIndex, setActiveFileIndex] = useState(0); // Get current navigation state to check if we're in sign mode useNavigation(); @@ -577,6 +582,10 @@ export const ViewerProvider: React.FC = ({ children }) => { setAnnotationMode, toggleAnnotationMode, + // Active file index + activeFileIndex, + setActiveFileIndex, + // State getters getScrollState, getZoomState, diff --git a/frontend/src/index.css b/frontend/src/index.css index b4bb41b3e..c5a604900 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -52,3 +52,12 @@ code { color: var(--mantine-color-blue-8); text-decoration: underline; } + +/* Viewer file tabs */ +.viewer-file-tab { + justify-content: flex-start; +} + +.viewer-file-tab[data-active] { + background-color: rgba(147, 197, 253, 0.5); +} diff --git a/frontend/src/tools/Sign.tsx b/frontend/src/tools/Sign.tsx index f76621895..64a304042 100644 --- a/frontend/src/tools/Sign.tsx +++ b/frontend/src/tools/Sign.tsx @@ -17,7 +17,7 @@ const Sign = (props: BaseToolProps) => { const { setWorkbench } = useNavigation(); const { setSignatureConfig, activateDrawMode, activateSignaturePlacementMode, deactivateDrawMode, updateDrawSettings, undo, redo, signatureApiRef, getImageData, setSignaturesApplied } = useSignature(); const { consumeFiles, selectors } = useFileContext(); - const { exportActions, getScrollState } = useViewer(); + const { exportActions, getScrollState, activeFileIndex, setActiveFileIndex } = useViewer(); const { setHasUnsavedChanges, unregisterUnsavedChangesChecker } = useNavigation(); // Track which signature mode was active for reactivation after save @@ -75,19 +75,11 @@ const Sign = (props: BaseToolProps) => { unregisterUnsavedChangesChecker(); setHasUnsavedChanges(false); - // Get the original file - let originalFile = null; - if (base.selectedFiles.length > 0) { - originalFile = base.selectedFiles[0]; - } else { - const allFileIds = selectors.getAllFileIds(); - if (allFileIds.length > 0) { - const stirlingFile = selectors.getFile(allFileIds[0]); - if (stirlingFile) { - originalFile = stirlingFile; - } - } - } + // Get the original file from FileContext using activeFileIndex + // The viewer displays files from FileContext, not from base.selectedFiles + const allFiles = selectors.getFiles(); + const fileIndex = activeFileIndex < allFiles.length ? activeFileIndex : 0; + const originalFile = allFiles[fileIndex]; if (!originalFile) { console.error('No file available to replace'); @@ -101,7 +93,8 @@ const Sign = (props: BaseToolProps) => { exportActions, selectors, originalFile, - getScrollState + getScrollState, + activeFileIndex }); if (flattenResult) { @@ -112,6 +105,10 @@ const Sign = (props: BaseToolProps) => { [flattenResult.outputStub] ); + // According to FileReducer.processFileSwap, new files are inserted at the beginning + // So the new file will be at index 0 + setActiveFileIndex(0); + // Mark signatures as applied setSignaturesApplied(true); @@ -125,7 +122,7 @@ const Sign = (props: BaseToolProps) => { } catch (error) { console.error('Error saving signed document:', error); } - }, [exportActions, base.selectedFiles, selectors, consumeFiles, signatureApiRef, getImageData, setWorkbench, activateDrawMode, setSignaturesApplied, getScrollState, handleDeactivateSignature, setHasUnsavedChanges, unregisterUnsavedChangesChecker]); + }, [exportActions, base.selectedFiles, selectors, consumeFiles, signatureApiRef, getImageData, setWorkbench, activateDrawMode, setSignaturesApplied, getScrollState, handleDeactivateSignature, setHasUnsavedChanges, unregisterUnsavedChangesChecker, activeFileIndex, setActiveFileIndex]); const getSteps = () => { const steps = []; diff --git a/frontend/src/utils/signatureFlattening.ts b/frontend/src/utils/signatureFlattening.ts index 1ff343155..ccb739253 100644 --- a/frontend/src/utils/signatureFlattening.ts +++ b/frontend/src/utils/signatureFlattening.ts @@ -19,6 +19,7 @@ interface SignatureFlatteningOptions { selectors: MinimalFileContextSelectors; originalFile?: StirlingFile; getScrollState: () => { currentPage: number; totalPages: number }; + activeFileIndex?: number; } export interface SignatureFlatteningResult { @@ -28,7 +29,7 @@ export interface SignatureFlatteningResult { } export async function flattenSignatures(options: SignatureFlatteningOptions): Promise { - const { signatureApiRef, getImageData, exportActions, selectors, originalFile, getScrollState } = options; + const { signatureApiRef, getImageData, exportActions, selectors, originalFile, getScrollState, activeFileIndex } = options; try { // Step 1: Extract all annotations from EmbedPDF before export @@ -104,10 +105,12 @@ export async function flattenSignatures(options: SignatureFlatteningOptions): Pr if (!currentFile) { const allFileIds = selectors.getAllFileIds(); if (allFileIds.length > 0) { - const fileStub = selectors.getStirlingFileStub(allFileIds[0]); - const fileObject = selectors.getFile(allFileIds[0]); + // Use activeFileIndex if provided, otherwise default to 0 + const fileIndex = activeFileIndex !== undefined && activeFileIndex < allFileIds.length ? activeFileIndex : 0; + const fileStub = selectors.getStirlingFileStub(allFileIds[fileIndex]); + const fileObject = selectors.getFile(allFileIds[fileIndex]); if (fileStub && fileObject) { - currentFile = createStirlingFile(fileObject, allFileIds[0] as FileId); + currentFile = createStirlingFile(fileObject, allFileIds[fileIndex] as FileId); } } }