mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 19:33:11 +02:00
Bump/embed pdfv2.8.0 (#5921)
please merge #5919, alternatively, just push this and delete that PR because this is a continuation of that. This PR bumps the embed PDF version to 2.8.0 and also adds comments functionaliy --------- Co-authored-by: ConnorYoh <[email protected]> Co-authored-by: Anthony Stirling <[email protected]>
This commit is contained in:
co-authored by
ConnorYoh
Anthony Stirling
parent
41945543e0
commit
c46156f37f
Generated
+162
-162
@@ -12,29 +12,29 @@
|
|||||||
"@atlaskit/pragmatic-drag-and-drop": "^1.7.7",
|
"@atlaskit/pragmatic-drag-and-drop": "^1.7.7",
|
||||||
"@cantoo/pdf-lib": "^2.5.3",
|
"@cantoo/pdf-lib": "^2.5.3",
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@embedpdf/core": "^2.7.0",
|
"@embedpdf/core": "^2.8.0",
|
||||||
"@embedpdf/engines": "^2.7.0",
|
"@embedpdf/engines": "^2.8.0",
|
||||||
"@embedpdf/models": "^2.7.0",
|
"@embedpdf/models": "^2.8.0",
|
||||||
"@embedpdf/plugin-annotation": "^2.7.0",
|
"@embedpdf/plugin-annotation": "^2.8.0",
|
||||||
"@embedpdf/plugin-attachment": "^2.7.0",
|
"@embedpdf/plugin-attachment": "^2.8.0",
|
||||||
"@embedpdf/plugin-bookmark": "^2.7.0",
|
"@embedpdf/plugin-bookmark": "^2.8.0",
|
||||||
"@embedpdf/plugin-document-manager": "^2.7.0",
|
"@embedpdf/plugin-document-manager": "^2.8.0",
|
||||||
"@embedpdf/plugin-export": "^2.7.0",
|
"@embedpdf/plugin-export": "^2.8.0",
|
||||||
"@embedpdf/plugin-history": "^2.7.0",
|
"@embedpdf/plugin-history": "^2.8.0",
|
||||||
"@embedpdf/plugin-interaction-manager": "^2.7.0",
|
"@embedpdf/plugin-interaction-manager": "^2.8.0",
|
||||||
"@embedpdf/plugin-pan": "^2.7.0",
|
"@embedpdf/plugin-pan": "^2.8.0",
|
||||||
"@embedpdf/plugin-print": "^2.7.0",
|
"@embedpdf/plugin-print": "^2.8.0",
|
||||||
"@embedpdf/plugin-redaction": "^2.7.0",
|
"@embedpdf/plugin-redaction": "^2.8.0",
|
||||||
"@embedpdf/plugin-render": "^2.7.0",
|
"@embedpdf/plugin-render": "^2.8.0",
|
||||||
"@embedpdf/plugin-rotate": "^2.7.0",
|
"@embedpdf/plugin-rotate": "^2.8.0",
|
||||||
"@embedpdf/plugin-scroll": "^2.7.0",
|
"@embedpdf/plugin-scroll": "^2.8.0",
|
||||||
"@embedpdf/plugin-search": "^2.7.0",
|
"@embedpdf/plugin-search": "^2.8.0",
|
||||||
"@embedpdf/plugin-selection": "^2.7.0",
|
"@embedpdf/plugin-selection": "^2.8.0",
|
||||||
"@embedpdf/plugin-spread": "^2.7.0",
|
"@embedpdf/plugin-spread": "^2.8.0",
|
||||||
"@embedpdf/plugin-thumbnail": "^2.7.0",
|
"@embedpdf/plugin-thumbnail": "^2.8.0",
|
||||||
"@embedpdf/plugin-tiling": "^2.7.0",
|
"@embedpdf/plugin-tiling": "^2.8.0",
|
||||||
"@embedpdf/plugin-viewport": "^2.7.0",
|
"@embedpdf/plugin-viewport": "^2.8.0",
|
||||||
"@embedpdf/plugin-zoom": "^2.7.0",
|
"@embedpdf/plugin-zoom": "^2.8.0",
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.1",
|
"@emotion/styled": "^11.14.1",
|
||||||
"@iconify/react": "^6.0.2",
|
"@iconify/react": "^6.0.2",
|
||||||
@@ -576,14 +576,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@embedpdf/core": {
|
"node_modules/@embedpdf/core": {
|
||||||
"version": "2.7.0",
|
"version": "2.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@embedpdf/core/-/core-2.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/@embedpdf/core/-/core-2.8.0.tgz",
|
||||||
"integrity": "sha512-dJ9pCWXVJxh6uSJP4sKuJP4v67+6Vlmw4WqJcv+CKJSUPdX9LhOSTIgTjwKLZ5zXo0c3DihNvrUupovb/DrhgQ==",
|
"integrity": "sha512-ui0HR4fl7ndiGPw40kMBxXCO9gZHctV1u3Q+/XTd34ONYJ+Pa2LoWNVW2IuPDK7PgKzABPT2axntowlVLPP10g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@embedpdf/engines": "2.7.0",
|
"@embedpdf/engines": "2.8.0",
|
||||||
"@embedpdf/models": "2.7.0"
|
"@embedpdf/models": "2.8.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"preact": "^10.26.4",
|
"preact": "^10.26.4",
|
||||||
@@ -594,9 +594,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@embedpdf/engines": {
|
"node_modules/@embedpdf/engines": {
|
||||||
"version": "2.7.0",
|
"version": "2.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@embedpdf/engines/-/engines-2.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/@embedpdf/engines/-/engines-2.8.0.tgz",
|
||||||
"integrity": "sha512-9BW/0f1EhMtklOXzpq+TtAELpXrvWcgCioFX9tSvPXBwWw/xva9c3UFx8NKehfrAkmcLGNz2c2Kqe8hDlKp9Rw==",
|
"integrity": "sha512-s749nppKxOcgvFraySKrwtiCt2VMXFe8TFuZUV5R7z8TtMagt6o5NOk6VsdvIpggUYxIsiKhLkFvAqvkNgcjng==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@embedpdf/fonts-arabic": "1.0.0",
|
"@embedpdf/fonts-arabic": "1.0.0",
|
||||||
@@ -606,8 +606,8 @@
|
|||||||
"@embedpdf/fonts-latin": "1.0.0",
|
"@embedpdf/fonts-latin": "1.0.0",
|
||||||
"@embedpdf/fonts-sc": "1.0.0",
|
"@embedpdf/fonts-sc": "1.0.0",
|
||||||
"@embedpdf/fonts-tc": "1.0.0",
|
"@embedpdf/fonts-tc": "1.0.0",
|
||||||
"@embedpdf/models": "2.7.0",
|
"@embedpdf/models": "2.8.0",
|
||||||
"@embedpdf/pdfium": "2.7.0"
|
"@embedpdf/pdfium": "2.8.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"preact": "^10.26.4",
|
"preact": "^10.26.4",
|
||||||
@@ -660,32 +660,32 @@
|
|||||||
"license": "OFL-1.1"
|
"license": "OFL-1.1"
|
||||||
},
|
},
|
||||||
"node_modules/@embedpdf/models": {
|
"node_modules/@embedpdf/models": {
|
||||||
"version": "2.7.0",
|
"version": "2.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@embedpdf/models/-/models-2.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/@embedpdf/models/-/models-2.8.0.tgz",
|
||||||
"integrity": "sha512-SRtDqYUn6Qw677ECIyipg0qqZR0+iLUU1mGI/X70ad4/qhw+TAOF5/aWddoFdPAw5XviEYVw5zR0oTz+MLBUbw==",
|
"integrity": "sha512-kk3Fm8exMmEX9Ce7VQePybmo04NQGdpsO3FsX1YOQqHpLVBk7tiTeOdetjBqI+YhQ2zWLa2naNKSOSGGzYLyxA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@embedpdf/pdfium": {
|
"node_modules/@embedpdf/pdfium": {
|
||||||
"version": "2.7.0",
|
"version": "2.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@embedpdf/pdfium/-/pdfium-2.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/@embedpdf/pdfium/-/pdfium-2.8.0.tgz",
|
||||||
"integrity": "sha512-PW6JnbBERvRKVrBzeCH/iGorQ8OTTRwr8rciyv2Kstqf3IdIFHjjNWvl6exK1/3K5wbbxYnHSNpRotl0OcsFJA==",
|
"integrity": "sha512-RlNLRNboF1Y6fNDy4sJ/a/FEYxATZyeM+n25r3KZJjG+RaM6bxBWXvWlFlGBU5Vx2eqQ5AzDAmIE9cn1agFmqA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@embedpdf/plugin-annotation": {
|
"node_modules/@embedpdf/plugin-annotation": {
|
||||||
"version": "2.7.0",
|
"version": "2.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-annotation/-/plugin-annotation-2.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-annotation/-/plugin-annotation-2.8.0.tgz",
|
||||||
"integrity": "sha512-bkn9+91XfcFoKgwZ0kiJLbnvWNyrorhcbX5grsvUw2gJ8mL0RtwHSnCjZVT3WOPleQVwnNkBZUHefJhNT+LEKw==",
|
"integrity": "sha512-h31dT0pvQjFSwsBLytL4BBLf3WDdz9kmAYNKR10filikge7MpgTzgVYFD0C6AOyy2qK1Y/vqworCyh+emVD5aA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@embedpdf/models": "2.7.0",
|
"@embedpdf/models": "2.8.0",
|
||||||
"@embedpdf/utils": "2.7.0"
|
"@embedpdf/utils": "2.8.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@embedpdf/core": "2.7.0",
|
"@embedpdf/core": "2.8.0",
|
||||||
"@embedpdf/plugin-history": "2.7.0",
|
"@embedpdf/plugin-history": "2.8.0",
|
||||||
"@embedpdf/plugin-interaction-manager": "2.7.0",
|
"@embedpdf/plugin-interaction-manager": "2.8.0",
|
||||||
"@embedpdf/plugin-selection": "2.7.0",
|
"@embedpdf/plugin-selection": "2.8.0",
|
||||||
"preact": "^10.26.4",
|
"preact": "^10.26.4",
|
||||||
"react": ">=16.8.0",
|
"react": ">=16.8.0",
|
||||||
"react-dom": ">=16.8.0",
|
"react-dom": ">=16.8.0",
|
||||||
@@ -694,15 +694,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@embedpdf/plugin-attachment": {
|
"node_modules/@embedpdf/plugin-attachment": {
|
||||||
"version": "2.7.0",
|
"version": "2.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-attachment/-/plugin-attachment-2.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-attachment/-/plugin-attachment-2.8.0.tgz",
|
||||||
"integrity": "sha512-APH9Y/P4KyawckS68bouQ5aGsmQurgkdBG0oRq9kirDM+yIVGonS2P+gzwu9otgNKve880S5mBImC3FndJL4Tg==",
|
"integrity": "sha512-g2jCwjhQsij9zz2JOxZJkIeLTAUxiBKsFh6K4hcsG45ougw/mI3WCw1f+bZlAPkZuOWPo2/nthsHb+wAlrcykQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@embedpdf/models": "2.7.0"
|
"@embedpdf/models": "2.8.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@embedpdf/core": "2.7.0",
|
"@embedpdf/core": "2.8.0",
|
||||||
"preact": "^10.26.4",
|
"preact": "^10.26.4",
|
||||||
"react": ">=16.8.0",
|
"react": ">=16.8.0",
|
||||||
"react-dom": ">=16.8.0",
|
"react-dom": ">=16.8.0",
|
||||||
@@ -711,15 +711,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@embedpdf/plugin-bookmark": {
|
"node_modules/@embedpdf/plugin-bookmark": {
|
||||||
"version": "2.7.0",
|
"version": "2.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-bookmark/-/plugin-bookmark-2.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-bookmark/-/plugin-bookmark-2.8.0.tgz",
|
||||||
"integrity": "sha512-um+naqyEAhBp4uFIis6K+V0t3dY4Dgry/vW17SwK9lFyMPQNCUT8PZMb7ysvj88vBXaDbzqCbtnlV6AyBEqxEQ==",
|
"integrity": "sha512-ab49e17amEshweobU2GbtDEuRDHj89vRYiahkRq9nU1ACI16JIyh4t4fE/m/ucL9YhMMXldjwdtTGHceV566Jg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@embedpdf/models": "2.7.0"
|
"@embedpdf/models": "2.8.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@embedpdf/core": "2.7.0",
|
"@embedpdf/core": "2.8.0",
|
||||||
"preact": "^10.26.4",
|
"preact": "^10.26.4",
|
||||||
"react": ">=16.8.0",
|
"react": ">=16.8.0",
|
||||||
"react-dom": ">=16.8.0",
|
"react-dom": ">=16.8.0",
|
||||||
@@ -728,15 +728,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@embedpdf/plugin-document-manager": {
|
"node_modules/@embedpdf/plugin-document-manager": {
|
||||||
"version": "2.7.0",
|
"version": "2.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-document-manager/-/plugin-document-manager-2.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-document-manager/-/plugin-document-manager-2.8.0.tgz",
|
||||||
"integrity": "sha512-eKM0MEUCPZmNkOkz5n6QWBcKN7m1c3RBZlfXB7faHBaScPyO3f+xgfipAht86UvfYwxT9BWk9ozbAtm2UPBHcQ==",
|
"integrity": "sha512-SS+IKJ2+rk4dHM3PvQQrvnfNdb5oOF1INR5r2w0MKo22C9WPD8Ncg1Jak/mCrGbSrJemW3pkIrKZhoREQtSrbA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@embedpdf/models": "2.7.0"
|
"@embedpdf/models": "2.8.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@embedpdf/core": "2.7.0",
|
"@embedpdf/core": "2.8.0",
|
||||||
"preact": "^10.26.4",
|
"preact": "^10.26.4",
|
||||||
"react": ">=16.8.0",
|
"react": ">=16.8.0",
|
||||||
"react-dom": ">=16.8.0",
|
"react-dom": ">=16.8.0",
|
||||||
@@ -745,15 +745,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@embedpdf/plugin-export": {
|
"node_modules/@embedpdf/plugin-export": {
|
||||||
"version": "2.7.0",
|
"version": "2.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-export/-/plugin-export-2.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-export/-/plugin-export-2.8.0.tgz",
|
||||||
"integrity": "sha512-wlP/ZdWPLCnJpsQWNCyZAlicvNiapbo7Dc95HMWHg2YgmkCjDe26z4C1QoubUanNSerGWJCK0qKXxJ/PVkJSnw==",
|
"integrity": "sha512-AvqfyhB58HLoZMKyXLLT+1ebE7wrMEnIAjr+OqEuaNnekxvb8atD8EBByzgtXLiNLStlxFn5g1CKyQThpw4Pcw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@embedpdf/models": "2.7.0"
|
"@embedpdf/models": "2.8.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@embedpdf/core": "2.7.0",
|
"@embedpdf/core": "2.8.0",
|
||||||
"preact": "^10.26.4",
|
"preact": "^10.26.4",
|
||||||
"react": ">=16.8.0",
|
"react": ">=16.8.0",
|
||||||
"react-dom": ">=16.8.0",
|
"react-dom": ">=16.8.0",
|
||||||
@@ -762,16 +762,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@embedpdf/plugin-history": {
|
"node_modules/@embedpdf/plugin-history": {
|
||||||
"version": "2.7.0",
|
"version": "2.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-history/-/plugin-history-2.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-history/-/plugin-history-2.8.0.tgz",
|
||||||
"integrity": "sha512-k2c5M2Nwey+j6EtZyf5Sw+cuyNT4MTWdqPQGR9NbmZUiNnkSrg/TmPWZCyaXZKGRjLmoJocc0qRdyaZnaaTpqw==",
|
"integrity": "sha512-S6TO7DqMqVtBYsztgvPvq8BOJTMl8rWdGjVMuoxD93HZdSgogoculwCATrJGor/BC+X6Vmtaqg6NJWSdIAeBEQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@embedpdf/models": "2.7.0"
|
"@embedpdf/models": "2.8.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@embedpdf/core": "2.7.0",
|
"@embedpdf/core": "2.8.0",
|
||||||
"preact": "^10.26.4",
|
"preact": "^10.26.4",
|
||||||
"react": ">=16.8.0",
|
"react": ">=16.8.0",
|
||||||
"react-dom": ">=16.8.0",
|
"react-dom": ">=16.8.0",
|
||||||
@@ -780,16 +780,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@embedpdf/plugin-interaction-manager": {
|
"node_modules/@embedpdf/plugin-interaction-manager": {
|
||||||
"version": "2.7.0",
|
"version": "2.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-interaction-manager/-/plugin-interaction-manager-2.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-interaction-manager/-/plugin-interaction-manager-2.8.0.tgz",
|
||||||
"integrity": "sha512-IfJiugr2k9VD8/gryMb22wC5yk4EQwSS3/Co0EdOhzkmkGSgUHA/KcG83WBu8wPEtj5Gwy473xe41fYKsGMlsA==",
|
"integrity": "sha512-xdRTAp1YiXWm+3WVqIN8dkRT3I/dHTumLJy5Kvt7lc1W2XM3M5bfCk5eTTcjY1DPv2buyt44i/4XNWoXAgBXDg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@embedpdf/models": "2.7.0"
|
"@embedpdf/models": "2.8.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@embedpdf/core": "2.7.0",
|
"@embedpdf/core": "2.8.0",
|
||||||
"preact": "^10.26.4",
|
"preact": "^10.26.4",
|
||||||
"react": ">=16.8.0",
|
"react": ">=16.8.0",
|
||||||
"react-dom": ">=16.8.0",
|
"react-dom": ">=16.8.0",
|
||||||
@@ -798,17 +798,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@embedpdf/plugin-pan": {
|
"node_modules/@embedpdf/plugin-pan": {
|
||||||
"version": "2.7.0",
|
"version": "2.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-pan/-/plugin-pan-2.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-pan/-/plugin-pan-2.8.0.tgz",
|
||||||
"integrity": "sha512-/jUyM4K42gd7eoVsKFDSfN4t8CHIf6ZX7kU4BHBBFvOcNB7bToOvibPiznC4oPrHmOuzcs4e+VOfu+yHKn6DrA==",
|
"integrity": "sha512-EFqTEHk9E7AMRwguRO24jRl0J/5+pG07wlAm5U2rB0LDk30oAqVklFuDygaALdi8ZRBdkWOfD3Wl3gurQvfwAA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@embedpdf/models": "2.7.0"
|
"@embedpdf/models": "2.8.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@embedpdf/core": "2.7.0",
|
"@embedpdf/core": "2.8.0",
|
||||||
"@embedpdf/plugin-interaction-manager": "2.7.0",
|
"@embedpdf/plugin-interaction-manager": "2.8.0",
|
||||||
"@embedpdf/plugin-viewport": "2.7.0",
|
"@embedpdf/plugin-viewport": "2.8.0",
|
||||||
"preact": "^10.26.4",
|
"preact": "^10.26.4",
|
||||||
"react": ">=16.8.0",
|
"react": ">=16.8.0",
|
||||||
"react-dom": ">=16.8.0",
|
"react-dom": ">=16.8.0",
|
||||||
@@ -817,15 +817,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@embedpdf/plugin-print": {
|
"node_modules/@embedpdf/plugin-print": {
|
||||||
"version": "2.7.0",
|
"version": "2.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-print/-/plugin-print-2.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-print/-/plugin-print-2.8.0.tgz",
|
||||||
"integrity": "sha512-sQqRSCN+r59EjKibQ3yMt1lKyHMxO1UqhZ6iI+w5hUBm9yTIHvkQRTVbawbkCfty8QNldjMQEwN3iYkf8LNHsg==",
|
"integrity": "sha512-PUdw2/1GwbewYrxVgsb+3lkYyYYcLU5qpazrBZjjx0v+SEpb2SMQOOPNUILeJGKbYUlxSz3qv/loFwqzALkUQA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@embedpdf/models": "2.7.0"
|
"@embedpdf/models": "2.8.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@embedpdf/core": "2.7.0",
|
"@embedpdf/core": "2.8.0",
|
||||||
"preact": "^10.26.4",
|
"preact": "^10.26.4",
|
||||||
"react": ">=18.0.0",
|
"react": ">=18.0.0",
|
||||||
"react-dom": ">=18.0.0",
|
"react-dom": ">=18.0.0",
|
||||||
@@ -834,20 +834,20 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@embedpdf/plugin-redaction": {
|
"node_modules/@embedpdf/plugin-redaction": {
|
||||||
"version": "2.7.0",
|
"version": "2.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-redaction/-/plugin-redaction-2.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-redaction/-/plugin-redaction-2.8.0.tgz",
|
||||||
"integrity": "sha512-N6LD9k35ZCkpHPDW5I6pW6g/YTDXGuNp/kWTHU4bcRGfTwinu6i0PaFJQKus1t1pkVprhmjJgCXbkM2xmIKXAg==",
|
"integrity": "sha512-pIDFNd9rX7cwrhY6rFCBa5MTnGdLZHX7magToqna/Ffs1KEr0CfNF8jIXG0/E6KB6SsppbHwC7AkwRVWz8NoHg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@embedpdf/models": "2.7.0",
|
"@embedpdf/models": "2.8.0",
|
||||||
"@embedpdf/utils": "2.7.0"
|
"@embedpdf/utils": "2.8.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@embedpdf/core": "2.7.0",
|
"@embedpdf/core": "2.8.0",
|
||||||
"@embedpdf/plugin-annotation": "2.7.0",
|
"@embedpdf/plugin-annotation": "2.8.0",
|
||||||
"@embedpdf/plugin-history": "2.7.0",
|
"@embedpdf/plugin-history": "2.8.0",
|
||||||
"@embedpdf/plugin-interaction-manager": "2.7.0",
|
"@embedpdf/plugin-interaction-manager": "2.8.0",
|
||||||
"@embedpdf/plugin-selection": "2.7.0",
|
"@embedpdf/plugin-selection": "2.8.0",
|
||||||
"preact": "^10.26.4",
|
"preact": "^10.26.4",
|
||||||
"react": ">=16.8.0",
|
"react": ">=16.8.0",
|
||||||
"react-dom": ">=16.8.0",
|
"react-dom": ">=16.8.0",
|
||||||
@@ -856,16 +856,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@embedpdf/plugin-render": {
|
"node_modules/@embedpdf/plugin-render": {
|
||||||
"version": "2.7.0",
|
"version": "2.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-render/-/plugin-render-2.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-render/-/plugin-render-2.8.0.tgz",
|
||||||
"integrity": "sha512-qJcBxl2Kgqbw/Yb4qiX3qNsBKmzI8ijAKix3PoYL+nlhX6iFbcCkUbJgF0rPCBbhIy59Bpp7eJL+RyY5DhQvUg==",
|
"integrity": "sha512-jVGSuyg366LmFzbDpqszLbu3G6VOfv1u46D1C0ph6pL3jisTlRswRosaXS4eVE/fAYTryBrI0olCpVYYct4bQw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@embedpdf/models": "2.7.0"
|
"@embedpdf/models": "2.8.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@embedpdf/core": "2.7.0",
|
"@embedpdf/core": "2.8.0",
|
||||||
"preact": "^10.26.4",
|
"preact": "^10.26.4",
|
||||||
"react": ">=16.8.0",
|
"react": ">=16.8.0",
|
||||||
"react-dom": ">=16.8.0",
|
"react-dom": ">=16.8.0",
|
||||||
@@ -874,15 +874,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@embedpdf/plugin-rotate": {
|
"node_modules/@embedpdf/plugin-rotate": {
|
||||||
"version": "2.7.0",
|
"version": "2.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-rotate/-/plugin-rotate-2.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-rotate/-/plugin-rotate-2.8.0.tgz",
|
||||||
"integrity": "sha512-ggqhQzpJB7ydfx9fDcuUrlGS9wxfsLG+rQwf1MMmUMMdEK1NPsHw5abSuQBerAm1vrpZ3Dyz0v2TxPVejvIhjA==",
|
"integrity": "sha512-lWATWEwhkBW77dJaKXlSJbqEMLTSy1lOhn0kDAZ5eyPZRo+7UWe7LHuT5HWzzRlbY9rpvnyixCT6Zp9rKT+WnQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@embedpdf/models": "2.7.0"
|
"@embedpdf/models": "2.8.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@embedpdf/core": "2.7.0",
|
"@embedpdf/core": "2.8.0",
|
||||||
"preact": "^10.26.4",
|
"preact": "^10.26.4",
|
||||||
"react": ">=16.8.0",
|
"react": ">=16.8.0",
|
||||||
"react-dom": ">=16.8.0",
|
"react-dom": ">=16.8.0",
|
||||||
@@ -891,17 +891,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@embedpdf/plugin-scroll": {
|
"node_modules/@embedpdf/plugin-scroll": {
|
||||||
"version": "2.7.0",
|
"version": "2.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-scroll/-/plugin-scroll-2.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-scroll/-/plugin-scroll-2.8.0.tgz",
|
||||||
"integrity": "sha512-uEs5XdHZ3XqH3OWnM8E2eHen+7+MxK9SrlBnfM8ZcPkzHR9n7hT/Aqy05BiLKxKem585HvtbRgFzNQZqmNIGHw==",
|
"integrity": "sha512-l6hFH6lsAI+07ZGuOwbC8qcRNdYzWSIfRszjt9UmhKZbvXLEp6YJeS/XOUC/37Kqi30tsmLDyZhMW1AooGAr1A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@embedpdf/models": "2.7.0"
|
"@embedpdf/models": "2.8.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@embedpdf/core": "2.7.0",
|
"@embedpdf/core": "2.8.0",
|
||||||
"@embedpdf/plugin-viewport": "2.7.0",
|
"@embedpdf/plugin-viewport": "2.8.0",
|
||||||
"preact": "^10.26.4",
|
"preact": "^10.26.4",
|
||||||
"react": ">=16.8.0",
|
"react": ">=16.8.0",
|
||||||
"react-dom": ">=16.8.0",
|
"react-dom": ">=16.8.0",
|
||||||
@@ -910,15 +910,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@embedpdf/plugin-search": {
|
"node_modules/@embedpdf/plugin-search": {
|
||||||
"version": "2.7.0",
|
"version": "2.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-search/-/plugin-search-2.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-search/-/plugin-search-2.8.0.tgz",
|
||||||
"integrity": "sha512-TeUMvlp6j4NXkkbPURSkg7fd6l/X8wUnN+ImFiM01VM+YBLQWTPXcP7AAyFzoI5iAh2m5+ZZEB/uGb5qm3Vamg==",
|
"integrity": "sha512-HZ7munHdAF2pJ1cT02yc5I0Xcr4b4CQ8GkhD4qhTpZK0yga/tQiDY2Jjstygv7XiZwHIDKvqOuQsQmOXDsEemw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@embedpdf/models": "2.7.0"
|
"@embedpdf/models": "2.8.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@embedpdf/core": "2.7.0",
|
"@embedpdf/core": "2.8.0",
|
||||||
"preact": "^10.26.4",
|
"preact": "^10.26.4",
|
||||||
"react": ">=16.8.0",
|
"react": ">=16.8.0",
|
||||||
"react-dom": ">=16.8.0",
|
"react-dom": ">=16.8.0",
|
||||||
@@ -927,18 +927,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@embedpdf/plugin-selection": {
|
"node_modules/@embedpdf/plugin-selection": {
|
||||||
"version": "2.7.0",
|
"version": "2.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-selection/-/plugin-selection-2.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-selection/-/plugin-selection-2.8.0.tgz",
|
||||||
"integrity": "sha512-MhTUHZ5jBh1jQU66xrtkx2jEtcreCfHy4zpmMY7iCoZtWUxrML+zf4jYNrR/ZP93O4i5HNBSnJNTG+RmRJ5KPw==",
|
"integrity": "sha512-DcPyOp2WKoVYVpbZIP5t+JsEmCL9Y7bxe8PmiEBtlm1lustuUezdWyA2G4wsGCR4I/5uNlY85qBGCBhL195sSA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@embedpdf/models": "2.7.0",
|
"@embedpdf/models": "2.8.0",
|
||||||
"@embedpdf/utils": "2.7.0"
|
"@embedpdf/utils": "2.8.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@embedpdf/core": "2.7.0",
|
"@embedpdf/core": "2.8.0",
|
||||||
"@embedpdf/plugin-interaction-manager": "2.7.0",
|
"@embedpdf/plugin-interaction-manager": "2.8.0",
|
||||||
"preact": "^10.26.4",
|
"preact": "^10.26.4",
|
||||||
"react": ">=16.8.0",
|
"react": ">=16.8.0",
|
||||||
"react-dom": ">=16.8.0",
|
"react-dom": ">=16.8.0",
|
||||||
@@ -947,15 +947,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@embedpdf/plugin-spread": {
|
"node_modules/@embedpdf/plugin-spread": {
|
||||||
"version": "2.7.0",
|
"version": "2.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-spread/-/plugin-spread-2.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-spread/-/plugin-spread-2.8.0.tgz",
|
||||||
"integrity": "sha512-FMW6nXlyZhjQKrj8l4gduu0+IWI/H8Sda9iiKKYOGLrDWcIsSo3Gt3mSJgUsjUxA2YS+l0m48VeuSESa7Ab8oA==",
|
"integrity": "sha512-0Ld5HERaG8cKyWW7ktojG3FK6angkzYnBnw7Rnqf4cQuNdO7nKSTslyxkrLzF3vvmKrB//2ghm/pRVkgci4BOw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@embedpdf/models": "2.7.0"
|
"@embedpdf/models": "2.8.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@embedpdf/core": "2.7.0",
|
"@embedpdf/core": "2.8.0",
|
||||||
"preact": "^10.26.4",
|
"preact": "^10.26.4",
|
||||||
"react": ">=16.8.0",
|
"react": ">=16.8.0",
|
||||||
"react-dom": ">=16.8.0",
|
"react-dom": ">=16.8.0",
|
||||||
@@ -964,16 +964,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@embedpdf/plugin-thumbnail": {
|
"node_modules/@embedpdf/plugin-thumbnail": {
|
||||||
"version": "2.7.0",
|
"version": "2.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-thumbnail/-/plugin-thumbnail-2.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-thumbnail/-/plugin-thumbnail-2.8.0.tgz",
|
||||||
"integrity": "sha512-ZnXt9ebtExFQUMUbWApUK8yC1eXYjDMEzS9631QpbLm0BhW9jYu9cs6U37Sww+zhYi7IFih4zb38lgAsbDgOcA==",
|
"integrity": "sha512-Dq/Cqsn4GClRXNWerqlXNH8WDWK/TtHxwq6yr2kqgXPo1FyREto4I7GfdO1hygTu0Dl6HK2auEVHBVC8QT53fw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@embedpdf/models": "2.7.0"
|
"@embedpdf/models": "2.8.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@embedpdf/core": "2.7.0",
|
"@embedpdf/core": "2.8.0",
|
||||||
"@embedpdf/plugin-render": "2.7.0",
|
"@embedpdf/plugin-render": "2.8.0",
|
||||||
"preact": "^10.26.4",
|
"preact": "^10.26.4",
|
||||||
"react": ">=16.8.0",
|
"react": ">=16.8.0",
|
||||||
"react-dom": ">=16.8.0",
|
"react-dom": ">=16.8.0",
|
||||||
@@ -982,18 +982,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@embedpdf/plugin-tiling": {
|
"node_modules/@embedpdf/plugin-tiling": {
|
||||||
"version": "2.7.0",
|
"version": "2.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-tiling/-/plugin-tiling-2.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-tiling/-/plugin-tiling-2.8.0.tgz",
|
||||||
"integrity": "sha512-/Cd7dR0kA5/Y1gykYLYS3gecStycc4m5Jjm+a0mFzO/7yQ6BLb0g4zkM95YloBAsFoP5GPXKKhBw3pi32pTObw==",
|
"integrity": "sha512-gPe5mG6hyyruki1eSuQYD2KDbo0Z0TxzSk8TcHIdEYYF6NlI2OghuO8Vczz5WLU8clX8xzn8c/c+tQMHEXfbZA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@embedpdf/models": "2.7.0"
|
"@embedpdf/models": "2.8.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@embedpdf/core": "2.7.0",
|
"@embedpdf/core": "2.8.0",
|
||||||
"@embedpdf/plugin-render": "2.7.0",
|
"@embedpdf/plugin-render": "2.8.0",
|
||||||
"@embedpdf/plugin-scroll": "2.7.0",
|
"@embedpdf/plugin-scroll": "2.8.0",
|
||||||
"@embedpdf/plugin-viewport": "2.7.0",
|
"@embedpdf/plugin-viewport": "2.8.0",
|
||||||
"preact": "^10.26.4",
|
"preact": "^10.26.4",
|
||||||
"react": ">=16.8.0",
|
"react": ">=16.8.0",
|
||||||
"react-dom": ">=16.8.0",
|
"react-dom": ">=16.8.0",
|
||||||
@@ -1002,16 +1002,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@embedpdf/plugin-viewport": {
|
"node_modules/@embedpdf/plugin-viewport": {
|
||||||
"version": "2.7.0",
|
"version": "2.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-viewport/-/plugin-viewport-2.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-viewport/-/plugin-viewport-2.8.0.tgz",
|
||||||
"integrity": "sha512-nVDoU86CgHme7f2r7OFhziqHz1Yg8vdE2K+bXzRCmtZaubH+qVo5C+77at9brYCSinA67znQn1yy6zbfxs4FwA==",
|
"integrity": "sha512-E16hc4yPA54XQGHp0Dy3OYyE8ilBaJE7LJirVmha4kMkP7XBu6xHNOJrXtq4GsZmdLQkf+x8ie2DDbWS+tcwnw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@embedpdf/models": "2.7.0"
|
"@embedpdf/models": "2.8.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@embedpdf/core": "2.7.0",
|
"@embedpdf/core": "2.8.0",
|
||||||
"preact": "^10.26.4",
|
"preact": "^10.26.4",
|
||||||
"react": ">=16.8.0",
|
"react": ">=16.8.0",
|
||||||
"react-dom": ">=16.8.0",
|
"react-dom": ">=16.8.0",
|
||||||
@@ -1020,17 +1020,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@embedpdf/plugin-zoom": {
|
"node_modules/@embedpdf/plugin-zoom": {
|
||||||
"version": "2.7.0",
|
"version": "2.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-zoom/-/plugin-zoom-2.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-zoom/-/plugin-zoom-2.8.0.tgz",
|
||||||
"integrity": "sha512-5u2cQcahHkbOfa5Ug5hxXwtIOz4TzrQSgvmXvO7+jV0jXKOp1vFVB9tU26SKwwwW9OL6e4L/P6roKJRPbOnWyw==",
|
"integrity": "sha512-HepSQ7NFYhMsQsgfnC/G3b3LOYtNCWkCME0C0jBLrEYsgqsWozf97jIgzfOAeJsugyjQmojpgLUNt3xE9CtG6Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@embedpdf/models": "2.7.0"
|
"@embedpdf/models": "2.8.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@embedpdf/core": "2.7.0",
|
"@embedpdf/core": "2.8.0",
|
||||||
"@embedpdf/plugin-scroll": "2.7.0",
|
"@embedpdf/plugin-scroll": "2.8.0",
|
||||||
"@embedpdf/plugin-viewport": "2.7.0",
|
"@embedpdf/plugin-viewport": "2.8.0",
|
||||||
"preact": "^10.26.4",
|
"preact": "^10.26.4",
|
||||||
"react": ">=16.8.0",
|
"react": ">=16.8.0",
|
||||||
"react-dom": ">=16.8.0",
|
"react-dom": ">=16.8.0",
|
||||||
@@ -1039,9 +1039,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@embedpdf/utils": {
|
"node_modules/@embedpdf/utils": {
|
||||||
"version": "2.7.0",
|
"version": "2.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@embedpdf/utils/-/utils-2.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/@embedpdf/utils/-/utils-2.8.0.tgz",
|
||||||
"integrity": "sha512-QtHOChToxkemBbJSwBTbgUqQbUO+obRj5HBaA/8KGv1ESCjtrdwjfAHMCBYdRuyvMVvrQGe1bHuSbKpb2JVS5w==",
|
"integrity": "sha512-mt3DiQ8pnPk95q0zv7dXfN+y5fzJT2WtXyST8ziYEgmfhz0l2HT/MHCAwLzB3Whlnd2BdHThLfJyG1UD6pC84g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"preact": "^10.26.4",
|
"preact": "^10.26.4",
|
||||||
|
|||||||
+23
-23
@@ -8,29 +8,29 @@
|
|||||||
"@atlaskit/pragmatic-drag-and-drop": "^1.7.7",
|
"@atlaskit/pragmatic-drag-and-drop": "^1.7.7",
|
||||||
"@cantoo/pdf-lib": "^2.5.3",
|
"@cantoo/pdf-lib": "^2.5.3",
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@embedpdf/core": "^2.7.0",
|
"@embedpdf/core": "^2.8.0",
|
||||||
"@embedpdf/engines": "^2.7.0",
|
"@embedpdf/engines": "^2.8.0",
|
||||||
"@embedpdf/models": "^2.7.0",
|
"@embedpdf/models": "^2.8.0",
|
||||||
"@embedpdf/plugin-annotation": "^2.7.0",
|
"@embedpdf/plugin-annotation": "^2.8.0",
|
||||||
"@embedpdf/plugin-attachment": "^2.7.0",
|
"@embedpdf/plugin-attachment": "^2.8.0",
|
||||||
"@embedpdf/plugin-bookmark": "^2.7.0",
|
"@embedpdf/plugin-bookmark": "^2.8.0",
|
||||||
"@embedpdf/plugin-document-manager": "^2.7.0",
|
"@embedpdf/plugin-document-manager": "^2.8.0",
|
||||||
"@embedpdf/plugin-export": "^2.7.0",
|
"@embedpdf/plugin-export": "^2.8.0",
|
||||||
"@embedpdf/plugin-history": "^2.7.0",
|
"@embedpdf/plugin-history": "^2.8.0",
|
||||||
"@embedpdf/plugin-interaction-manager": "^2.7.0",
|
"@embedpdf/plugin-interaction-manager": "^2.8.0",
|
||||||
"@embedpdf/plugin-pan": "^2.7.0",
|
"@embedpdf/plugin-pan": "^2.8.0",
|
||||||
"@embedpdf/plugin-print": "^2.7.0",
|
"@embedpdf/plugin-print": "^2.8.0",
|
||||||
"@embedpdf/plugin-redaction": "^2.7.0",
|
"@embedpdf/plugin-redaction": "^2.8.0",
|
||||||
"@embedpdf/plugin-render": "^2.7.0",
|
"@embedpdf/plugin-render": "^2.8.0",
|
||||||
"@embedpdf/plugin-rotate": "^2.7.0",
|
"@embedpdf/plugin-rotate": "^2.8.0",
|
||||||
"@embedpdf/plugin-scroll": "^2.7.0",
|
"@embedpdf/plugin-scroll": "^2.8.0",
|
||||||
"@embedpdf/plugin-search": "^2.7.0",
|
"@embedpdf/plugin-search": "^2.8.0",
|
||||||
"@embedpdf/plugin-selection": "^2.7.0",
|
"@embedpdf/plugin-selection": "^2.8.0",
|
||||||
"@embedpdf/plugin-spread": "^2.7.0",
|
"@embedpdf/plugin-spread": "^2.8.0",
|
||||||
"@embedpdf/plugin-thumbnail": "^2.7.0",
|
"@embedpdf/plugin-thumbnail": "^2.8.0",
|
||||||
"@embedpdf/plugin-tiling": "^2.7.0",
|
"@embedpdf/plugin-tiling": "^2.8.0",
|
||||||
"@embedpdf/plugin-viewport": "^2.7.0",
|
"@embedpdf/plugin-viewport": "^2.8.0",
|
||||||
"@embedpdf/plugin-zoom": "^2.7.0",
|
"@embedpdf/plugin-zoom": "^2.8.0",
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.1",
|
"@emotion/styled": "^11.14.1",
|
||||||
"@iconify/react": "^6.0.2",
|
"@iconify/react": "^6.0.2",
|
||||||
|
|||||||
@@ -1503,8 +1503,9 @@ editSelected = "Edit Annotation"
|
|||||||
editSquare = "Edit Square"
|
editSquare = "Edit Square"
|
||||||
editStampHint = "To change the image, delete this stamp and add a new one."
|
editStampHint = "To change the image, delete this stamp and add a new one."
|
||||||
editSwitchToSelect = "Switch to Select & Edit to edit this annotation."
|
editSwitchToSelect = "Switch to Select & Edit to edit this annotation."
|
||||||
editText = "Edit Text Box"
|
editText = "Edit"
|
||||||
editTextMarkup = "Edit Text Markup"
|
editTextMarkup = "Edit Text Markup"
|
||||||
|
annotationStyle = "Annotation style"
|
||||||
ellipse = "Ellipse"
|
ellipse = "Ellipse"
|
||||||
exit = "Exit annotation mode"
|
exit = "Exit annotation mode"
|
||||||
fillColor = "Fill Colour"
|
fillColor = "Fill Colour"
|
||||||
@@ -1515,13 +1516,19 @@ highlight = "Highlight"
|
|||||||
imagePreview = "Preview"
|
imagePreview = "Preview"
|
||||||
inkHighlighter = "Freehand Highlighter"
|
inkHighlighter = "Freehand Highlighter"
|
||||||
line = "Line"
|
line = "Line"
|
||||||
|
lineArrow = "Arrow"
|
||||||
noBackground = "No background"
|
noBackground = "No background"
|
||||||
note = "Note"
|
note = "Note"
|
||||||
|
comment = "Comment"
|
||||||
|
comments = "Comments"
|
||||||
|
insertText = "Insert Text"
|
||||||
|
replaceText = "Replace Text"
|
||||||
noteIcon = "Note Icon"
|
noteIcon = "Note Icon"
|
||||||
notesStamps = "Notes & Stamps"
|
notesStamps = "Notes & Stamps"
|
||||||
opacity = "Opacity"
|
opacity = "Opacity"
|
||||||
pen = "Pen"
|
pen = "Pen"
|
||||||
polygon = "Polygon"
|
polygon = "Polygon"
|
||||||
|
polyline = "Polyline"
|
||||||
properties = "Properties"
|
properties = "Properties"
|
||||||
rectangle = "Rectangle"
|
rectangle = "Rectangle"
|
||||||
redo = "Redo"
|
redo = "Redo"
|
||||||
@@ -4463,6 +4470,7 @@ rotateLeft = "Rotate Left"
|
|||||||
rotateRight = "Rotate Right"
|
rotateRight = "Rotate Right"
|
||||||
toggleSidebar = "Toggle Sidebar"
|
toggleSidebar = "Toggle Sidebar"
|
||||||
toggleBookmarks = "Toggle Bookmarks"
|
toggleBookmarks = "Toggle Bookmarks"
|
||||||
|
toggleComments = "Comments"
|
||||||
print = "Print PDF"
|
print = "Print PDF"
|
||||||
ruler = "Ruler / Measure"
|
ruler = "Ruler / Measure"
|
||||||
readAloud = "Read Aloud"
|
readAloud = "Read Aloud"
|
||||||
@@ -6914,6 +6922,31 @@ loading = "Loading attachments..."
|
|||||||
empty = "No attachments in this document"
|
empty = "No attachments in this document"
|
||||||
noMatch = "No attachments match your search"
|
noMatch = "No attachments match your search"
|
||||||
|
|
||||||
|
[viewer.comments]
|
||||||
|
title = "Comments"
|
||||||
|
hint = "Place comments with the Comment, Insert Text, or Replace Text tools. They will appear here by page."
|
||||||
|
placeholder = "Type your comment..."
|
||||||
|
pageLabel = "Page {{page}}"
|
||||||
|
oneComment = "1 comment"
|
||||||
|
nComments = "{{count}} comments"
|
||||||
|
addCommentPlaceholder = "Add comment..."
|
||||||
|
addLink = "Add link"
|
||||||
|
goToLink = "Go to link"
|
||||||
|
addComment = "Add comment"
|
||||||
|
viewComment = "View comment"
|
||||||
|
addReplyPlaceholder = "Add reply..."
|
||||||
|
saveReply = "Save reply"
|
||||||
|
send = "Send"
|
||||||
|
moreActions = "More actions"
|
||||||
|
typeComment = "Comment"
|
||||||
|
typeInsertText = "Insert Text"
|
||||||
|
typeReplaceText = "Replace Text"
|
||||||
|
locateAnnotation = "Locate in document"
|
||||||
|
deleteTitle = "Remove annotation from comments?"
|
||||||
|
deleteDescription = "This annotation has a comment attached. You can remove just the comment from the sidebar while keeping the annotation, or delete everything."
|
||||||
|
removeCommentOnly = "Remove comment only"
|
||||||
|
deleteAnnotationAndComment = "Delete annotation & comment"
|
||||||
|
|
||||||
[viewer.formBar]
|
[viewer.formBar]
|
||||||
title = "Form Fields"
|
title = "Form Fields"
|
||||||
unsavedBadge = "Unsaved"
|
unsavedBadge = "Unsaved"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ActionIcon, Tooltip, Popover, Stack, ColorSwatch, ColorPicker as MantineColorPicker, Group } from '@mantine/core';
|
import { ActionIcon, Tooltip, Popover, Stack, ColorSwatch, ColorPicker as MantineColorPicker, Group } from '@mantine/core';
|
||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback, useEffect } from 'react';
|
||||||
import ColorizeIcon from '@mui/icons-material/Colorize';
|
import ColorizeIcon from '@mui/icons-material/Colorize';
|
||||||
|
|
||||||
// safari and firefox do not support the eye dropper API, only edge, chrome and opera do.
|
// safari and firefox do not support the eye dropper API, only edge, chrome and opera do.
|
||||||
@@ -20,6 +20,11 @@ interface ColorControlProps {
|
|||||||
|
|
||||||
export function ColorControl({ value, onChange, label, disabled = false }: ColorControlProps) {
|
export function ColorControl({ value, onChange, label, disabled = false }: ColorControlProps) {
|
||||||
const [opened, setOpened] = useState(false);
|
const [opened, setOpened] = useState(false);
|
||||||
|
// Buffer the colour locally so the picker stays responsive during drag.
|
||||||
|
// Only propagate to the parent (which triggers expensive annotation updates)
|
||||||
|
// on onChangeEnd (mouse-up / swatch click), preventing infinite re-render loops.
|
||||||
|
const [localColor, setLocalColor] = useState(value);
|
||||||
|
useEffect(() => { setLocalColor(value); }, [value]);
|
||||||
|
|
||||||
const handleEyeDropper = useCallback(async () => {
|
const handleEyeDropper = useCallback(async () => {
|
||||||
if (!supportsEyeDropper) return;
|
if (!supportsEyeDropper) return;
|
||||||
@@ -56,7 +61,7 @@ export function ColorControl({ value, onChange, label, disabled = false }: Color
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ColorSwatch color={value} size={18} />
|
<ColorSwatch color={localColor} size={18} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Popover.Target>
|
</Popover.Target>
|
||||||
@@ -64,8 +69,9 @@ export function ColorControl({ value, onChange, label, disabled = false }: Color
|
|||||||
<Stack gap="xs">
|
<Stack gap="xs">
|
||||||
<MantineColorPicker
|
<MantineColorPicker
|
||||||
format="hex"
|
format="hex"
|
||||||
value={value}
|
value={localColor}
|
||||||
onChange={onChange}
|
onChange={setLocalColor}
|
||||||
|
onChangeEnd={onChange}
|
||||||
swatches={[
|
swatches={[
|
||||||
'#000000', '#ffffff', '#ff0000', '#00ff00', '#0000ff',
|
'#000000', '#ffffff', '#ff0000', '#00ff00', '#0000ff',
|
||||||
'#ffff00', '#ff00ff', '#00ffff', '#ffa500', 'transparent'
|
'#ffff00', '#ff00ff', '#00ffff', '#ffa500', 'transparent'
|
||||||
@@ -76,7 +82,7 @@ export function ColorControl({ value, onChange, label, disabled = false }: Color
|
|||||||
{supportsEyeDropper && (
|
{supportsEyeDropper && (
|
||||||
<Group justify="flex-end">
|
<Group justify="flex-end">
|
||||||
<Tooltip label="Pick colour from screen">
|
<Tooltip label="Pick colour from screen">
|
||||||
<ActionIcon variant="subtle" color="gray" size="sm" onClick={handleEyeDropper}>
|
<ActionIcon variant="subtle" color="gray" size="sm" onClick={handleEyeDropper} style={{ color: 'var(--text-primary)' }}>
|
||||||
<ColorizeIcon style={{ fontSize: 16 }} />
|
<ColorizeIcon style={{ fontSize: 16 }} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
import { ActionIcon, Tooltip, Popover, Stack, Slider, Text, Group, Button } from '@mantine/core';
|
import { ActionIcon, Tooltip, Popover, Stack, Slider, Text, Group, Button } from '@mantine/core';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import type { TrackedAnnotation } from '@embedpdf/plugin-annotation';
|
||||||
|
import type { PdfAnnotationObject } from '@embedpdf/models';
|
||||||
|
import type { AnnotationPatch } from '@app/components/viewer/viewerTypes';
|
||||||
import TuneIcon from '@mui/icons-material/Tune';
|
import TuneIcon from '@mui/icons-material/Tune';
|
||||||
import FormatAlignLeftIcon from '@mui/icons-material/FormatAlignLeft';
|
import FormatAlignLeftIcon from '@mui/icons-material/FormatAlignLeft';
|
||||||
import FormatAlignCenterIcon from '@mui/icons-material/FormatAlignCenter';
|
import FormatAlignCenterIcon from '@mui/icons-material/FormatAlignCenter';
|
||||||
import FormatAlignRightIcon from '@mui/icons-material/FormatAlignRight';
|
import FormatAlignRightIcon from '@mui/icons-material/FormatAlignRight';
|
||||||
|
|
||||||
type AnnotationType = 'text' | 'note' | 'shape';
|
export type PropertiesAnnotationType = 'text' | 'note' | 'shape';
|
||||||
|
|
||||||
interface PropertiesPopoverProps {
|
interface PropertiesPopoverProps {
|
||||||
annotationType: AnnotationType;
|
annotationType: PropertiesAnnotationType;
|
||||||
annotation: any;
|
annotation: TrackedAnnotation<PdfAnnotationObject> | undefined;
|
||||||
onUpdate: (patch: Record<string, any>) => void;
|
onUpdate: (patch: AnnotationPatch) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,7 +27,15 @@ export function PropertiesPopover({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [opened, setOpened] = useState(false);
|
const [opened, setOpened] = useState(false);
|
||||||
|
|
||||||
const obj = annotation?.object;
|
interface AnnotationObjectProps {
|
||||||
|
fontSize?: number;
|
||||||
|
textAlign?: number | string;
|
||||||
|
opacity?: number;
|
||||||
|
borderWidth?: number;
|
||||||
|
strokeWidth?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const obj = annotation?.object as (PdfAnnotationObject & AnnotationObjectProps) | undefined;
|
||||||
|
|
||||||
// Get current values
|
// Get current values
|
||||||
const fontSize = obj?.fontSize ?? 14;
|
const fontSize = obj?.fontSize ?? 14;
|
||||||
|
|||||||
@@ -81,6 +81,18 @@ type AnnotationDefaults =
|
|||||||
imageSize?: { width: number; height: number };
|
imageSize?: { width: number; height: number };
|
||||||
customData?: Record<string, unknown>;
|
customData?: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
type: PdfAnnotationSubtype.TEXT;
|
||||||
|
strokeColor?: string;
|
||||||
|
opacity?: number;
|
||||||
|
customData?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: PdfAnnotationSubtype.CARET;
|
||||||
|
strokeColor?: string;
|
||||||
|
opacity?: number;
|
||||||
|
customData?: Record<string, unknown>;
|
||||||
|
}
|
||||||
| null;
|
| null;
|
||||||
|
|
||||||
type AnnotationApiSurface = {
|
type AnnotationApiSurface = {
|
||||||
@@ -294,6 +306,24 @@ const TOOL_DEFAULT_BUILDERS: Record<AnnotationToolId, ToolDefaultsBuilder> = {
|
|||||||
stamp: buildStampDefaults,
|
stamp: buildStampDefaults,
|
||||||
signatureStamp: buildStampDefaults,
|
signatureStamp: buildStampDefaults,
|
||||||
signatureInk: (options) => buildInkDefaults(options),
|
signatureInk: (options) => buildInkDefaults(options),
|
||||||
|
textComment: (options) => ({
|
||||||
|
type: PdfAnnotationSubtype.TEXT,
|
||||||
|
strokeColor: options?.color ?? DEFAULTS.note,
|
||||||
|
opacity: options?.opacity ?? 1,
|
||||||
|
...withCustomData(options),
|
||||||
|
}),
|
||||||
|
insertText: (options) => ({
|
||||||
|
type: PdfAnnotationSubtype.CARET,
|
||||||
|
strokeColor: options?.color ?? '#E44234',
|
||||||
|
opacity: options?.opacity ?? 1,
|
||||||
|
...withCustomData(options),
|
||||||
|
}),
|
||||||
|
replaceText: (options) => ({
|
||||||
|
type: PdfAnnotationSubtype.CARET,
|
||||||
|
strokeColor: options?.color ?? '#E44234',
|
||||||
|
opacity: options?.opacity ?? 1,
|
||||||
|
...withCustomData(options),
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AnnotationAPIBridge = forwardRef<AnnotationAPI>(function AnnotationAPIBridge(_props, ref) {
|
export const AnnotationAPIBridge = forwardRef<AnnotationAPI>(function AnnotationAPIBridge(_props, ref) {
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
import { ActionIcon, Tooltip, Popover, TextInput, Button, Stack } from '@mantine/core';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import DeleteIcon from '@mui/icons-material/Delete';
|
||||||
|
import EditIcon from '@mui/icons-material/Edit';
|
||||||
|
import CommentIcon from '@mui/icons-material/ChatBubbleOutlineRounded';
|
||||||
|
import AddCommentIcon from '@mui/icons-material/AddCommentOutlined';
|
||||||
|
import OpenInNewIcon from '@mui/icons-material/OpenInNewRounded';
|
||||||
|
import LocalIcon from '@app/components/shared/LocalIcon';
|
||||||
|
import type { FirstLinkTarget } from '@app/components/viewer/useAnnotationMenuHandlers';
|
||||||
|
|
||||||
|
export const commonButtonStyles = {
|
||||||
|
root: {
|
||||||
|
flexShrink: 0,
|
||||||
|
backgroundColor: 'var(--bg-raised)',
|
||||||
|
border: '1px solid var(--border-default)',
|
||||||
|
color: 'var(--text-secondary)',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'var(--hover-bg)',
|
||||||
|
borderColor: 'var(--border-strong)',
|
||||||
|
color: 'var(--text-primary)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function DeleteButton({ onDelete }: { onDelete: () => void }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<Tooltip label={t('annotation.delete', 'Delete')}>
|
||||||
|
<ActionIcon
|
||||||
|
variant="subtle"
|
||||||
|
color="red"
|
||||||
|
size="md"
|
||||||
|
onClick={onDelete}
|
||||||
|
styles={{
|
||||||
|
root: {
|
||||||
|
...commonButtonStyles.root,
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'var(--mantine-color-red-1)',
|
||||||
|
borderColor: 'var(--mantine-color-red-4)',
|
||||||
|
color: 'var(--mantine-color-red-7)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DeleteIcon style={{ fontSize: 18 }} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EditTextButton({ onEdit }: { onEdit: () => void }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<Tooltip label={t('annotation.editText', 'Edit Text')}>
|
||||||
|
<ActionIcon variant="subtle" color="gray" size="md" onClick={onEdit} styles={commonButtonStyles}>
|
||||||
|
<EditIcon style={{ fontSize: 18 }} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AttachCommentButtonProps {
|
||||||
|
isInSidebar: boolean;
|
||||||
|
onView: () => void;
|
||||||
|
onAdd: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AttachCommentButton({ isInSidebar, onView, onAdd }: AttachCommentButtonProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<Tooltip label={isInSidebar ? t('viewer.comments.viewComment', 'View comment') : t('viewer.comments.addComment', 'Add comment')}>
|
||||||
|
<ActionIcon
|
||||||
|
variant={isInSidebar ? 'filled' : 'subtle'}
|
||||||
|
color={isInSidebar ? 'blue' : 'gray'}
|
||||||
|
size="md"
|
||||||
|
onClick={isInSidebar ? onView : onAdd}
|
||||||
|
styles={isInSidebar ? undefined : commonButtonStyles}
|
||||||
|
>
|
||||||
|
<AddCommentIcon style={{ fontSize: 18 }} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CommentButtonProps {
|
||||||
|
hasContent: boolean;
|
||||||
|
onClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CommentButton({ hasContent, onClick }: CommentButtonProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<Tooltip label={hasContent ? t('viewer.comments.viewComment', 'View comment') : t('viewer.comments.addComment', 'Add comment')}>
|
||||||
|
<ActionIcon variant="subtle" color="gray" size="md" onClick={onClick} styles={commonButtonStyles}>
|
||||||
|
<CommentIcon style={{ fontSize: 18 }} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LinkButtonProps {
|
||||||
|
firstLinkTarget: FirstLinkTarget | null;
|
||||||
|
onGoToLink: () => void;
|
||||||
|
onAddLink: (url: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LinkButton({ firstLinkTarget, onGoToLink, onAddLink }: LinkButtonProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [url, setUrl] = useState('');
|
||||||
|
|
||||||
|
if (firstLinkTarget) {
|
||||||
|
return (
|
||||||
|
<Tooltip label={t('viewer.comments.goToLink', 'Go to link')}>
|
||||||
|
<ActionIcon variant="subtle" color="gray" size="md" onClick={onGoToLink} styles={commonButtonStyles}>
|
||||||
|
<OpenInNewIcon style={{ fontSize: 18 }} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover opened={open} onClose={() => setOpen(false)} position="top">
|
||||||
|
<Popover.Target>
|
||||||
|
<Tooltip label={t('viewer.comments.addLink', 'Add link')}>
|
||||||
|
<ActionIcon variant="subtle" color="gray" size="md" onClick={() => setOpen((o) => !o)} styles={commonButtonStyles}>
|
||||||
|
<LocalIcon icon="link" width="1.25rem" height="1.25rem" />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
</Popover.Target>
|
||||||
|
<Popover.Dropdown>
|
||||||
|
<Stack gap="xs">
|
||||||
|
<TextInput
|
||||||
|
placeholder="https://..."
|
||||||
|
value={url}
|
||||||
|
onChange={(e) => setUrl(e.currentTarget.value)}
|
||||||
|
size="sm"
|
||||||
|
style={{ minWidth: 220 }}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
disabled={!url.trim()}
|
||||||
|
onClick={() => {
|
||||||
|
onAddLink(url);
|
||||||
|
setUrl('');
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('viewer.comments.addLink', 'Add link')}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Popover.Dropdown>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,18 +1,14 @@
|
|||||||
import { ActionIcon, Tooltip, Group } from '@mantine/core';
|
import { Group } from '@mantine/core';
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import { useEffect, useState, useRef, useCallback } from 'react';
|
import { useEffect, useState, useRef, useCallback } from 'react';
|
||||||
import DeleteIcon from '@mui/icons-material/Delete';
|
|
||||||
import EditIcon from '@mui/icons-material/Edit';
|
|
||||||
import { useAnnotation } from '@embedpdf/plugin-annotation/react';
|
import { useAnnotation } from '@embedpdf/plugin-annotation/react';
|
||||||
import type { TrackedAnnotation } from '@embedpdf/plugin-annotation';
|
import type { TrackedAnnotation } from '@embedpdf/plugin-annotation';
|
||||||
import type { PdfAnnotationObject } from '@embedpdf/models';
|
import { PdfAnnotationSubtype, type PdfAnnotationObject } from '@embedpdf/models';
|
||||||
import type { AnnotationPatch, AnnotationObject } from '@app/components/viewer/viewerTypes';
|
import type { AnnotationObject } from '@app/components/viewer/viewerTypes';
|
||||||
import { useActiveDocumentId } from '@app/components/viewer/useActiveDocumentId';
|
import { useActiveDocumentId } from '@app/components/viewer/useActiveDocumentId';
|
||||||
import { OpacityControl } from '@app/components/annotation/shared/OpacityControl';
|
import { useViewer } from '@app/contexts/ViewerContext';
|
||||||
import { WidthControl } from '@app/components/annotation/shared/WidthControl';
|
import { useAnnotationMenuHandlers } from '@app/components/viewer/useAnnotationMenuHandlers';
|
||||||
import { PropertiesPopover } from '@app/components/annotation/shared/PropertiesPopover';
|
import { AnnotationTypeButtons } from '@app/components/viewer/AnnotationTypeButtons';
|
||||||
import { ColorControl } from '@app/components/annotation/shared/ColorControl';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Props interface matching EmbedPDF's annotation selection menu pattern
|
* Props interface matching EmbedPDF's annotation selection menu pattern
|
||||||
@@ -34,22 +30,10 @@ export interface AnnotationSelectionMenuProps {
|
|||||||
|
|
||||||
export function AnnotationSelectionMenu(props: AnnotationSelectionMenuProps) {
|
export function AnnotationSelectionMenu(props: AnnotationSelectionMenuProps) {
|
||||||
const activeDocumentId = useActiveDocumentId();
|
const activeDocumentId = useActiveDocumentId();
|
||||||
|
if (!activeDocumentId) return null;
|
||||||
// Don't render until we have a valid document ID
|
return <AnnotationSelectionMenuInner documentId={activeDocumentId} {...props} />;
|
||||||
if (!activeDocumentId) {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
|
||||||
<AnnotationSelectionMenuInner
|
|
||||||
documentId={activeDocumentId}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
type AnnotationType = 'textMarkup' | 'ink' | 'inkHighlighter' | 'text' | 'note' | 'shape' | 'line' | 'stamp' | 'unknown';
|
|
||||||
|
|
||||||
function AnnotationSelectionMenuInner({
|
function AnnotationSelectionMenuInner({
|
||||||
documentId,
|
documentId,
|
||||||
context,
|
context,
|
||||||
@@ -58,366 +42,56 @@ function AnnotationSelectionMenuInner({
|
|||||||
}: AnnotationSelectionMenuProps & { documentId: string }) {
|
}: AnnotationSelectionMenuProps & { documentId: string }) {
|
||||||
const annotation = context?.annotation;
|
const annotation = context?.annotation;
|
||||||
const pageIndex = context?.pageIndex;
|
const pageIndex = context?.pageIndex;
|
||||||
const { t } = useTranslation();
|
|
||||||
const { provides } = useAnnotation(documentId);
|
const { provides } = useAnnotation(documentId);
|
||||||
|
const { scrollActions, requestCommentFocus } = useViewer();
|
||||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||||
const [menuPosition, setMenuPosition] = useState<{ top: number; left: number } | null>(null);
|
const [menuPosition, setMenuPosition] = useState<{ top: number; left: number } | null>(null);
|
||||||
|
|
||||||
// Merge refs - menuWrapperProps.ref is a callback ref
|
const handlers = useAnnotationMenuHandlers({
|
||||||
|
annotation,
|
||||||
|
pageIndex,
|
||||||
|
documentId,
|
||||||
|
provides,
|
||||||
|
scrollActions,
|
||||||
|
requestCommentFocus,
|
||||||
|
wrapperRef,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-open the comments sidebar when a comment annotation is selected
|
||||||
|
useEffect(() => {
|
||||||
|
const annObj = annotation?.object as AnnotationObject | undefined;
|
||||||
|
const annId = annObj?.id;
|
||||||
|
if (!selected || !annId || pageIndex === undefined) return;
|
||||||
|
const toolId = annObj?.customData?.toolId;
|
||||||
|
const annType = annObj?.type;
|
||||||
|
const isComment =
|
||||||
|
(annType === PdfAnnotationSubtype.TEXT && toolId === 'textComment') ||
|
||||||
|
(annType === PdfAnnotationSubtype.CARET && (toolId === 'insertText' || toolId === 'replaceText'));
|
||||||
|
if (!isComment) return;
|
||||||
|
requestCommentFocus(documentId, pageIndex, annId, (annObj?.contents ?? '').trim().length > 0);
|
||||||
|
}, [selected, annotation?.object]);
|
||||||
|
|
||||||
|
// Click outside to deselect
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selected) return;
|
||||||
|
const handlePointerDown = (e: PointerEvent) => {
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
if (target.closest('[data-annotation-selection-menu]')) return;
|
||||||
|
if (target.closest('[data-no-interaction]')) return;
|
||||||
|
if (target.closest('.mantine-Popover-dropdown')) return;
|
||||||
|
provides?.deselectAnnotation?.();
|
||||||
|
};
|
||||||
|
document.addEventListener('pointerdown', handlePointerDown);
|
||||||
|
return () => document.removeEventListener('pointerdown', handlePointerDown);
|
||||||
|
}, [selected, provides]);
|
||||||
|
|
||||||
|
// Merge refs — menuWrapperProps.ref is a callback ref from EmbedPDF
|
||||||
const setRef = useCallback((node: HTMLDivElement | null) => {
|
const setRef = useCallback((node: HTMLDivElement | null) => {
|
||||||
wrapperRef.current = node;
|
wrapperRef.current = node;
|
||||||
// Call the EmbedPDF ref callback
|
|
||||||
menuWrapperProps?.ref?.(node);
|
menuWrapperProps?.ref?.(node);
|
||||||
}, [menuWrapperProps]);
|
}, [menuWrapperProps]);
|
||||||
|
|
||||||
// Type detection
|
// Track menu position via MutationObserver (handles drag repositioning)
|
||||||
const getAnnotationType = useCallback((): AnnotationType => {
|
|
||||||
const type = annotation?.object?.type;
|
|
||||||
const toolId = (annotation?.object as AnnotationObject | undefined)?.customData?.toolId;
|
|
||||||
|
|
||||||
// Map type numbers to categories
|
|
||||||
if (type !== undefined && [9, 10, 11, 12].includes(type)) return 'textMarkup';
|
|
||||||
if (type === 15) {
|
|
||||||
return toolId === 'inkHighlighter' ? 'inkHighlighter' : 'ink';
|
|
||||||
}
|
|
||||||
if (type === 3) {
|
|
||||||
return toolId === 'note' ? 'note' : 'text';
|
|
||||||
}
|
|
||||||
if (type !== undefined && [5, 6, 7].includes(type)) return 'shape';
|
|
||||||
if (type !== undefined && [4, 8].includes(type)) return 'line';
|
|
||||||
if (type === 13) return 'stamp';
|
|
||||||
|
|
||||||
return 'unknown';
|
|
||||||
}, [annotation]);
|
|
||||||
|
|
||||||
// Calculate menu width based on annotation type
|
|
||||||
const calculateWidth = (annotationType: AnnotationType): number => {
|
|
||||||
switch (annotationType) {
|
|
||||||
case 'stamp':
|
|
||||||
return 80;
|
|
||||||
case 'inkHighlighter':
|
|
||||||
return 220;
|
|
||||||
case 'shape':
|
|
||||||
return 200;
|
|
||||||
default:
|
|
||||||
return 180;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get annotation properties
|
|
||||||
const obj = annotation?.object as AnnotationObject | undefined;
|
|
||||||
const annotationType = getAnnotationType();
|
|
||||||
const annotationId = obj?.id;
|
|
||||||
|
|
||||||
// Get current colors
|
|
||||||
const getCurrentColor = (): string => {
|
|
||||||
if (!obj) return '#000000';
|
|
||||||
const type = obj.type;
|
|
||||||
// Text annotations use textColor
|
|
||||||
if (type === 3) return obj.textColor || obj.color || '#000000';
|
|
||||||
// Shape annotations use strokeColor
|
|
||||||
if (type !== undefined && [4, 5, 6, 7, 8].includes(type)) return obj.strokeColor || obj.color || '#000000';
|
|
||||||
// Default to color property
|
|
||||||
return obj.color || obj.strokeColor || '#000000';
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStrokeColor = (): string => {
|
|
||||||
return obj?.strokeColor || obj?.color || '#000000';
|
|
||||||
};
|
|
||||||
|
|
||||||
const getFillColor = (): string => {
|
|
||||||
return obj?.color || obj?.fillColor || '#0000ff';
|
|
||||||
};
|
|
||||||
|
|
||||||
const getBackgroundColor = (): string => {
|
|
||||||
// Check multiple possible properties for background color
|
|
||||||
return obj?.backgroundColor || obj?.fillColor || obj?.color || '#ffffff';
|
|
||||||
};
|
|
||||||
|
|
||||||
const getTextColor = (): string => {
|
|
||||||
return obj?.textColor || obj?.color || '#000000';
|
|
||||||
};
|
|
||||||
|
|
||||||
const getOpacity = (): number => {
|
|
||||||
return Math.round((obj?.opacity ?? 1) * 100);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getWidth = (): number => {
|
|
||||||
return obj?.strokeWidth ?? obj?.borderWidth ?? obj?.lineWidth ?? obj?.thickness ?? 2;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handlers
|
|
||||||
const handleDelete = useCallback(() => {
|
|
||||||
if (provides?.deleteAnnotation && annotationId && pageIndex !== undefined) {
|
|
||||||
provides.deleteAnnotation(pageIndex, annotationId);
|
|
||||||
}
|
|
||||||
}, [provides, annotationId, pageIndex]);
|
|
||||||
|
|
||||||
// Focus inline text input (same as double-clicking the note/text box). Dispatches dblclick on the
|
|
||||||
// annotation hit-area div (EmbedPDF's inner div with onDoubleClick) so built-in FreeText editing is used.
|
|
||||||
const handleFocusTextEdit = useCallback(() => {
|
|
||||||
const root = wrapperRef.current?.closest('[data-no-interaction]');
|
|
||||||
const main = root?.firstElementChild;
|
|
||||||
// EmbedPDF puts onDoubleClick on the content div. For text/note (no rotation) it's the first child.
|
|
||||||
const hitArea = main?.lastElementChild ?? main?.firstElementChild;
|
|
||||||
if (!hitArea) return;
|
|
||||||
hitArea.dispatchEvent(new MouseEvent('dblclick', { bubbles: true, cancelable: true, view: window }));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleColorChange = useCallback((color: string, target: 'main' | 'stroke' | 'fill' | 'text' | 'background') => {
|
|
||||||
if (!provides?.updateAnnotation || !annotationId || pageIndex === undefined) return;
|
|
||||||
|
|
||||||
const type = obj?.type;
|
|
||||||
const patch: AnnotationPatch = {};
|
|
||||||
|
|
||||||
if (target === 'stroke') {
|
|
||||||
// Shape stroke - preserve fill color
|
|
||||||
patch.strokeColor = color;
|
|
||||||
patch.color = obj?.color || '#0000ff'; // Preserve fill
|
|
||||||
patch.strokeWidth = getWidth();
|
|
||||||
} else if (target === 'fill') {
|
|
||||||
// Shape fill - preserve stroke color
|
|
||||||
patch.color = color;
|
|
||||||
patch.strokeColor = obj?.strokeColor || '#000000'; // Preserve stroke
|
|
||||||
patch.strokeWidth = getWidth();
|
|
||||||
} else if (target === 'background') {
|
|
||||||
// Background color for text/note - set multiple properties for compatibility
|
|
||||||
patch.backgroundColor = color;
|
|
||||||
patch.fillColor = color;
|
|
||||||
patch.color = color;
|
|
||||||
} else if (target === 'text') {
|
|
||||||
// Text color for text/note - TRY PROPERTY COMBINATIONS
|
|
||||||
patch.textColor = color;
|
|
||||||
patch.fontColor = color; // EmbedPDF might expect this instead
|
|
||||||
|
|
||||||
// Include font metadata (EmbedPDF might require these together)
|
|
||||||
patch.fontSize = obj?.fontSize ?? 14;
|
|
||||||
patch.fontFamily = obj?.fontFamily ?? 'Helvetica';
|
|
||||||
|
|
||||||
// Re-submit text content
|
|
||||||
patch.contents = obj?.contents ?? '';
|
|
||||||
} else {
|
|
||||||
// Main color - for highlights, ink, etc.
|
|
||||||
patch.color = color;
|
|
||||||
|
|
||||||
// For text markup annotations (highlight, underline, strikeout, squiggly)
|
|
||||||
if (type !== undefined && [9, 10, 11, 12].includes(type)) {
|
|
||||||
patch.strokeColor = color;
|
|
||||||
patch.fillColor = color;
|
|
||||||
patch.opacity = obj?.opacity ?? 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For line annotations (type 4, 8), include stroke properties
|
|
||||||
if (type !== undefined && [4, 8].includes(type)) {
|
|
||||||
patch.strokeColor = color;
|
|
||||||
patch.strokeWidth = obj?.strokeWidth ?? obj?.lineWidth ?? 2;
|
|
||||||
patch.lineWidth = obj?.lineWidth ?? obj?.strokeWidth ?? 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For ink annotations (type 15), include all stroke-related properties
|
|
||||||
if (type === 15) {
|
|
||||||
patch.strokeColor = color;
|
|
||||||
patch.strokeWidth = obj?.strokeWidth ?? obj?.thickness ?? 2;
|
|
||||||
patch.opacity = obj?.opacity ?? 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
provides.updateAnnotation(pageIndex, annotationId, patch);
|
|
||||||
}, [provides, annotationId, pageIndex, obj]);
|
|
||||||
|
|
||||||
const handleOpacityChange = useCallback((opacity: number) => {
|
|
||||||
if (!provides?.updateAnnotation || !annotationId || pageIndex === undefined) return;
|
|
||||||
|
|
||||||
provides.updateAnnotation(pageIndex, annotationId, {
|
|
||||||
opacity: opacity / 100,
|
|
||||||
});
|
|
||||||
}, [provides, annotationId, pageIndex]);
|
|
||||||
|
|
||||||
const handleWidthChange = useCallback((width: number) => {
|
|
||||||
if (!provides?.updateAnnotation || !annotationId || pageIndex === undefined) return;
|
|
||||||
|
|
||||||
provides.updateAnnotation(pageIndex, annotationId, {
|
|
||||||
strokeWidth: width,
|
|
||||||
});
|
|
||||||
}, [provides, annotationId, pageIndex]);
|
|
||||||
|
|
||||||
const handlePropertiesUpdate = useCallback((patch: Record<string, any>) => {
|
|
||||||
if (!provides?.updateAnnotation || !annotationId || pageIndex === undefined) return;
|
|
||||||
|
|
||||||
provides.updateAnnotation(pageIndex, annotationId, patch);
|
|
||||||
}, [provides, annotationId, pageIndex]);
|
|
||||||
|
|
||||||
// Render button groups based on annotation type
|
|
||||||
const renderButtons = () => {
|
|
||||||
const commonButtonStyles = {
|
|
||||||
root: {
|
|
||||||
flexShrink: 0,
|
|
||||||
backgroundColor: 'var(--bg-raised)',
|
|
||||||
border: '1px solid var(--border-default)',
|
|
||||||
color: 'var(--text-secondary)',
|
|
||||||
'&:hover': {
|
|
||||||
backgroundColor: 'var(--hover-bg)',
|
|
||||||
borderColor: 'var(--border-strong)',
|
|
||||||
color: 'var(--text-primary)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const EditTextButton = () => (
|
|
||||||
<Tooltip label={t('annotation.editText', 'Edit Text')}>
|
|
||||||
<ActionIcon
|
|
||||||
variant="subtle"
|
|
||||||
color="gray"
|
|
||||||
size="md"
|
|
||||||
onClick={handleFocusTextEdit}
|
|
||||||
styles={commonButtonStyles}
|
|
||||||
>
|
|
||||||
<EditIcon style={{ fontSize: 18 }} />
|
|
||||||
</ActionIcon>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
|
|
||||||
const DeleteButton = () => (
|
|
||||||
<Tooltip label={t('annotation.delete', 'Delete')}>
|
|
||||||
<ActionIcon
|
|
||||||
variant="subtle"
|
|
||||||
color="red"
|
|
||||||
size="md"
|
|
||||||
onClick={handleDelete}
|
|
||||||
styles={{
|
|
||||||
root: {
|
|
||||||
...commonButtonStyles.root,
|
|
||||||
'&:hover': {
|
|
||||||
backgroundColor: 'var(--mantine-color-red-1)',
|
|
||||||
borderColor: 'var(--mantine-color-red-4)',
|
|
||||||
color: 'var(--mantine-color-red-7)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DeleteIcon style={{ fontSize: 18 }} />
|
|
||||||
</ActionIcon>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
|
|
||||||
switch (annotationType) {
|
|
||||||
case 'textMarkup':
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ColorControl
|
|
||||||
value={getCurrentColor()}
|
|
||||||
onChange={(color) => handleColorChange(color, 'main')}
|
|
||||||
label={t('annotation.changeColor', 'Change Colour')}
|
|
||||||
/>
|
|
||||||
<OpacityControl value={getOpacity()} onChange={handleOpacityChange} />
|
|
||||||
<DeleteButton />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 'ink':
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ColorControl
|
|
||||||
value={getCurrentColor()}
|
|
||||||
onChange={(color) => handleColorChange(color, 'main')}
|
|
||||||
label={t('annotation.changeColor', 'Change Colour')}
|
|
||||||
/>
|
|
||||||
<WidthControl value={getWidth()} onChange={handleWidthChange} min={1} max={12} />
|
|
||||||
<DeleteButton />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 'inkHighlighter':
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ColorControl
|
|
||||||
value={getCurrentColor()}
|
|
||||||
onChange={(color) => handleColorChange(color, 'main')}
|
|
||||||
label={t('annotation.changeColor', 'Change Colour')}
|
|
||||||
/>
|
|
||||||
<WidthControl value={getWidth()} onChange={handleWidthChange} min={1} max={20} />
|
|
||||||
<OpacityControl value={getOpacity()} onChange={handleOpacityChange} />
|
|
||||||
<DeleteButton />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 'text':
|
|
||||||
case 'note':
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ColorControl
|
|
||||||
value={getTextColor()}
|
|
||||||
onChange={(color) => handleColorChange(color, 'text')}
|
|
||||||
label={t('annotation.color', 'Color')}
|
|
||||||
/>
|
|
||||||
<ColorControl
|
|
||||||
value={getBackgroundColor()}
|
|
||||||
onChange={(color) => handleColorChange(color, 'background')}
|
|
||||||
label={t('annotation.backgroundColor', 'Background color')}
|
|
||||||
/>
|
|
||||||
<EditTextButton />
|
|
||||||
<PropertiesPopover
|
|
||||||
annotationType={annotationType}
|
|
||||||
annotation={annotation}
|
|
||||||
onUpdate={handlePropertiesUpdate}
|
|
||||||
/>
|
|
||||||
<DeleteButton />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 'shape':
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ColorControl
|
|
||||||
value={getStrokeColor()}
|
|
||||||
onChange={(color) => handleColorChange(color, 'stroke')}
|
|
||||||
label={t('annotation.strokeColor', 'Stroke Colour')}
|
|
||||||
/>
|
|
||||||
<ColorControl
|
|
||||||
value={getFillColor()}
|
|
||||||
onChange={(color) => handleColorChange(color, 'fill')}
|
|
||||||
label={t('annotation.fillColor', 'Fill Colour')}
|
|
||||||
/>
|
|
||||||
<PropertiesPopover
|
|
||||||
annotationType="shape"
|
|
||||||
annotation={annotation}
|
|
||||||
onUpdate={handlePropertiesUpdate}
|
|
||||||
/>
|
|
||||||
<DeleteButton />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 'line':
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ColorControl
|
|
||||||
value={getCurrentColor()}
|
|
||||||
onChange={(color) => handleColorChange(color, 'main')}
|
|
||||||
label={t('annotation.changeColor', 'Change Colour')}
|
|
||||||
/>
|
|
||||||
<WidthControl value={getWidth()} onChange={handleWidthChange} min={1} max={12} />
|
|
||||||
<DeleteButton />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 'stamp':
|
|
||||||
return <DeleteButton />;
|
|
||||||
|
|
||||||
default:
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ColorControl
|
|
||||||
value={getCurrentColor()}
|
|
||||||
onChange={(color) => handleColorChange(color, 'main')}
|
|
||||||
label={t('annotation.changeColor', 'Change Colour')}
|
|
||||||
/>
|
|
||||||
<DeleteButton />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Calculate position for portal based on wrapper element
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selected || !annotation || !wrapperRef.current) {
|
if (!selected || !annotation || !wrapperRef.current) {
|
||||||
setMenuPosition(null);
|
setMenuPosition(null);
|
||||||
@@ -426,24 +100,15 @@ function AnnotationSelectionMenuInner({
|
|||||||
|
|
||||||
const updatePosition = () => {
|
const updatePosition = () => {
|
||||||
const wrapper = wrapperRef.current;
|
const wrapper = wrapperRef.current;
|
||||||
if (!wrapper) {
|
if (!wrapper) { setMenuPosition(null); return; }
|
||||||
setMenuPosition(null);
|
const rect = wrapper.getBoundingClientRect();
|
||||||
return;
|
setMenuPosition({ top: rect.bottom + 8, left: rect.left + rect.width / 2 });
|
||||||
}
|
|
||||||
|
|
||||||
const wrapperRect = wrapper.getBoundingClientRect();
|
|
||||||
setMenuPosition({
|
|
||||||
top: wrapperRect.bottom + 8,
|
|
||||||
left: wrapperRect.left + wrapperRect.width / 2,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
updatePosition();
|
updatePosition();
|
||||||
|
|
||||||
// MutationObserver catches EmbedPDF updating the wrapper's inline style during drag
|
|
||||||
const observer = new MutationObserver(updatePosition);
|
const observer = new MutationObserver(updatePosition);
|
||||||
observer.observe(wrapperRef.current, { attributes: true, attributeFilter: ['style'] });
|
observer.observe(wrapperRef.current, { attributes: true, attributeFilter: ['style'] });
|
||||||
|
|
||||||
window.addEventListener('scroll', updatePosition, true);
|
window.addEventListener('scroll', updatePosition, true);
|
||||||
window.addEventListener('resize', updatePosition);
|
window.addEventListener('resize', updatePosition);
|
||||||
|
|
||||||
@@ -452,9 +117,8 @@ function AnnotationSelectionMenuInner({
|
|||||||
window.removeEventListener('scroll', updatePosition, true);
|
window.removeEventListener('scroll', updatePosition, true);
|
||||||
window.removeEventListener('resize', updatePosition);
|
window.removeEventListener('resize', updatePosition);
|
||||||
};
|
};
|
||||||
}, [selected, annotation]);
|
}, [selected]);
|
||||||
|
|
||||||
// Early return AFTER all hooks have been called
|
|
||||||
if (!selected || !annotation) return null;
|
if (!selected || !annotation) return null;
|
||||||
|
|
||||||
const menuContent = menuPosition ? (
|
const menuContent = menuPosition ? (
|
||||||
@@ -466,39 +130,34 @@ function AnnotationSelectionMenuInner({
|
|||||||
left: `${menuPosition.left}px`,
|
left: `${menuPosition.left}px`,
|
||||||
transform: 'translateX(-50%)',
|
transform: 'translateX(-50%)',
|
||||||
pointerEvents: 'auto',
|
pointerEvents: 'auto',
|
||||||
zIndex: 10000, // Very high z-index to appear above everything
|
zIndex: 10000,
|
||||||
backgroundColor: 'var(--mantine-color-body)',
|
backgroundColor: 'var(--mantine-color-body)',
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
padding: '8px 12px',
|
padding: '8px 12px',
|
||||||
boxShadow: '0 2px 12px rgba(0, 0, 0, 0.25)',
|
boxShadow: '0 2px 12px rgba(0, 0, 0, 0.25)',
|
||||||
border: '1px solid var(--mantine-color-default-border)',
|
border: '1px solid var(--mantine-color-default-border)',
|
||||||
fontSize: '14px',
|
fontSize: '14px',
|
||||||
minWidth: `${calculateWidth(annotationType)}px`,
|
minWidth: `${handlers.menuWidth}px`,
|
||||||
transition: 'min-width 0.2s ease',
|
transition: 'min-width 0.2s ease',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Group gap="sm" wrap="nowrap" justify="center">
|
<Group gap="sm" wrap="nowrap" justify="center">
|
||||||
{renderButtons()}
|
<AnnotationTypeButtons
|
||||||
|
{...handlers}
|
||||||
|
annotation={annotation}
|
||||||
|
documentId={documentId}
|
||||||
|
pageIndex={pageIndex}
|
||||||
|
annotationId={handlers.annotationId}
|
||||||
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
</div>
|
</div>
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Invisible wrapper that provides positioning - uses EmbedPDF's menuWrapperProps.
|
{/* Invisible wrapper for EmbedPDF positioning — pointer-events:none so drag still works */}
|
||||||
Must stay pointerEvents:none so EmbedPDF's internal drag handlers receive events.
|
<div ref={setRef} style={{ ...menuWrapperProps?.style, opacity: 0, pointerEvents: 'none' }} />
|
||||||
Edit Text button dispatches dblclick on this wrapper's parent to use EmbedPDF's built-in inline editing. */}
|
{typeof document !== 'undefined' && menuContent ? createPortal(menuContent, document.body) : null}
|
||||||
<div
|
|
||||||
ref={setRef}
|
|
||||||
style={{
|
|
||||||
...menuWrapperProps?.style,
|
|
||||||
opacity: 0,
|
|
||||||
pointerEvents: 'none',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{typeof document !== 'undefined' && menuContent
|
|
||||||
? createPortal(menuContent, document.body)
|
|
||||||
: null}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,201 @@
|
|||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import type { TrackedAnnotation } from '@embedpdf/plugin-annotation';
|
||||||
|
import type { PdfAnnotationObject } from '@embedpdf/models';
|
||||||
|
import { OpacityControl } from '@app/components/annotation/shared/OpacityControl';
|
||||||
|
import { WidthControl } from '@app/components/annotation/shared/WidthControl';
|
||||||
|
import { PropertiesPopover, type PropertiesAnnotationType } from '@app/components/annotation/shared/PropertiesPopover';
|
||||||
|
import { ColorControl } from '@app/components/annotation/shared/ColorControl';
|
||||||
|
import {
|
||||||
|
DeleteButton,
|
||||||
|
EditTextButton,
|
||||||
|
AttachCommentButton,
|
||||||
|
CommentButton,
|
||||||
|
LinkButton,
|
||||||
|
} from '@app/components/viewer/AnnotationMenuButtons';
|
||||||
|
import type { AnnotationType, AnnotationMenuState, AnnotationMenuHandlers } from '@app/components/viewer/useAnnotationMenuHandlers';
|
||||||
|
|
||||||
|
export interface AnnotationTypeButtonsProps extends AnnotationMenuState, AnnotationMenuHandlers {
|
||||||
|
annotation: TrackedAnnotation<PdfAnnotationObject> | undefined;
|
||||||
|
documentId: string;
|
||||||
|
pageIndex: number | undefined;
|
||||||
|
annotationId: string | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AnnotationTypeButtons(props: AnnotationTypeButtonsProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const {
|
||||||
|
annotationType,
|
||||||
|
currentColor,
|
||||||
|
strokeColor,
|
||||||
|
fillColor,
|
||||||
|
backgroundColor,
|
||||||
|
textColor,
|
||||||
|
currentOpacity,
|
||||||
|
currentWidth,
|
||||||
|
hasCommentContent,
|
||||||
|
isInSidebar,
|
||||||
|
firstLinkTarget,
|
||||||
|
obj,
|
||||||
|
annotation,
|
||||||
|
onDelete,
|
||||||
|
onEdit,
|
||||||
|
onColorChange,
|
||||||
|
onOpacityChange,
|
||||||
|
onWidthChange,
|
||||||
|
onPropertiesUpdate,
|
||||||
|
onGoToLink,
|
||||||
|
onAddLink,
|
||||||
|
onAddToSidebar,
|
||||||
|
onViewComment,
|
||||||
|
onCommentColorChange,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const attachCommentButton = (
|
||||||
|
<AttachCommentButton isInSidebar={isInSidebar} onView={onViewComment} onAdd={onAddToSidebar} />
|
||||||
|
);
|
||||||
|
|
||||||
|
switch (annotationType as AnnotationType) {
|
||||||
|
case 'textMarkup':
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{attachCommentButton}
|
||||||
|
<ColorControl
|
||||||
|
value={currentColor}
|
||||||
|
onChange={(color) => onColorChange(color, 'main')}
|
||||||
|
label={t('annotation.changeColor', 'Change Colour')}
|
||||||
|
/>
|
||||||
|
<OpacityControl value={currentOpacity} onChange={onOpacityChange} />
|
||||||
|
<DeleteButton onDelete={onDelete} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'ink':
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{attachCommentButton}
|
||||||
|
<ColorControl
|
||||||
|
value={currentColor}
|
||||||
|
onChange={(color) => onColorChange(color, 'main')}
|
||||||
|
label={t('annotation.changeColor', 'Change Colour')}
|
||||||
|
/>
|
||||||
|
<WidthControl value={currentWidth} onChange={onWidthChange} min={1} max={12} />
|
||||||
|
<DeleteButton onDelete={onDelete} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'inkHighlighter':
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{attachCommentButton}
|
||||||
|
<ColorControl
|
||||||
|
value={currentColor}
|
||||||
|
onChange={(color) => onColorChange(color, 'main')}
|
||||||
|
label={t('annotation.changeColor', 'Change Colour')}
|
||||||
|
/>
|
||||||
|
<WidthControl value={currentWidth} onChange={onWidthChange} min={1} max={20} />
|
||||||
|
<OpacityControl value={currentOpacity} onChange={onOpacityChange} />
|
||||||
|
<DeleteButton onDelete={onDelete} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'text':
|
||||||
|
case 'note':
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{attachCommentButton}
|
||||||
|
<ColorControl
|
||||||
|
value={textColor}
|
||||||
|
onChange={(color) => onColorChange(color, 'text')}
|
||||||
|
label={t('annotation.color', 'Color')}
|
||||||
|
/>
|
||||||
|
<ColorControl
|
||||||
|
value={backgroundColor}
|
||||||
|
onChange={(color) => onColorChange(color, 'background')}
|
||||||
|
label={t('annotation.backgroundColor', 'Background color')}
|
||||||
|
/>
|
||||||
|
<EditTextButton onEdit={onEdit} />
|
||||||
|
<PropertiesPopover
|
||||||
|
annotationType={annotationType as PropertiesAnnotationType}
|
||||||
|
annotation={annotation}
|
||||||
|
onUpdate={onPropertiesUpdate}
|
||||||
|
/>
|
||||||
|
<DeleteButton onDelete={onDelete} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'comment':
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<CommentButton hasContent={hasCommentContent} onClick={onViewComment} />
|
||||||
|
<EditTextButton onEdit={onEdit} />
|
||||||
|
<ColorControl
|
||||||
|
value={obj?.strokeColor || obj?.color || '#ffa000'}
|
||||||
|
onChange={onCommentColorChange}
|
||||||
|
label={t('annotation.annotationStyle', 'Annotation style')}
|
||||||
|
/>
|
||||||
|
<LinkButton firstLinkTarget={firstLinkTarget} onGoToLink={onGoToLink} onAddLink={onAddLink} />
|
||||||
|
<DeleteButton onDelete={onDelete} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'shape':
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{attachCommentButton}
|
||||||
|
<ColorControl
|
||||||
|
value={strokeColor}
|
||||||
|
onChange={(color) => onColorChange(color, 'stroke')}
|
||||||
|
label={t('annotation.strokeColor', 'Stroke Colour')}
|
||||||
|
/>
|
||||||
|
<ColorControl
|
||||||
|
value={fillColor}
|
||||||
|
onChange={(color) => onColorChange(color, 'fill')}
|
||||||
|
label={t('annotation.fillColor', 'Fill Colour')}
|
||||||
|
/>
|
||||||
|
<PropertiesPopover
|
||||||
|
annotationType="shape"
|
||||||
|
annotation={annotation}
|
||||||
|
onUpdate={onPropertiesUpdate}
|
||||||
|
/>
|
||||||
|
<DeleteButton onDelete={onDelete} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'line':
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{attachCommentButton}
|
||||||
|
<ColorControl
|
||||||
|
value={currentColor}
|
||||||
|
onChange={(color) => onColorChange(color, 'main')}
|
||||||
|
label={t('annotation.changeColor', 'Change Colour')}
|
||||||
|
/>
|
||||||
|
<WidthControl value={currentWidth} onChange={onWidthChange} min={1} max={12} />
|
||||||
|
<DeleteButton onDelete={onDelete} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'stamp':
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{attachCommentButton}
|
||||||
|
<DeleteButton onDelete={onDelete} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{attachCommentButton}
|
||||||
|
<CommentButton hasContent={hasCommentContent} onClick={onViewComment} />
|
||||||
|
<LinkButton firstLinkTarget={firstLinkTarget} onGoToLink={onGoToLink} onAddLink={onAddLink} />
|
||||||
|
<ColorControl
|
||||||
|
value={currentColor}
|
||||||
|
onChange={(color) => onColorChange(color, 'main')}
|
||||||
|
label={t('annotation.changeColor', 'Change Colour')}
|
||||||
|
/>
|
||||||
|
<DeleteButton onDelete={onDelete} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import '@app/components/viewer/AttachmentSidebar.css';
|
|||||||
interface AttachmentSidebarProps {
|
interface AttachmentSidebarProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
thumbnailVisible: boolean;
|
thumbnailVisible: boolean;
|
||||||
|
bookmarkVisible: boolean;
|
||||||
documentCacheKey?: string;
|
documentCacheKey?: string;
|
||||||
preloadCacheKeys?: string[];
|
preloadCacheKeys?: string[];
|
||||||
}
|
}
|
||||||
@@ -32,7 +33,7 @@ const createEntry = (overrides: Partial<AttachmentCacheEntry> = {}): AttachmentC
|
|||||||
...overrides,
|
...overrides,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const AttachmentSidebar = ({ visible, thumbnailVisible, documentCacheKey, preloadCacheKeys = [] }: AttachmentSidebarProps) => {
|
export const AttachmentSidebar = ({ visible, thumbnailVisible, bookmarkVisible, documentCacheKey, preloadCacheKeys = [] }: AttachmentSidebarProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { attachmentActions, hasAttachmentSupport } = useViewer();
|
const { attachmentActions, hasAttachmentSupport } = useViewer();
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
@@ -286,7 +287,7 @@ export const AttachmentSidebar = ({ visible, thumbnailVisible, documentCacheKey,
|
|||||||
className="attachment-sidebar"
|
className="attachment-sidebar"
|
||||||
style={{
|
style={{
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
right: thumbnailVisible ? SIDEBAR_WIDTH : 0,
|
right: `${(thumbnailVisible ? 15 : 0) + (bookmarkVisible ? 15 : 0)}rem`,
|
||||||
top: 0,
|
top: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
width: SIDEBAR_WIDTH,
|
width: SIDEBAR_WIDTH,
|
||||||
|
|||||||
@@ -0,0 +1,704 @@
|
|||||||
|
import { useMemo, useState, useCallback, useEffect, useRef } from 'react';
|
||||||
|
import { Box, ScrollArea, Text, Textarea, Stack, ActionIcon, Group, Tooltip, TextInput, Menu, Modal, Button, UnstyledButton } from '@mantine/core';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import DeleteIcon from '@mui/icons-material/Delete';
|
||||||
|
import CheckIcon from '@mui/icons-material/CheckRounded';
|
||||||
|
import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
|
||||||
|
import EditIcon from '@mui/icons-material/Edit';
|
||||||
|
import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||||
|
import { useAnnotation } from '@embedpdf/plugin-annotation/react';
|
||||||
|
import { getSidebarAnnotationsWithRepliesGroupedByPage } from '@embedpdf/plugin-annotation';
|
||||||
|
import { PdfAnnotationSubtype, PdfAnnotationReplyType } from '@embedpdf/models';
|
||||||
|
import { useCommentAuthor } from '@app/contexts/CommentAuthorContext';
|
||||||
|
import { useViewer } from '@app/contexts/ViewerContext';
|
||||||
|
import LocalIcon from '@app/components/shared/LocalIcon';
|
||||||
|
|
||||||
|
const SIDEBAR_WIDTH = '18rem';
|
||||||
|
|
||||||
|
/** Format annotation date for display (e.g. "Mar 11, 6:05 PM"). */
|
||||||
|
function formatCommentDate(obj: any): string {
|
||||||
|
const raw = obj?.modifiedDate ?? obj?.creationDate ?? obj?.customData?.modifiedDate ?? obj?.M;
|
||||||
|
if (raw == null) return '';
|
||||||
|
const d = raw instanceof Date ? raw : new Date(raw);
|
||||||
|
if (Number.isNaN(d.getTime())) return '';
|
||||||
|
return d.toLocaleString(undefined, { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' });
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CommentsSidebarProps {
|
||||||
|
documentId: string;
|
||||||
|
visible: boolean;
|
||||||
|
rightOffset: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCommentDisplayContent(entry: { annotation: { object: any }; replies: Array<{ object: any }> }): string {
|
||||||
|
const main = entry.annotation?.object?.contents;
|
||||||
|
if (main != null && String(main).trim()) return String(main).trim();
|
||||||
|
const firstReply = entry.replies?.[0]?.object?.contents;
|
||||||
|
if (firstReply != null && String(firstReply).trim()) return String(firstReply).trim();
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Placeholder authors we never show; use current user's name from context instead. */
|
||||||
|
const PLACEHOLDER_AUTHORS = new Set(['Guest', 'Digital Signature', '']);
|
||||||
|
|
||||||
|
function getAuthorName(obj: any, currentDisplayName: string): string {
|
||||||
|
const stored = (obj?.author ?? 'Guest').trim() || 'Guest';
|
||||||
|
if (PLACEHOLDER_AUTHORS.has(stored)) return currentDisplayName || 'Guest';
|
||||||
|
return stored;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Replies store an explicit author; only allow edit when it matches the current comment author name. */
|
||||||
|
function isReplyAuthoredByCurrentUser(obj: any, currentDisplayName: string): boolean {
|
||||||
|
const stored = (obj?.author ?? '').trim() || 'Guest';
|
||||||
|
// Resolve current user the same way getAuthorName does
|
||||||
|
const resolvedMine = PLACEHOLDER_AUTHORS.has((currentDisplayName ?? '').trim())
|
||||||
|
? 'Guest'
|
||||||
|
: (currentDisplayName ?? '').trim();
|
||||||
|
const resolvedStored = PLACEHOLDER_AUTHORS.has(stored) ? 'Guest' : stored;
|
||||||
|
// Both are guest/anonymous → same unauthenticated user, allow editing
|
||||||
|
if (resolvedStored === 'Guest' && resolvedMine === 'Guest') return true;
|
||||||
|
if (!resolvedMine) return false;
|
||||||
|
return resolvedStored === resolvedMine;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map toolId → LocalIcon icon name (matches AnnotationPanel icon definitions)
|
||||||
|
const TOOL_ICON_MAP: Record<string, string> = {
|
||||||
|
highlight: 'highlight',
|
||||||
|
underline: 'format-underlined',
|
||||||
|
strikeout: 'strikethrough-s',
|
||||||
|
squiggly: 'show-chart',
|
||||||
|
ink: 'edit',
|
||||||
|
inkHighlighter: 'brush',
|
||||||
|
square: 'crop-square',
|
||||||
|
circle: 'radio-button-unchecked',
|
||||||
|
line: 'show-chart',
|
||||||
|
lineArrow: 'show-chart',
|
||||||
|
polyline: 'show-chart',
|
||||||
|
polygon: 'change-history',
|
||||||
|
text: 'text-fields',
|
||||||
|
note: 'sticky-note-2',
|
||||||
|
stamp: 'add-photo-alternate',
|
||||||
|
textComment: 'comment',
|
||||||
|
insertText: 'add-comment',
|
||||||
|
replaceText: 'find-replace',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Type-based fallback icon when no toolId is present
|
||||||
|
function getIconByType(type: number | undefined): string {
|
||||||
|
if (type === 1) return 'comment';
|
||||||
|
if (type === 3) return 'sticky-note-2';
|
||||||
|
if (type === 4 || type === 8) return 'show-chart';
|
||||||
|
if (type === 5) return 'crop-square';
|
||||||
|
if (type === 6) return 'radio-button-unchecked';
|
||||||
|
if (type === 7 || type === 8) return 'change-history';
|
||||||
|
if (type === 9) return 'highlight';
|
||||||
|
if (type === 10) return 'format-underlined';
|
||||||
|
if (type === 11) return 'show-chart';
|
||||||
|
if (type === 12) return 'strikethrough-s';
|
||||||
|
if (type === 13) return 'add-photo-alternate';
|
||||||
|
if (type === 14) return 'add-comment';
|
||||||
|
if (type === 15) return 'edit';
|
||||||
|
return 'comment';
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCommentAnnotation(ann: any): boolean {
|
||||||
|
const toolId = ann?.customData?.toolId ?? ann?.customData?.annotationToolId;
|
||||||
|
if (toolId === 'textComment' || toolId === 'insertText' || toolId === 'replaceText') return true;
|
||||||
|
// Any annotation explicitly added to comments via the "Add comment" button
|
||||||
|
if (ann?.customData?.isComment === true) return true;
|
||||||
|
// CARET (type 14) = insertText/replaceText; TEXT (type 1) = textComment
|
||||||
|
if (!toolId && (ann?.type === 14 || ann?.type === 1)) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAnnotationToolId(ann: any): string {
|
||||||
|
return ann?.customData?.toolId ?? ann?.customData?.annotationToolId ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAnnotationTypeLabel(ann: any, t: (key: string, fallback: string) => string): string {
|
||||||
|
const toolId = getAnnotationToolId(ann);
|
||||||
|
const labels: Record<string, string> = {
|
||||||
|
highlight: t('annotation.highlight', 'Highlight'),
|
||||||
|
underline: t('annotation.underline', 'Underline'),
|
||||||
|
strikeout: t('annotation.strikeout', 'Strikeout'),
|
||||||
|
squiggly: t('annotation.squiggly', 'Squiggly'),
|
||||||
|
ink: t('annotation.pen', 'Pen'),
|
||||||
|
inkHighlighter: t('annotation.freehandHighlighter', 'Freehand Highlighter'),
|
||||||
|
square: t('annotation.square', 'Square'),
|
||||||
|
circle: t('annotation.circle', 'Circle'),
|
||||||
|
line: t('annotation.line', 'Line'),
|
||||||
|
lineArrow: t('annotation.lineArrow', 'Arrow'),
|
||||||
|
polyline: t('annotation.polyline', 'Polyline'),
|
||||||
|
polygon: t('annotation.polygon', 'Polygon'),
|
||||||
|
text: t('annotation.text', 'Text box'),
|
||||||
|
note: t('annotation.note', 'Note'),
|
||||||
|
stamp: t('annotation.stamp', 'Stamp'),
|
||||||
|
textComment: t('viewer.comments.typeComment', 'Comment'),
|
||||||
|
insertText: t('viewer.comments.typeInsertText', 'Insert Text'),
|
||||||
|
replaceText: t('viewer.comments.typeReplaceText', 'Replace Text'),
|
||||||
|
};
|
||||||
|
if (labels[toolId]) return labels[toolId];
|
||||||
|
// Type-based fallback (mirrors getIconByType) for annotations without customData.toolId
|
||||||
|
const type = ann?.type;
|
||||||
|
if (type === 14) return t('viewer.comments.typeInsertText', 'Insert Text');
|
||||||
|
if (type === 1) return t('viewer.comments.typeComment', 'Comment');
|
||||||
|
return t('viewer.comments.typeComment', 'Comment');
|
||||||
|
}
|
||||||
|
|
||||||
|
function AnnotationTypeIcon({ ann }: { ann: any }) {
|
||||||
|
const toolId = getAnnotationToolId(ann);
|
||||||
|
const iconName = TOOL_ICON_MAP[toolId] ?? getIconByType(ann?.type);
|
||||||
|
return (
|
||||||
|
<LocalIcon
|
||||||
|
icon={iconName}
|
||||||
|
width="1.25rem"
|
||||||
|
height="1.25rem"
|
||||||
|
style={{ flexShrink: 0, color: 'var(--mantine-color-blue-5)' }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CommentsSidebar({ documentId, visible, rightOffset }: CommentsSidebarProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { displayName } = useCommentAuthor();
|
||||||
|
const { highlightCommentRequest, clearHighlightCommentRequest, scrollActions, getZoomState } = useViewer() ?? {};
|
||||||
|
const scrollViewportRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const { state, provides } = useAnnotation(documentId);
|
||||||
|
const [draftContents, setDraftContents] = useState<Record<string, string>>({});
|
||||||
|
const [replyDrafts, setReplyDrafts] = useState<Record<string, string>>({});
|
||||||
|
/** Draft text while editing an existing reply (`${pageIndex}_${parentId}_${replyId}`). */
|
||||||
|
const [replyEditDrafts, setReplyEditDrafts] = useState<Record<string, string>>({});
|
||||||
|
/** When set, this card's main comment is in edit mode (show textarea for main comment). */
|
||||||
|
const [editingMainKey, setEditingMainKey] = useState<string | null>(null);
|
||||||
|
/** Which reply is in edit mode (same key shape as replyEditDrafts). */
|
||||||
|
const [editingReplyKey, setEditingReplyKey] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// React to request to focus or highlight a comment card (e.g. from "Add comment" / "View comment" in selection menu)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!visible || !highlightCommentRequest || highlightCommentRequest.documentId !== documentId) return;
|
||||||
|
const { pageIndex, annotationId, action } = highlightCommentRequest;
|
||||||
|
const cardKey = `${pageIndex}_${annotationId}`;
|
||||||
|
const root = scrollViewportRef.current;
|
||||||
|
if (!root) return;
|
||||||
|
const card = root.querySelector<HTMLElement>(`[data-comment-card="${cardKey}"]`);
|
||||||
|
if (!card) {
|
||||||
|
clearHighlightCommentRequest?.();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
card.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
if (action === 'highlight') {
|
||||||
|
card.classList.remove('comment-card-flash-highlight');
|
||||||
|
void card.offsetWidth;
|
||||||
|
card.classList.add('comment-card-flash-highlight');
|
||||||
|
const tId = window.setTimeout(() => {
|
||||||
|
card.classList.remove('comment-card-flash-highlight');
|
||||||
|
clearHighlightCommentRequest?.();
|
||||||
|
}, 1500);
|
||||||
|
return () => window.clearTimeout(tId);
|
||||||
|
}
|
||||||
|
// action === 'focus': focus the first textarea or reply input in the card
|
||||||
|
const input = card.querySelector<HTMLTextAreaElement | HTMLInputElement>('textarea, input');
|
||||||
|
if (input) {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
input.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
clearHighlightCommentRequest?.();
|
||||||
|
}, [visible, highlightCommentRequest, documentId, clearHighlightCommentRequest]);
|
||||||
|
|
||||||
|
const handleLocateAnnotation = useCallback((pageIndex: number, ann: any) => {
|
||||||
|
scrollActions?.scrollToPage(pageIndex + 1, 'smooth');
|
||||||
|
setTimeout(() => {
|
||||||
|
const pageEl = document.querySelector<HTMLElement>(`[data-page-index="${pageIndex}"]`);
|
||||||
|
if (!pageEl || !ann?.rect) return;
|
||||||
|
const zoom = getZoomState?.()?.currentZoom ?? 1;
|
||||||
|
const { origin, size } = ann.rect as { origin: { x: number; y: number }; size: { width: number; height: number } };
|
||||||
|
const flashEl = document.createElement('div');
|
||||||
|
// Append to page element so it scrolls with the page (position: absolute relative to page)
|
||||||
|
flashEl.style.cssText = `
|
||||||
|
position: absolute;
|
||||||
|
left: ${origin.x * zoom}px;
|
||||||
|
top: ${origin.y * zoom}px;
|
||||||
|
width: ${size.width * zoom}px;
|
||||||
|
height: ${size.height * zoom}px;
|
||||||
|
background: rgba(255, 213, 0, 0.55);
|
||||||
|
border: 2px solid rgba(255, 170, 0, 0.8);
|
||||||
|
border-radius: 3px;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 9998;
|
||||||
|
animation: annotation-locate-flash 1.6s ease-out forwards;
|
||||||
|
`;
|
||||||
|
if (!document.getElementById('annotation-locate-flash-style')) {
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.id = 'annotation-locate-flash-style';
|
||||||
|
style.textContent = `@keyframes annotation-locate-flash {
|
||||||
|
0% { opacity: 0; transform: scale(1.08); }
|
||||||
|
15% { opacity: 1; transform: scale(1); }
|
||||||
|
70% { opacity: 1; }
|
||||||
|
100% { opacity: 0; }
|
||||||
|
}`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
}
|
||||||
|
pageEl.appendChild(flashEl);
|
||||||
|
setTimeout(() => flashEl.remove(), 1700);
|
||||||
|
}, 550);
|
||||||
|
}, [scrollActions, getZoomState]);
|
||||||
|
|
||||||
|
const byPage = useMemo(() => {
|
||||||
|
try {
|
||||||
|
const all = getSidebarAnnotationsWithRepliesGroupedByPage(state) ?? {};
|
||||||
|
const filtered: typeof all = {};
|
||||||
|
for (const [page, entries] of Object.entries(all)) {
|
||||||
|
const commentEntries = (entries as typeof entries).filter(
|
||||||
|
(e) => isCommentAnnotation((e as any).annotation?.object)
|
||||||
|
);
|
||||||
|
if (commentEntries.length > 0) {
|
||||||
|
filtered[Number(page)] = commentEntries;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filtered;
|
||||||
|
} catch {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}, [state]);
|
||||||
|
|
||||||
|
// Derive the set of selected annotation IDs from EmbedPDF's selection state.
|
||||||
|
// state is AnnotationDocumentState — selectedUids are keys in byUid, and may equal id.
|
||||||
|
const selectedAnnotationIds = useMemo(() => {
|
||||||
|
const selectedUids: string[] = state?.selectedUids ?? [];
|
||||||
|
const byUid: Record<string, any> = (state as any)?.byUid ?? {};
|
||||||
|
const ids = new Set<string>();
|
||||||
|
for (const uid of selectedUids) {
|
||||||
|
// uid itself may be the annotation id
|
||||||
|
ids.add(uid);
|
||||||
|
const annId = byUid[uid]?.object?.id;
|
||||||
|
if (annId) ids.add(annId);
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}, [state]);
|
||||||
|
|
||||||
|
const pageNumbers = useMemo(() => Object.keys(byPage).map(Number).sort((a, b) => a - b), [byPage]);
|
||||||
|
const totalCount = useMemo(() => pageNumbers.reduce((sum, p) => sum + (byPage[p]?.length ?? 0), 0), [pageNumbers, byPage]);
|
||||||
|
|
||||||
|
const handleContentsChange = useCallback(
|
||||||
|
(pageIndex: number, annotationId: string, value: string) => {
|
||||||
|
setDraftContents((prev) => ({ ...prev, [pageIndex + '_' + annotationId]: value }));
|
||||||
|
if (!provides?.updateAnnotation) return;
|
||||||
|
provides.updateAnnotation(pageIndex, annotationId, { contents: value });
|
||||||
|
},
|
||||||
|
[provides]
|
||||||
|
);
|
||||||
|
|
||||||
|
const [deleteModal, setDeleteModal] = useState<{ pageIndex: number; id: string; ann: any } | null>(null);
|
||||||
|
|
||||||
|
const isLinkedAnnotation = (ann: any) => {
|
||||||
|
const toolId = ann?.customData?.toolId ?? ann?.customData?.annotationToolId;
|
||||||
|
return ann?.customData?.isComment === true &&
|
||||||
|
toolId !== 'textComment' && toolId !== 'insertText' && toolId !== 'replaceText';
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteClick = useCallback(
|
||||||
|
(pageIndex: number, annotationId: string, ann: any) => {
|
||||||
|
if (isLinkedAnnotation(ann)) {
|
||||||
|
setDeleteModal({ pageIndex, id: annotationId, ann });
|
||||||
|
} else {
|
||||||
|
provides?.deleteAnnotation?.(pageIndex, annotationId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[provides]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleRemoveFromSidebar = useCallback(() => {
|
||||||
|
if (!deleteModal || !provides?.updateAnnotation) return;
|
||||||
|
const { pageIndex, id, ann } = deleteModal;
|
||||||
|
const existing = (ann?.customData ?? {}) as Record<string, unknown>;
|
||||||
|
const { isComment: _removed, ...rest } = existing;
|
||||||
|
provides.updateAnnotation(pageIndex, id, { customData: rest } as any);
|
||||||
|
setDeleteModal(null);
|
||||||
|
}, [deleteModal, provides]);
|
||||||
|
|
||||||
|
const handleDeleteAnnotation = useCallback(() => {
|
||||||
|
if (!deleteModal) return;
|
||||||
|
provides?.deleteAnnotation?.(deleteModal.pageIndex, deleteModal.id);
|
||||||
|
setDeleteModal(null);
|
||||||
|
}, [deleteModal, provides]);
|
||||||
|
|
||||||
|
const handleSendMainComment = useCallback(
|
||||||
|
(pageIndex: number, annotationId: string, value: string) => {
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (!trimmed || !provides?.updateAnnotation) return;
|
||||||
|
provides.updateAnnotation(pageIndex, annotationId, { contents: trimmed, author: displayName });
|
||||||
|
setDraftContents((prev) => ({ ...prev, [pageIndex + '_' + annotationId]: trimmed }));
|
||||||
|
},
|
||||||
|
[provides, displayName]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSendReply = useCallback(
|
||||||
|
(pageIndex: number, parentId: string, parentRect: any) => {
|
||||||
|
const key = `${pageIndex}_${parentId}_reply`;
|
||||||
|
const text = replyDrafts[key]?.trim();
|
||||||
|
if (!text || !provides?.createAnnotation) return;
|
||||||
|
const rect = parentRect ?? { origin: { x: 0, y: 0 }, size: { width: 1, height: 1 } };
|
||||||
|
provides.createAnnotation(pageIndex, {
|
||||||
|
type: PdfAnnotationSubtype.TEXT,
|
||||||
|
id: `reply-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
|
||||||
|
pageIndex,
|
||||||
|
rect,
|
||||||
|
contents: text,
|
||||||
|
inReplyToId: parentId,
|
||||||
|
replyType: PdfAnnotationReplyType.Reply,
|
||||||
|
author: displayName,
|
||||||
|
} as any);
|
||||||
|
setReplyDrafts((prev) => ({ ...prev, [key]: '' }));
|
||||||
|
},
|
||||||
|
[provides, replyDrafts, displayName]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSaveReplyEdit = useCallback(
|
||||||
|
(editKey: string, pageIndex: number, replyId: string, value: string) => {
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (!trimmed || !provides?.updateAnnotation) return;
|
||||||
|
provides.updateAnnotation(pageIndex, replyId, { contents: trimmed, author: displayName });
|
||||||
|
setReplyEditDrafts((prev) => {
|
||||||
|
const next = { ...prev };
|
||||||
|
delete next[editKey];
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
setEditingReplyKey(null);
|
||||||
|
},
|
||||||
|
[provides, displayName]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!visible) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
ref={scrollViewportRef}
|
||||||
|
style={{
|
||||||
|
position: 'fixed',
|
||||||
|
right: rightOffset,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
width: SIDEBAR_WIDTH,
|
||||||
|
backgroundColor: 'var(--right-rail-bg)',
|
||||||
|
borderLeft: '1px solid var(--border-subtle)',
|
||||||
|
zIndex: 998,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
boxShadow: '-2px 0 8px rgba(0, 0, 0, 0.1)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: '0.75rem 1rem',
|
||||||
|
borderBottom: '1px solid var(--border-subtle)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '0.5rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LocalIcon icon="comment" width="1.25rem" height="1.25rem" style={{ color: 'var(--mantine-color-dimmed)', flexShrink: 0 }} />
|
||||||
|
<Text fw={600} size="sm" tt="uppercase" lts={0.5}>
|
||||||
|
{t('viewer.comments.title', 'Comments')}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<ScrollArea style={{ flex: 1 }}>
|
||||||
|
<Stack p="sm" gap="md">
|
||||||
|
{totalCount === 0 ? (
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
{t('viewer.comments.hint', 'Place comments with the Comment, Insert Text, or Replace Text tools. They will appear here by page.')}
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
pageNumbers.map((pageIndex) => {
|
||||||
|
const entries = byPage[pageIndex] ?? [];
|
||||||
|
const pageNum = pageIndex + 1;
|
||||||
|
return (
|
||||||
|
<Box key={pageIndex} mb="md">
|
||||||
|
<Text size="sm" fw={700} mb={2}>
|
||||||
|
{t('viewer.comments.pageLabel', 'Page {{page}}', { page: pageNum })}
|
||||||
|
</Text>
|
||||||
|
<Text size="xs" c="dimmed" mb="sm">
|
||||||
|
{entries.length === 1
|
||||||
|
? t('viewer.comments.oneComment', '1 comment')
|
||||||
|
: t('viewer.comments.nComments', '{{count}} comments', { count: entries.length })}
|
||||||
|
</Text>
|
||||||
|
<Box
|
||||||
|
mb="xs"
|
||||||
|
style={{
|
||||||
|
borderBottom: '1px solid var(--border-subtle)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Stack gap="sm">
|
||||||
|
{entries.map((entry) => {
|
||||||
|
const ann = entry.annotation?.object;
|
||||||
|
const id = ann?.id;
|
||||||
|
if (!id) return null;
|
||||||
|
const key = `${pageIndex}_${id}`;
|
||||||
|
const replyKey = `${pageIndex}_${id}_reply`;
|
||||||
|
const displayContent = getCommentDisplayContent(entry);
|
||||||
|
const draft = draftContents[key] !== undefined ? draftContents[key] : displayContent;
|
||||||
|
const replyDraft = replyDrafts[replyKey] ?? '';
|
||||||
|
const authorName = getAuthorName(ann, displayName);
|
||||||
|
/** Only treat as "comment posted" when annotation actually has content (user clicked Send), not on every keystroke. */
|
||||||
|
const hasMainContent = (displayContent ?? '').trim().length > 0;
|
||||||
|
const isEditingMain = editingMainKey === key;
|
||||||
|
|
||||||
|
const mainTimestamp = formatCommentDate(ann);
|
||||||
|
const typeLabel = getAnnotationTypeLabel(ann, t);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
key={key}
|
||||||
|
data-comment-card={key}
|
||||||
|
p="sm"
|
||||||
|
style={{
|
||||||
|
border: selectedAnnotationIds.has(id)
|
||||||
|
? '1px solid var(--mantine-color-blue-3)'
|
||||||
|
: '1px solid var(--border-subtle)',
|
||||||
|
borderRadius: 8,
|
||||||
|
backgroundColor: 'var(--bg-raised)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Group wrap="nowrap" gap="xs" justify="space-between" align="flex-start" mb="xs">
|
||||||
|
<Group wrap="nowrap" gap="xs" style={{ minWidth: 0, flex: 1 }}>
|
||||||
|
<AnnotationTypeIcon ann={ann} />
|
||||||
|
<Box style={{ minWidth: 0 }}>
|
||||||
|
<Text size="sm" fw={600}>
|
||||||
|
{authorName}
|
||||||
|
</Text>
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
{typeLabel}{mainTimestamp ? ` · ${mainTimestamp}` : ''}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Group>
|
||||||
|
<Group gap={2} wrap="nowrap" style={{ flexShrink: 0 }}>
|
||||||
|
<Tooltip label={t('viewer.comments.locateAnnotation', 'Locate in document')}>
|
||||||
|
<ActionIcon
|
||||||
|
variant="subtle"
|
||||||
|
size="sm"
|
||||||
|
color="gray"
|
||||||
|
onClick={() => handleLocateAnnotation(pageIndex, ann)}
|
||||||
|
>
|
||||||
|
<VisibilityIcon style={{ fontSize: 16 }} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
<Menu position="bottom-end" withArrow>
|
||||||
|
<Menu.Target>
|
||||||
|
<Tooltip label={t('viewer.comments.moreActions', 'More actions')}>
|
||||||
|
<ActionIcon variant="subtle" size="sm" color="gray">
|
||||||
|
<MoreHorizIcon style={{ fontSize: 20 }} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
</Menu.Target>
|
||||||
|
<Menu.Dropdown>
|
||||||
|
<Menu.Item
|
||||||
|
leftSection={<EditIcon style={{ fontSize: 18 }} />}
|
||||||
|
onClick={() => setEditingMainKey(key)}
|
||||||
|
>
|
||||||
|
{t('annotation.editText', 'Edit')}
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item
|
||||||
|
leftSection={<DeleteIcon style={{ fontSize: 18 }} />}
|
||||||
|
color="red"
|
||||||
|
onClick={() => handleDeleteClick(pageIndex, id, ann)}
|
||||||
|
>
|
||||||
|
{t('annotation.delete', 'Delete')}
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu.Dropdown>
|
||||||
|
</Menu>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{!hasMainContent || isEditingMain ? (
|
||||||
|
<>
|
||||||
|
<Textarea
|
||||||
|
placeholder={t('viewer.comments.addCommentPlaceholder', 'Add comment...')}
|
||||||
|
minRows={2}
|
||||||
|
autosize
|
||||||
|
value={draft ?? ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
const v = (e?.currentTarget ?? e?.target)?.value ?? '';
|
||||||
|
setDraftContents((prev) => ({ ...prev, [key]: v }));
|
||||||
|
if (isEditingMain) {
|
||||||
|
handleContentsChange(pageIndex, id, v);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
styles={{ root: { width: '100%' } }}
|
||||||
|
mb="xs"
|
||||||
|
/>
|
||||||
|
<Group gap={4} wrap="nowrap" justify="flex-end">
|
||||||
|
<Tooltip label={t('viewer.comments.addComment', 'Add comment')}>
|
||||||
|
<ActionIcon
|
||||||
|
variant="filled"
|
||||||
|
size="sm"
|
||||||
|
color="blue"
|
||||||
|
onClick={() => {
|
||||||
|
handleSendMainComment(pageIndex, id, draft ?? '');
|
||||||
|
setEditingMainKey(null);
|
||||||
|
}}
|
||||||
|
disabled={!(draft ?? '').trim()}
|
||||||
|
>
|
||||||
|
<CheckIcon style={{ fontSize: 18, color: 'white' }} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
</Group>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Text size="sm" mb="sm" style={{ whiteSpace: 'pre-wrap' }}>
|
||||||
|
{displayContent}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{entry.replies?.length ? (
|
||||||
|
<Stack gap="sm" mb="sm">
|
||||||
|
{entry.replies.map((r) => {
|
||||||
|
const rObj = r?.object;
|
||||||
|
const rId = rObj?.id;
|
||||||
|
if (!rId) return null;
|
||||||
|
const rAuthor = getAuthorName(rObj, displayName);
|
||||||
|
const rTimestamp = formatCommentDate(rObj);
|
||||||
|
const replyEditKey = `${pageIndex}_${id}_${rId}`;
|
||||||
|
const isEditingReply = editingReplyKey === replyEditKey;
|
||||||
|
const canEditReply = isReplyAuthoredByCurrentUser(rObj, displayName);
|
||||||
|
const replyBody =
|
||||||
|
replyEditDrafts[replyEditKey] !== undefined
|
||||||
|
? replyEditDrafts[replyEditKey]
|
||||||
|
: (rObj?.contents ?? '');
|
||||||
|
return (
|
||||||
|
<Box key={rId} pl="xs" style={{ borderLeft: '2px solid var(--mantine-color-blue-3)' }}>
|
||||||
|
<Box style={{ minWidth: 0 }}>
|
||||||
|
<Group wrap="nowrap" justify="space-between" align="flex-start" gap={4} mb={2}>
|
||||||
|
<Text size="sm" fw={600}>
|
||||||
|
{rAuthor}
|
||||||
|
</Text>
|
||||||
|
<Group wrap="nowrap" gap="xs" align="center">
|
||||||
|
{canEditReply && !isEditingReply ? (
|
||||||
|
<UnstyledButton
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setEditingReplyKey(replyEditKey);
|
||||||
|
setReplyEditDrafts(() => ({
|
||||||
|
[replyEditKey]: String(rObj?.contents ?? ''),
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text size="xs" c="blue">
|
||||||
|
{t('annotation.editText', 'Edit')}
|
||||||
|
</Text>
|
||||||
|
</UnstyledButton>
|
||||||
|
) : null}
|
||||||
|
{rTimestamp ? (
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
{rTimestamp}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
{isEditingReply ? (
|
||||||
|
<>
|
||||||
|
<Textarea
|
||||||
|
minRows={2}
|
||||||
|
autosize
|
||||||
|
value={replyBody}
|
||||||
|
onChange={(e) => {
|
||||||
|
const v = (e?.currentTarget ?? e?.target)?.value ?? '';
|
||||||
|
setReplyEditDrafts((p) => ({ ...p, [replyEditKey]: v }));
|
||||||
|
}}
|
||||||
|
styles={{ root: { width: '100%' } }}
|
||||||
|
mb="xs"
|
||||||
|
/>
|
||||||
|
<Group gap={4} wrap="nowrap" justify="flex-end">
|
||||||
|
<Tooltip label={t('viewer.comments.saveReply', 'Save reply')}>
|
||||||
|
<ActionIcon
|
||||||
|
variant="filled"
|
||||||
|
size="sm"
|
||||||
|
color="blue"
|
||||||
|
onClick={() =>
|
||||||
|
handleSaveReplyEdit(replyEditKey, pageIndex, rId, replyBody)
|
||||||
|
}
|
||||||
|
disabled={!replyBody.trim()}
|
||||||
|
>
|
||||||
|
<CheckIcon style={{ fontSize: 18, color: 'white' }} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
</Group>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Text size="sm" style={{ whiteSpace: 'pre-wrap' }}>
|
||||||
|
{rObj?.contents ?? ''}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Stack>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<Group gap="xs" wrap="nowrap" align="flex-end">
|
||||||
|
<TextInput
|
||||||
|
placeholder={t('viewer.comments.addReplyPlaceholder', 'Add reply...')}
|
||||||
|
size="xs"
|
||||||
|
value={replyDraft}
|
||||||
|
onChange={(e) => {
|
||||||
|
const v = (e?.currentTarget ?? e?.target)?.value ?? '';
|
||||||
|
setReplyDrafts((p) => ({ ...p, [replyKey]: v }));
|
||||||
|
}}
|
||||||
|
style={{ flex: 1, minWidth: 0 }}
|
||||||
|
styles={{
|
||||||
|
input: {
|
||||||
|
borderColor: 'var(--mantine-color-blue-3)',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Tooltip label={t('viewer.comments.addComment', 'Add comment')}>
|
||||||
|
<ActionIcon
|
||||||
|
variant="filled"
|
||||||
|
size="md"
|
||||||
|
color="blue"
|
||||||
|
style={{ backgroundColor: 'var(--mantine-color-blue-6)' }}
|
||||||
|
onClick={() => handleSendReply(pageIndex, id, ann?.rect)}
|
||||||
|
disabled={!replyDraft.trim()}
|
||||||
|
>
|
||||||
|
<CheckIcon style={{ fontSize: 20, color: 'white' }} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
</Group>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</ScrollArea>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
opened={!!deleteModal}
|
||||||
|
onClose={() => setDeleteModal(null)}
|
||||||
|
title={t('viewer.comments.deleteTitle', 'Remove annotation from comments?')}
|
||||||
|
centered
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
<Text size="sm" c="dimmed" mb="lg">
|
||||||
|
{t(
|
||||||
|
'viewer.comments.deleteDescription',
|
||||||
|
'This annotation has a comment attached. You can remove just the comment from the sidebar while keeping the annotation, or delete everything.'
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
<Group justify="flex-end" gap="sm">
|
||||||
|
<Button variant="default" onClick={handleRemoveFromSidebar}>
|
||||||
|
{t('viewer.comments.removeCommentOnly', 'Remove comment only')}
|
||||||
|
</Button>
|
||||||
|
<Button color="red" onClick={handleDeleteAnnotation}>
|
||||||
|
{t('viewer.comments.deleteAnnotationAndComment', 'Delete annotation & comment')}
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Modal>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -126,6 +126,7 @@ const EmbedPdfViewerContent = ({
|
|||||||
toggleThumbnailSidebar,
|
toggleThumbnailSidebar,
|
||||||
isBookmarkSidebarVisible,
|
isBookmarkSidebarVisible,
|
||||||
isAttachmentSidebarVisible,
|
isAttachmentSidebarVisible,
|
||||||
|
isCommentsSidebarVisible,
|
||||||
isSearchInterfaceVisible,
|
isSearchInterfaceVisible,
|
||||||
searchInterfaceActions,
|
searchInterfaceActions,
|
||||||
zoomActions,
|
zoomActions,
|
||||||
@@ -201,8 +202,9 @@ const EmbedPdfViewerContent = ({
|
|||||||
const isSignatureMode = isInAnnotationTool;
|
const isSignatureMode = isInAnnotationTool;
|
||||||
const isManualRedactMode = selectedTool === 'redact';
|
const isManualRedactMode = selectedTool === 'redact';
|
||||||
|
|
||||||
// Enable annotations only when annotation tool is selected
|
// Enable annotations when annotation tool is selected OR when annotations are visible
|
||||||
const shouldEnableAnnotations = selectedTool === 'annotate' || isSignatureMode;
|
// (so users can interact with existing comment annotations in reader/viewer mode)
|
||||||
|
const shouldEnableAnnotations = selectedTool === 'annotate' || isSignatureMode || isAnnotationsVisible;
|
||||||
|
|
||||||
// Enable redaction only when redaction tool is selected
|
// Enable redaction only when redaction tool is selected
|
||||||
const shouldEnableRedaction = selectedTool === 'redact';
|
const shouldEnableRedaction = selectedTool === 'redact';
|
||||||
@@ -910,10 +912,12 @@ const EmbedPdfViewerContent = ({
|
|||||||
}, [isFormFillToolActive, currentFile, currentFileId, fetchFormFields]);
|
}, [isFormFillToolActive, currentFile, currentFileId, fetchFormFields]);
|
||||||
|
|
||||||
const sidebarWidthRem = 15;
|
const sidebarWidthRem = 15;
|
||||||
|
const commentsSidebarWidthRem = 18;
|
||||||
const totalRightMargin =
|
const totalRightMargin =
|
||||||
(isThumbnailSidebarVisible ? sidebarWidthRem : 0) +
|
(isThumbnailSidebarVisible ? sidebarWidthRem : 0) +
|
||||||
(isBookmarkSidebarVisible ? sidebarWidthRem : 0) +
|
(isBookmarkSidebarVisible ? sidebarWidthRem : 0) +
|
||||||
(isAttachmentSidebarVisible ? sidebarWidthRem : 0);
|
(isAttachmentSidebarVisible ? sidebarWidthRem : 0) +
|
||||||
|
(isCommentsSidebarVisible ? commentsSidebarWidthRem : 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@@ -969,6 +973,7 @@ const EmbedPdfViewerContent = ({
|
|||||||
(effectiveFile?.file instanceof File ? effectiveFile.file.name : undefined))
|
(effectiveFile?.file instanceof File ? effectiveFile.file.name : undefined))
|
||||||
}
|
}
|
||||||
enableAnnotations={shouldEnableAnnotations}
|
enableAnnotations={shouldEnableAnnotations}
|
||||||
|
isSignMode={selectedTool === 'sign'}
|
||||||
showBakedAnnotations={isAnnotationsVisible}
|
showBakedAnnotations={isAnnotationsVisible}
|
||||||
enableRedaction={shouldEnableRedaction}
|
enableRedaction={shouldEnableRedaction}
|
||||||
enableFormFill={shouldEnableFormFill}
|
enableFormFill={shouldEnableFormFill}
|
||||||
@@ -978,6 +983,8 @@ const EmbedPdfViewerContent = ({
|
|||||||
historyApiRef={historyApiRef as React.RefObject<any>}
|
historyApiRef={historyApiRef as React.RefObject<any>}
|
||||||
redactionTrackerRef={redactionTrackerRef as React.RefObject<RedactionPendingTrackerAPI>}
|
redactionTrackerRef={redactionTrackerRef as React.RefObject<RedactionPendingTrackerAPI>}
|
||||||
fileId={currentFileId}
|
fileId={currentFileId}
|
||||||
|
isCommentsSidebarVisible={isCommentsSidebarVisible}
|
||||||
|
commentsSidebarRightOffset={`${(isThumbnailSidebarVisible ? sidebarWidthRem : 0) + (isBookmarkSidebarVisible ? sidebarWidthRem : 0) + (isAttachmentSidebarVisible ? sidebarWidthRem : 0)}rem`}
|
||||||
onSignatureAdded={() => {
|
onSignatureAdded={() => {
|
||||||
// Handle signature added - for debugging, enable console logs as needed
|
// Handle signature added - for debugging, enable console logs as needed
|
||||||
// Future: Handle signature completion
|
// Future: Handle signature completion
|
||||||
@@ -1043,6 +1050,7 @@ const EmbedPdfViewerContent = ({
|
|||||||
<AttachmentSidebar
|
<AttachmentSidebar
|
||||||
visible={isAttachmentSidebarVisible}
|
visible={isAttachmentSidebarVisible}
|
||||||
thumbnailVisible={isThumbnailSidebarVisible}
|
thumbnailVisible={isThumbnailSidebarVisible}
|
||||||
|
bookmarkVisible={isBookmarkSidebarVisible}
|
||||||
documentCacheKey={bookmarkCacheKey}
|
documentCacheKey={bookmarkCacheKey}
|
||||||
preloadCacheKeys={allBookmarkCacheKeys}
|
preloadCacheKeys={allBookmarkCacheKeys}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -61,6 +61,9 @@ import { DocumentReadyWrapper } from '@app/components/viewer/DocumentReadyWrappe
|
|||||||
import { ActiveDocumentProvider } from '@app/components/viewer/ActiveDocumentContext';
|
import { ActiveDocumentProvider } from '@app/components/viewer/ActiveDocumentContext';
|
||||||
import { absoluteWithBasePath } from '@app/constants/app';
|
import { absoluteWithBasePath } from '@app/constants/app';
|
||||||
import { FormFieldOverlay } from '@app/tools/formFill/FormFieldOverlay';
|
import { FormFieldOverlay } from '@app/tools/formFill/FormFieldOverlay';
|
||||||
|
import { CommentsSidebar } from '@app/components/viewer/CommentsSidebar';
|
||||||
|
import { CommentAuthorProvider } from '@app/contexts/CommentAuthorContext';
|
||||||
|
import { accountService } from '@app/services/accountService';
|
||||||
|
|
||||||
interface LocalEmbedPDFProps {
|
interface LocalEmbedPDFProps {
|
||||||
file?: File | Blob;
|
file?: File | Blob;
|
||||||
@@ -78,12 +81,24 @@ interface LocalEmbedPDFProps {
|
|||||||
redactionTrackerRef?: React.RefObject<RedactionPendingTrackerAPI>;
|
redactionTrackerRef?: React.RefObject<RedactionPendingTrackerAPI>;
|
||||||
/** File identity passed through to FormFieldOverlay for stale-field guards */
|
/** File identity passed through to FormFieldOverlay for stale-field guards */
|
||||||
fileId?: string | null;
|
fileId?: string | null;
|
||||||
|
/** Comments sidebar visibility and offset (from EmbedPdfViewer) */
|
||||||
|
isCommentsSidebarVisible?: boolean;
|
||||||
|
commentsSidebarRightOffset?: string;
|
||||||
|
/** When true, blocks the general ink/pen annotation tool (sign tool context). */
|
||||||
|
isSignMode?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LocalEmbedPDF({ file, url, fileName, enableAnnotations = false, enableRedaction = false, enableFormFill = false, isManualRedactionMode = false, showBakedAnnotations = true, onSignatureAdded, signatureApiRef, annotationApiRef, historyApiRef, redactionTrackerRef, fileId }: LocalEmbedPDFProps) {
|
export function LocalEmbedPDF({ file, url, fileName, enableAnnotations = false, enableRedaction = false, enableFormFill = false, isManualRedactionMode = false, showBakedAnnotations = true, onSignatureAdded, signatureApiRef, annotationApiRef, historyApiRef, redactionTrackerRef, fileId, isCommentsSidebarVisible = false, commentsSidebarRightOffset = '0rem', isSignMode = false }: LocalEmbedPDFProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [pdfUrl, setPdfUrl] = useState<string | null>(null);
|
const [pdfUrl, setPdfUrl] = useState<string | null>(null);
|
||||||
const [, setAnnotations] = useState<Array<{id: string, pageIndex: number, rect: Rect}>>([]);
|
const [, setAnnotations] = useState<Array<{id: string, pageIndex: number, rect: Rect}>>([]);
|
||||||
|
const [commentAuthorName, setCommentAuthorName] = useState<string>('Guest');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
accountService.getAccountData().then((data) => {
|
||||||
|
if (data?.username) setCommentAuthorName(data.username);
|
||||||
|
}).catch(() => {/* not logged in or security disabled */});
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Convert File to URL if needed
|
// Convert File to URL if needed
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -655,6 +670,27 @@ export function LocalEmbedPDF({ file, url, fileName, enableAnnotations = false,
|
|||||||
rect: event.annotation.rect
|
rect: event.annotation.rect
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
|
// If the annotation doesn't have customData.toolId, patch it from the active tool.
|
||||||
|
// EmbedPDF doesn't always persist customData from setToolDefaults into created annotations.
|
||||||
|
const annotationId = event.annotation.id;
|
||||||
|
const existingCustomData = (event.annotation as unknown as { customData?: Record<string, unknown> }).customData;
|
||||||
|
if (annotationId && !existingCustomData?.toolId) {
|
||||||
|
const activeTool = (annotationApi as unknown as { getActiveTool?: () => { id: string } | null }).getActiveTool?.();
|
||||||
|
if (activeTool?.id && activeTool.id !== 'select') {
|
||||||
|
(annotationApi as unknown as { updateAnnotation?: (page: number, id: string, patch: Record<string, unknown>) => void })
|
||||||
|
.updateAnnotation?.(event.pageIndex, annotationId, {
|
||||||
|
customData: { ...(existingCustomData ?? {}), toolId: activeTool.id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-select the annotation after creation so the selection menu appears immediately,
|
||||||
|
// letting users discover the editing options before they click away.
|
||||||
|
if (annotationId) {
|
||||||
|
(annotationApi as unknown as { selectAnnotation?: (pageIndex: number, id: string) => void })
|
||||||
|
.selectAnnotation?.(event.pageIndex, annotationId);
|
||||||
|
}
|
||||||
|
|
||||||
if (onSignatureAdded) {
|
if (onSignatureAdded) {
|
||||||
onSignatureAdded(event.annotation);
|
onSignatureAdded(event.annotation);
|
||||||
}
|
}
|
||||||
@@ -678,7 +714,7 @@ export function LocalEmbedPDF({ file, url, fileName, enableAnnotations = false,
|
|||||||
{/* Always render RedactionAPIBridge when in manual redaction mode so buttons can switch from annotation mode */}
|
{/* Always render RedactionAPIBridge when in manual redaction mode so buttons can switch from annotation mode */}
|
||||||
{(enableRedaction || isManualRedactionMode) && <RedactionAPIBridge />}
|
{(enableRedaction || isManualRedactionMode) && <RedactionAPIBridge />}
|
||||||
{/* Always render SignatureAPIBridge so annotation tools (draw) can be activated even when starting in redaction mode */}
|
{/* Always render SignatureAPIBridge so annotation tools (draw) can be activated even when starting in redaction mode */}
|
||||||
{(enableAnnotations || enableRedaction || isManualRedactionMode) && <SignatureAPIBridge ref={signatureApiRef} />}
|
{(enableAnnotations || enableRedaction || isManualRedactionMode) && <SignatureAPIBridge ref={signatureApiRef} isSignMode={isSignMode} />}
|
||||||
{(enableRedaction || isManualRedactionMode) && <RedactionPendingTracker ref={redactionTrackerRef} />}
|
{(enableRedaction || isManualRedactionMode) && <RedactionPendingTracker ref={redactionTrackerRef} />}
|
||||||
{enableAnnotations && <AnnotationAPIBridge ref={annotationApiRef} />}
|
{enableAnnotations && <AnnotationAPIBridge ref={annotationApiRef} />}
|
||||||
|
|
||||||
@@ -699,6 +735,7 @@ export function LocalEmbedPDF({ file, url, fileName, enableAnnotations = false,
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{(documentId) => (
|
{(documentId) => (
|
||||||
|
<>
|
||||||
<GlobalPointerProvider documentId={documentId}>
|
<GlobalPointerProvider documentId={documentId}>
|
||||||
<Viewport
|
<Viewport
|
||||||
documentId={documentId}
|
documentId={documentId}
|
||||||
@@ -793,6 +830,16 @@ export function LocalEmbedPDF({ file, url, fileName, enableAnnotations = false,
|
|||||||
/>
|
/>
|
||||||
</Viewport>
|
</Viewport>
|
||||||
</GlobalPointerProvider>
|
</GlobalPointerProvider>
|
||||||
|
{enableAnnotations && (
|
||||||
|
<CommentAuthorProvider displayName={commentAuthorName}>
|
||||||
|
<CommentsSidebar
|
||||||
|
documentId={documentId}
|
||||||
|
visible={isCommentsSidebarVisible}
|
||||||
|
rightOffset={commentsSidebarRightOffset}
|
||||||
|
/>
|
||||||
|
</CommentAuthorProvider>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</DocumentReadyWrapper>
|
</DocumentReadyWrapper>
|
||||||
</ActiveDocumentProvider>
|
</ActiveDocumentProvider>
|
||||||
|
|||||||
@@ -97,11 +97,13 @@ export function PdfViewerToolbar({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handlePreviousPage = () => {
|
const handlePreviousPage = () => {
|
||||||
scrollActions.scrollToPreviousPage();
|
const { currentPage: cur } = getScrollState();
|
||||||
|
if (cur > 1) scrollActions.scrollToPage(cur - 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleNextPage = () => {
|
const handleNextPage = () => {
|
||||||
scrollActions.scrollToNextPage();
|
const { currentPage: cur, totalPages: tot } = getScrollState();
|
||||||
|
if (cur < tot) scrollActions.scrollToPage(cur + 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLastPage = () => {
|
const handleLastPage = () => {
|
||||||
|
|||||||
@@ -133,7 +133,12 @@ const createTextStampImage = (
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SignatureAPIBridge = forwardRef<SignatureAPI>(function SignatureAPIBridge(_, ref) {
|
interface SignatureAPIBridgeProps {
|
||||||
|
/** When true (sign tool context), the general ink/pen annotation tool is blocked. */
|
||||||
|
isSignMode?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SignatureAPIBridge = forwardRef<SignatureAPI, SignatureAPIBridgeProps>(function SignatureAPIBridge({ isSignMode = false }, ref) {
|
||||||
const { provides: annotationApi } = useAnnotationCapability();
|
const { provides: annotationApi } = useAnnotationCapability();
|
||||||
const { signatureConfig, storeImageData, isPlacementMode, placementPreviewSize, setSignaturesApplied } = useSignature();
|
const { signatureConfig, storeImageData, isPlacementMode, placementPreviewSize, setSignaturesApplied } = useSignature();
|
||||||
const { getZoomState, registerImmediateZoomUpdate } = useViewer();
|
const { getZoomState, registerImmediateZoomUpdate } = useViewer();
|
||||||
@@ -151,6 +156,14 @@ export const SignatureAPIBridge = forwardRef<SignatureAPI>(function SignatureAPI
|
|||||||
};
|
};
|
||||||
}, [getZoomState, registerImmediateZoomUpdate]);
|
}, [getZoomState, registerImmediateZoomUpdate]);
|
||||||
|
|
||||||
|
// When entering sign mode, deactivate any active annotation tool immediately.
|
||||||
|
// Only signature-specific tools (signatureInk, stamp) should be usable.
|
||||||
|
useEffect(() => {
|
||||||
|
if (isSignMode && annotationApi && documentReady) {
|
||||||
|
annotationApi.setActiveTool(null);
|
||||||
|
}
|
||||||
|
}, [isSignMode, annotationApi, documentReady]);
|
||||||
|
|
||||||
const cssToPdfSize = useCallback(
|
const cssToPdfSize = useCallback(
|
||||||
(size: { width: number; height: number }) => {
|
(size: { width: number; height: number }) => {
|
||||||
const zoom = currentZoom || 1;
|
const zoom = currentZoom || 1;
|
||||||
@@ -302,13 +315,12 @@ export const SignatureAPIBridge = forwardRef<SignatureAPI>(function SignatureAPI
|
|||||||
activateDrawMode: () => {
|
activateDrawMode: () => {
|
||||||
if (!annotationApi) return;
|
if (!annotationApi) return;
|
||||||
|
|
||||||
// Activate the built-in ink tool for drawing
|
// Use signatureInk (not ink) so the annotation pen tool stays unaffected
|
||||||
annotationApi.setActiveTool('ink');
|
annotationApi.setActiveTool('signatureInk');
|
||||||
|
|
||||||
// Set default ink tool properties (black color, 2px width)
|
|
||||||
const activeTool = annotationApi.getActiveTool();
|
const activeTool = annotationApi.getActiveTool();
|
||||||
if (activeTool && activeTool.id === 'ink') {
|
if (activeTool && activeTool.id === 'signatureInk') {
|
||||||
annotationApi.setToolDefaults('ink', {
|
annotationApi.setToolDefaults('signatureInk', {
|
||||||
color: '#000000',
|
color: '#000000',
|
||||||
thickness: 2,
|
thickness: 2,
|
||||||
lineWidth: 2,
|
lineWidth: 2,
|
||||||
@@ -338,11 +350,16 @@ export const SignatureAPIBridge = forwardRef<SignatureAPI>(function SignatureAPI
|
|||||||
width: size
|
width: size
|
||||||
});
|
});
|
||||||
|
|
||||||
// Force reactivate ink tool to ensure new settings take effect
|
// Only reactivate ink if it was already the active tool — never auto-activate it.
|
||||||
annotationApi.setActiveTool(null); // Deactivate first
|
// This prevents the ink (pen annotation) tool from being activated when
|
||||||
|
// updateDrawSettings is called on sign-tool mount.
|
||||||
|
const currentTool = annotationApi.getActiveTool();
|
||||||
|
if (currentTool?.id === 'ink') {
|
||||||
|
annotationApi.setActiveTool(null);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
annotationApi.setActiveTool('ink'); // Reactivate with new settings
|
annotationApi.setActiveTool('ink');
|
||||||
}, 50);
|
}, 50);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
activateDeleteMode: () => {
|
activateDeleteMode: () => {
|
||||||
|
|||||||
@@ -0,0 +1,289 @@
|
|||||||
|
import { useCallback, useMemo, type RefObject } from 'react';
|
||||||
|
import { PdfActionType, PdfAnnotationSubtype, PdfAnnotationReplyType, type PdfAnnotationObject } from '@embedpdf/models';
|
||||||
|
import type { TrackedAnnotation, AnnotationScope } from '@embedpdf/plugin-annotation';
|
||||||
|
import type { AnnotationObject, AnnotationPatch } from '@app/components/viewer/viewerTypes';
|
||||||
|
import type { ScrollActions } from '@app/contexts/viewer/viewerActions';
|
||||||
|
|
||||||
|
export type AnnotationType =
|
||||||
|
| 'textMarkup'
|
||||||
|
| 'ink'
|
||||||
|
| 'inkHighlighter'
|
||||||
|
| 'text'
|
||||||
|
| 'note'
|
||||||
|
| 'comment'
|
||||||
|
| 'shape'
|
||||||
|
| 'line'
|
||||||
|
| 'stamp'
|
||||||
|
| 'unknown';
|
||||||
|
|
||||||
|
export type FirstLinkTarget =
|
||||||
|
| { type: 'uri'; uri: string }
|
||||||
|
| { type: 'goto'; pageIndex: number };
|
||||||
|
|
||||||
|
interface UseAnnotationMenuHandlersParams {
|
||||||
|
annotation: TrackedAnnotation<PdfAnnotationObject> | undefined;
|
||||||
|
pageIndex: number | undefined;
|
||||||
|
documentId: string;
|
||||||
|
provides: AnnotationScope | null;
|
||||||
|
scrollActions: ScrollActions;
|
||||||
|
requestCommentFocus: (documentId: string, pageIndex: number, annotationId: string, hasContent: boolean) => void;
|
||||||
|
wrapperRef: RefObject<HTMLDivElement | null>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AnnotationMenuState {
|
||||||
|
annotationType: AnnotationType;
|
||||||
|
menuWidth: number;
|
||||||
|
obj: AnnotationObject | undefined;
|
||||||
|
annotationId: string | undefined;
|
||||||
|
firstLinkTarget: FirstLinkTarget | null;
|
||||||
|
hasCommentContent: boolean;
|
||||||
|
isInSidebar: boolean;
|
||||||
|
currentColor: string;
|
||||||
|
strokeColor: string;
|
||||||
|
fillColor: string;
|
||||||
|
backgroundColor: string;
|
||||||
|
textColor: string;
|
||||||
|
currentOpacity: number;
|
||||||
|
currentWidth: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AnnotationMenuHandlers {
|
||||||
|
onDelete: () => void;
|
||||||
|
onEdit: () => void;
|
||||||
|
onColorChange: (color: string, target: 'main' | 'stroke' | 'fill' | 'text' | 'background') => void;
|
||||||
|
onOpacityChange: (opacity: number) => void;
|
||||||
|
onWidthChange: (width: number) => void;
|
||||||
|
onPropertiesUpdate: (patch: AnnotationPatch) => void;
|
||||||
|
onGoToLink: () => void;
|
||||||
|
onAddLink: (url: string) => void;
|
||||||
|
onAddToSidebar: () => void;
|
||||||
|
onViewComment: () => void;
|
||||||
|
onCommentColorChange: (color: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAnnotationMenuHandlers({
|
||||||
|
annotation,
|
||||||
|
pageIndex,
|
||||||
|
documentId,
|
||||||
|
provides,
|
||||||
|
scrollActions,
|
||||||
|
requestCommentFocus,
|
||||||
|
wrapperRef,
|
||||||
|
}: UseAnnotationMenuHandlersParams): AnnotationMenuState & AnnotationMenuHandlers {
|
||||||
|
const obj = annotation?.object as AnnotationObject | undefined;
|
||||||
|
const annotationId = obj?.id;
|
||||||
|
|
||||||
|
const attachedLinks = useMemo(() => {
|
||||||
|
if (!annotationId || !provides?.getAttachedLinks) return [];
|
||||||
|
try {
|
||||||
|
return provides.getAttachedLinks(annotationId) ?? [];
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}, [annotationId, provides]);
|
||||||
|
|
||||||
|
const firstLinkTarget = useMemo<FirstLinkTarget | null>(() => {
|
||||||
|
const linkObj = (attachedLinks[0]?.object as {
|
||||||
|
target?: {
|
||||||
|
type: string;
|
||||||
|
action?: { type: number; uri?: string; destination?: { pageIndex: number } };
|
||||||
|
};
|
||||||
|
} | undefined);
|
||||||
|
if (!linkObj?.target || linkObj.target.type !== 'action') return null;
|
||||||
|
const act = linkObj.target.action;
|
||||||
|
if (act && act.type === PdfActionType.URI && act.uri) return { type: 'uri', uri: act.uri };
|
||||||
|
if (act && (act.type === PdfActionType.Goto || act.type === PdfActionType.RemoteGoto) && act.destination)
|
||||||
|
return { type: 'goto', pageIndex: act.destination.pageIndex };
|
||||||
|
return null;
|
||||||
|
}, [attachedLinks]);
|
||||||
|
|
||||||
|
const annotationType = useMemo((): AnnotationType => {
|
||||||
|
const type = annotation?.object?.type;
|
||||||
|
const toolId = (annotation?.object as AnnotationObject | undefined)?.customData?.toolId;
|
||||||
|
|
||||||
|
if (type !== undefined && [9, 10, 11, 12].includes(type)) return 'textMarkup';
|
||||||
|
if (type === 15) return toolId === 'inkHighlighter' ? 'inkHighlighter' : 'ink';
|
||||||
|
if (type === 1 && toolId === 'textComment') return 'comment';
|
||||||
|
if (type === 14 && (toolId === 'insertText' || toolId === 'replaceText')) return 'comment';
|
||||||
|
if (type === 3) return 'note';
|
||||||
|
if (type !== undefined && [5, 6, 7].includes(type)) return 'shape';
|
||||||
|
if (type !== undefined && [4, 8].includes(type)) return 'line';
|
||||||
|
if (type === 13) return 'stamp';
|
||||||
|
return 'unknown';
|
||||||
|
}, [annotation]);
|
||||||
|
|
||||||
|
const menuWidth = useMemo((): number => {
|
||||||
|
switch (annotationType) {
|
||||||
|
case 'stamp': return 80;
|
||||||
|
case 'inkHighlighter':
|
||||||
|
case 'comment':
|
||||||
|
case 'textMarkup':
|
||||||
|
case 'text':
|
||||||
|
case 'note': return 280;
|
||||||
|
case 'shape': return 200;
|
||||||
|
default: return 260;
|
||||||
|
}
|
||||||
|
}, [annotationType]);
|
||||||
|
|
||||||
|
const hasCommentContent = (obj?.contents ?? '').trim().length > 0;
|
||||||
|
const isInSidebar = (obj?.customData as Record<string, unknown> | undefined)?.isComment === true;
|
||||||
|
|
||||||
|
// Derived style values
|
||||||
|
const currentColor = (() => {
|
||||||
|
if (!obj) return '#000000';
|
||||||
|
const type = obj.type;
|
||||||
|
if (type === 3) return obj.textColor || obj.color || '#000000';
|
||||||
|
if (type !== undefined && [4, 5, 6, 7, 8].includes(type)) return obj.strokeColor || obj.color || '#000000';
|
||||||
|
return obj.color || obj.strokeColor || '#000000';
|
||||||
|
})();
|
||||||
|
const strokeColor = obj?.strokeColor || obj?.color || '#000000';
|
||||||
|
const fillColor = obj?.color || obj?.fillColor || '#0000ff';
|
||||||
|
const backgroundColor = obj?.backgroundColor || obj?.fillColor || obj?.color || '#ffffff';
|
||||||
|
const textColor = obj?.textColor || obj?.color || '#000000';
|
||||||
|
const currentOpacity = Math.round((obj?.opacity ?? 1) * 100);
|
||||||
|
const currentWidth = obj?.strokeWidth ?? obj?.borderWidth ?? obj?.lineWidth ?? obj?.thickness ?? 2;
|
||||||
|
|
||||||
|
// Handlers
|
||||||
|
const onDelete = useCallback(() => {
|
||||||
|
if (provides?.deleteAnnotation && annotationId && pageIndex !== undefined) {
|
||||||
|
provides.deleteAnnotation(pageIndex, annotationId);
|
||||||
|
}
|
||||||
|
}, [provides, annotationId, pageIndex]);
|
||||||
|
|
||||||
|
const onEdit = useCallback(() => {
|
||||||
|
const root = wrapperRef.current?.closest('[data-no-interaction]');
|
||||||
|
const main = root?.firstElementChild;
|
||||||
|
const hitArea = main?.lastElementChild ?? main?.firstElementChild;
|
||||||
|
if (!hitArea) return;
|
||||||
|
hitArea.dispatchEvent(new MouseEvent('dblclick', { bubbles: true, cancelable: true, view: window }));
|
||||||
|
}, [wrapperRef]);
|
||||||
|
|
||||||
|
const onColorChange = useCallback((color: string, target: 'main' | 'stroke' | 'fill' | 'text' | 'background') => {
|
||||||
|
if (!provides?.updateAnnotation || !annotationId || pageIndex === undefined) return;
|
||||||
|
const type = obj?.type;
|
||||||
|
const patch: AnnotationPatch = {};
|
||||||
|
|
||||||
|
if (target === 'stroke') {
|
||||||
|
patch.strokeColor = color;
|
||||||
|
patch.color = obj?.color || '#0000ff';
|
||||||
|
patch.strokeWidth = currentWidth;
|
||||||
|
} else if (target === 'fill') {
|
||||||
|
patch.color = color;
|
||||||
|
patch.strokeColor = obj?.strokeColor || '#000000';
|
||||||
|
patch.strokeWidth = currentWidth;
|
||||||
|
} else if (target === 'background') {
|
||||||
|
patch.backgroundColor = color;
|
||||||
|
patch.fillColor = color;
|
||||||
|
patch.color = color;
|
||||||
|
} else if (target === 'text') {
|
||||||
|
patch.textColor = color;
|
||||||
|
patch.fontColor = color;
|
||||||
|
patch.fontSize = obj?.fontSize ?? 14;
|
||||||
|
patch.fontFamily = obj?.fontFamily ?? 'Helvetica';
|
||||||
|
patch.contents = obj?.contents ?? '';
|
||||||
|
} else {
|
||||||
|
patch.color = color;
|
||||||
|
if (type !== undefined && [9, 10, 11, 12].includes(type)) {
|
||||||
|
patch.strokeColor = color;
|
||||||
|
patch.fillColor = color;
|
||||||
|
patch.opacity = obj?.opacity ?? 1;
|
||||||
|
}
|
||||||
|
if (type !== undefined && [4, 8].includes(type)) {
|
||||||
|
patch.strokeColor = color;
|
||||||
|
patch.strokeWidth = obj?.strokeWidth ?? obj?.lineWidth ?? 2;
|
||||||
|
patch.lineWidth = obj?.lineWidth ?? obj?.strokeWidth ?? 2;
|
||||||
|
}
|
||||||
|
if (type === 15) {
|
||||||
|
patch.strokeColor = color;
|
||||||
|
patch.strokeWidth = obj?.strokeWidth ?? obj?.thickness ?? 2;
|
||||||
|
patch.opacity = obj?.opacity ?? 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
provides.updateAnnotation(pageIndex, annotationId, patch);
|
||||||
|
}, [provides, annotationId, pageIndex, obj, currentWidth]);
|
||||||
|
|
||||||
|
const onOpacityChange = useCallback((opacity: number) => {
|
||||||
|
if (!provides?.updateAnnotation || !annotationId || pageIndex === undefined) return;
|
||||||
|
provides.updateAnnotation(pageIndex, annotationId, { opacity: opacity / 100 });
|
||||||
|
}, [provides, annotationId, pageIndex]);
|
||||||
|
|
||||||
|
const onWidthChange = useCallback((width: number) => {
|
||||||
|
if (!provides?.updateAnnotation || !annotationId || pageIndex === undefined) return;
|
||||||
|
provides.updateAnnotation(pageIndex, annotationId, { strokeWidth: width });
|
||||||
|
}, [provides, annotationId, pageIndex]);
|
||||||
|
|
||||||
|
const onPropertiesUpdate = useCallback((patch: Record<string, unknown>) => {
|
||||||
|
if (!provides?.updateAnnotation || !annotationId || pageIndex === undefined) return;
|
||||||
|
provides.updateAnnotation(pageIndex, annotationId, patch);
|
||||||
|
}, [provides, annotationId, pageIndex]);
|
||||||
|
|
||||||
|
const onGoToLink = useCallback(() => {
|
||||||
|
if (!firstLinkTarget) return;
|
||||||
|
if (firstLinkTarget.type === 'uri') {
|
||||||
|
window.open(firstLinkTarget.uri, '_blank', 'noopener,noreferrer');
|
||||||
|
} else {
|
||||||
|
scrollActions.scrollToPage(firstLinkTarget.pageIndex + 1);
|
||||||
|
}
|
||||||
|
}, [firstLinkTarget, scrollActions]);
|
||||||
|
|
||||||
|
const onAddLink = useCallback((url: string) => {
|
||||||
|
const uri = url.trim();
|
||||||
|
if (!uri || !provides?.createAnnotation || pageIndex === undefined || !annotationId || !obj?.rect) return;
|
||||||
|
provides.createAnnotation(pageIndex, {
|
||||||
|
type: PdfAnnotationSubtype.LINK,
|
||||||
|
id: `link-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
|
||||||
|
pageIndex,
|
||||||
|
rect: obj.rect,
|
||||||
|
target: { type: 'action', action: { type: PdfActionType.URI, uri } },
|
||||||
|
inReplyToId: annotationId,
|
||||||
|
replyType: PdfAnnotationReplyType.Group,
|
||||||
|
});
|
||||||
|
}, [provides, pageIndex, annotationId, obj?.rect]);
|
||||||
|
|
||||||
|
const onAddToSidebar = useCallback(() => {
|
||||||
|
if (!provides?.updateAnnotation || !annotationId || pageIndex === undefined) return;
|
||||||
|
const existingCustomData = (obj?.customData ?? {}) as Record<string, unknown>;
|
||||||
|
provides.updateAnnotation(pageIndex, annotationId, {
|
||||||
|
customData: { ...existingCustomData, isComment: true },
|
||||||
|
} as AnnotationPatch);
|
||||||
|
requestCommentFocus(documentId, pageIndex, annotationId, false);
|
||||||
|
}, [provides, annotationId, pageIndex, obj, documentId, requestCommentFocus]);
|
||||||
|
|
||||||
|
const onViewComment = useCallback(() => {
|
||||||
|
requestCommentFocus(documentId, pageIndex ?? 0, annotationId ?? '', hasCommentContent);
|
||||||
|
}, [requestCommentFocus, documentId, pageIndex, annotationId, hasCommentContent]);
|
||||||
|
|
||||||
|
const onCommentColorChange = useCallback((color: string) => {
|
||||||
|
if (!provides?.updateAnnotation || !annotationId || pageIndex === undefined) return;
|
||||||
|
provides.updateAnnotation(pageIndex, annotationId, { strokeColor: color, color });
|
||||||
|
}, [provides, annotationId, pageIndex]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
annotationType,
|
||||||
|
menuWidth,
|
||||||
|
obj,
|
||||||
|
annotationId,
|
||||||
|
firstLinkTarget,
|
||||||
|
hasCommentContent,
|
||||||
|
isInSidebar,
|
||||||
|
currentColor,
|
||||||
|
strokeColor,
|
||||||
|
fillColor,
|
||||||
|
backgroundColor,
|
||||||
|
textColor,
|
||||||
|
currentOpacity,
|
||||||
|
currentWidth,
|
||||||
|
onDelete,
|
||||||
|
onEdit,
|
||||||
|
onColorChange,
|
||||||
|
onOpacityChange,
|
||||||
|
onWidthChange,
|
||||||
|
onPropertiesUpdate,
|
||||||
|
onGoToLink,
|
||||||
|
onAddLink,
|
||||||
|
onAddToSidebar,
|
||||||
|
onViewComment,
|
||||||
|
onCommentColorChange,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -26,7 +26,7 @@ export function useViewerRightRailButtons(
|
|||||||
) {
|
) {
|
||||||
const { t, i18n } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
const viewer = useViewer();
|
const viewer = useViewer();
|
||||||
const { isThumbnailSidebarVisible, isBookmarkSidebarVisible, isAttachmentSidebarVisible, isSearchInterfaceVisible, registerImmediatePanUpdate } = viewer;
|
const { isThumbnailSidebarVisible, isBookmarkSidebarVisible, isAttachmentSidebarVisible, isCommentsSidebarVisible, toggleCommentsSidebar, isSearchInterfaceVisible, registerImmediatePanUpdate } = viewer;
|
||||||
const [isPanning, setIsPanning] = useState<boolean>(() => viewer.getPanState()?.isPanning ?? false);
|
const [isPanning, setIsPanning] = useState<boolean>(() => viewer.getPanState()?.isPanning ?? false);
|
||||||
const { sidebarRefs } = useSidebarContext();
|
const { sidebarRefs } = useSidebarContext();
|
||||||
const { position: tooltipPosition } = useRightRailTooltipSide(sidebarRefs, 12);
|
const { position: tooltipPosition } = useRightRailTooltipSide(sidebarRefs, 12);
|
||||||
@@ -63,10 +63,8 @@ export function useViewerRightRailButtons(
|
|||||||
if (selectedTool === 'annotate') {
|
if (selectedTool === 'annotate') {
|
||||||
setIsAnnotationsActive(true);
|
setIsAnnotationsActive(true);
|
||||||
} else if (selectedTool) {
|
} else if (selectedTool) {
|
||||||
// Any other tool is active — annotate button should not be highlighted
|
|
||||||
setIsAnnotationsActive(false);
|
setIsAnnotationsActive(false);
|
||||||
} else {
|
} else {
|
||||||
// No tool selected — fall back to URL path check
|
|
||||||
setIsAnnotationsActive(isAnnotationsPath());
|
setIsAnnotationsActive(isAnnotationsPath());
|
||||||
}
|
}
|
||||||
}, [selectedTool, isAnnotationsPath]);
|
}, [selectedTool, isAnnotationsPath]);
|
||||||
@@ -85,6 +83,7 @@ export function useViewerRightRailButtons(
|
|||||||
const sidebarLabel = t('rightRail.toggleSidebar', 'Toggle Sidebar');
|
const sidebarLabel = t('rightRail.toggleSidebar', 'Toggle Sidebar');
|
||||||
const bookmarkLabel = t('rightRail.toggleBookmarks', 'Toggle Bookmarks');
|
const bookmarkLabel = t('rightRail.toggleBookmarks', 'Toggle Bookmarks');
|
||||||
const attachmentLabel = t('rightRail.toggleAttachments', 'Toggle Attachments');
|
const attachmentLabel = t('rightRail.toggleAttachments', 'Toggle Attachments');
|
||||||
|
const commentsLabel = t('rightRail.toggleComments', 'Comments');
|
||||||
const printLabel = t('rightRail.print', 'Print PDF');
|
const printLabel = t('rightRail.print', 'Print PDF');
|
||||||
const annotationsLabel = t('rightRail.annotations', 'Annotations');
|
const annotationsLabel = t('rightRail.annotations', 'Annotations');
|
||||||
const formFillLabel = t('rightRail.formFill', 'Fill Form');
|
const formFillLabel = t('rightRail.formFill', 'Fill Form');
|
||||||
@@ -237,13 +236,25 @@ export function useViewerRightRailButtons(
|
|||||||
viewer.toggleAttachmentSidebar();
|
viewer.toggleAttachmentSidebar();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'viewer-toggle-comments',
|
||||||
|
icon: <LocalIcon icon="comment" width="1.5rem" height="1.5rem" />,
|
||||||
|
tooltip: commentsLabel,
|
||||||
|
ariaLabel: commentsLabel,
|
||||||
|
section: 'top' as const,
|
||||||
|
order: 56.5,
|
||||||
|
active: isCommentsSidebarVisible,
|
||||||
|
onClick: () => {
|
||||||
|
toggleCommentsSidebar();
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'viewer-print',
|
id: 'viewer-print',
|
||||||
icon: <LocalIcon icon="print" width="1.5rem" height="1.5rem" />,
|
icon: <LocalIcon icon="print" width="1.5rem" height="1.5rem" />,
|
||||||
tooltip: printLabel,
|
tooltip: printLabel,
|
||||||
ariaLabel: printLabel,
|
ariaLabel: printLabel,
|
||||||
section: 'top' as const,
|
section: 'top' as const,
|
||||||
order: 56,
|
order: 57,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
viewer.printActions.print();
|
viewer.printActions.print();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,6 +71,9 @@ export type AnnotationToolId =
|
|||||||
| 'inkHighlighter'
|
| 'inkHighlighter'
|
||||||
| 'text'
|
| 'text'
|
||||||
| 'note'
|
| 'note'
|
||||||
|
| 'textComment'
|
||||||
|
| 'insertText'
|
||||||
|
| 'replaceText'
|
||||||
| 'square'
|
| 'square'
|
||||||
| 'circle'
|
| 'circle'
|
||||||
| 'line'
|
| 'line'
|
||||||
@@ -84,7 +87,6 @@ export type AnnotationToolId =
|
|||||||
// Import for internal use within this file, and re-export for external consumers
|
// Import for internal use within this file, and re-export for external consumers
|
||||||
import type { AnnotationEvent } from '@embedpdf/plugin-annotation';
|
import type { AnnotationEvent } from '@embedpdf/plugin-annotation';
|
||||||
export type { AnnotationEvent } from '@embedpdf/plugin-annotation';
|
export type { AnnotationEvent } from '@embedpdf/plugin-annotation';
|
||||||
export type { PdfAnnotationObject } from '@embedpdf/models';
|
|
||||||
|
|
||||||
export type AnnotationPatch = Record<string, unknown>;
|
export type AnnotationPatch = Record<string, unknown>;
|
||||||
|
|
||||||
@@ -142,4 +144,5 @@ export interface AnnotationToolOptions {
|
|||||||
icon?: 'Comment' | 'Key' | 'Note' | 'Help' | 'NewParagraph' | 'Paragraph' | 'Insert';
|
icon?: 'Comment' | 'Key' | 'Note' | 'Help' | 'NewParagraph' | 'Paragraph' | 'Insert';
|
||||||
contents?: string;
|
contents?: string;
|
||||||
customData?: Record<string, unknown>;
|
customData?: Record<string, unknown>;
|
||||||
|
flags?: ('invisible' | 'hidden' | 'print' | 'noZoom' | 'noRotate' | 'noView' | 'readOnly' | 'locked' | 'toggleNoView')[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { createContext, useContext, type ReactNode } from 'react';
|
||||||
|
|
||||||
|
export interface CommentAuthorValue {
|
||||||
|
displayName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultValue: CommentAuthorValue = { displayName: 'Guest' };
|
||||||
|
|
||||||
|
const CommentAuthorContext = createContext<CommentAuthorValue>(defaultValue);
|
||||||
|
|
||||||
|
export function CommentAuthorProvider({
|
||||||
|
children,
|
||||||
|
displayName = 'Guest',
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
displayName?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<CommentAuthorContext.Provider value={{ displayName }}>
|
||||||
|
{children}
|
||||||
|
</CommentAuthorContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCommentAuthor(): CommentAuthorValue {
|
||||||
|
return useContext(CommentAuthorContext);
|
||||||
|
}
|
||||||
@@ -85,6 +85,19 @@ export interface ViewerContextType {
|
|||||||
toggleBookmarkSidebar: () => void;
|
toggleBookmarkSidebar: () => void;
|
||||||
isAttachmentSidebarVisible: boolean;
|
isAttachmentSidebarVisible: boolean;
|
||||||
toggleAttachmentSidebar: () => void;
|
toggleAttachmentSidebar: () => void;
|
||||||
|
isCommentsSidebarVisible: boolean;
|
||||||
|
setCommentsSidebarVisible: (visible: boolean) => void;
|
||||||
|
toggleCommentsSidebar: () => void;
|
||||||
|
|
||||||
|
/** Request focus or highlight of a comment card in the sidebar (opens sidebar, then scrolls + flashes or focuses input). */
|
||||||
|
highlightCommentRequest: {
|
||||||
|
documentId: string;
|
||||||
|
pageIndex: number;
|
||||||
|
annotationId: string;
|
||||||
|
action: 'focus' | 'highlight';
|
||||||
|
} | null;
|
||||||
|
requestCommentFocus: (documentId: string, pageIndex: number, annotationId: string, hasContent: boolean) => void;
|
||||||
|
clearHighlightCommentRequest: () => void;
|
||||||
|
|
||||||
// Search interface visibility
|
// Search interface visibility
|
||||||
isSearchInterfaceVisible: boolean;
|
isSearchInterfaceVisible: boolean;
|
||||||
@@ -170,6 +183,13 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
|
|||||||
const [isThumbnailSidebarVisible, setIsThumbnailSidebarVisible] = useState(false);
|
const [isThumbnailSidebarVisible, setIsThumbnailSidebarVisible] = useState(false);
|
||||||
const [isBookmarkSidebarVisible, setIsBookmarkSidebarVisible] = useState(false);
|
const [isBookmarkSidebarVisible, setIsBookmarkSidebarVisible] = useState(false);
|
||||||
const [isAttachmentSidebarVisible, setIsAttachmentSidebarVisible] = useState(false);
|
const [isAttachmentSidebarVisible, setIsAttachmentSidebarVisible] = useState(false);
|
||||||
|
const [isCommentsSidebarVisible, setIsCommentsSidebarVisible] = useState(false);
|
||||||
|
const [highlightCommentRequest, setHighlightCommentRequest] = useState<{
|
||||||
|
documentId: string;
|
||||||
|
pageIndex: number;
|
||||||
|
annotationId: string;
|
||||||
|
action: 'focus' | 'highlight';
|
||||||
|
} | null>(null);
|
||||||
const [isSearchInterfaceVisible, setSearchInterfaceVisible] = useState(false);
|
const [isSearchInterfaceVisible, setSearchInterfaceVisible] = useState(false);
|
||||||
const [isAnnotationsVisible, setIsAnnotationsVisible] = useState(true);
|
const [isAnnotationsVisible, setIsAnnotationsVisible] = useState(true);
|
||||||
const [isAnnotationMode, setIsAnnotationModeState] = useState(false);
|
const [isAnnotationMode, setIsAnnotationModeState] = useState(false);
|
||||||
@@ -261,6 +281,31 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
|
|||||||
setIsAttachmentSidebarVisible(prev => !prev);
|
setIsAttachmentSidebarVisible(prev => !prev);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setCommentsSidebarVisible = (visible: boolean) => {
|
||||||
|
setIsCommentsSidebarVisible(visible);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleCommentsSidebar = () => {
|
||||||
|
setIsCommentsSidebarVisible(prev => !prev);
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestCommentFocus = useCallback(
|
||||||
|
(documentId: string, pageIndex: number, annotationId: string, hasContent: boolean) => {
|
||||||
|
setIsCommentsSidebarVisible(true);
|
||||||
|
setHighlightCommentRequest({
|
||||||
|
documentId,
|
||||||
|
pageIndex,
|
||||||
|
annotationId,
|
||||||
|
action: hasContent ? 'highlight' : 'focus',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const clearHighlightCommentRequest = useCallback(() => {
|
||||||
|
setHighlightCommentRequest(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const searchInterfaceActions = {
|
const searchInterfaceActions = {
|
||||||
open: () => setSearchInterfaceVisible(true),
|
open: () => setSearchInterfaceVisible(true),
|
||||||
close: () => setSearchInterfaceVisible(false),
|
close: () => setSearchInterfaceVisible(false),
|
||||||
@@ -392,6 +437,12 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
|
|||||||
toggleBookmarkSidebar,
|
toggleBookmarkSidebar,
|
||||||
isAttachmentSidebarVisible,
|
isAttachmentSidebarVisible,
|
||||||
toggleAttachmentSidebar,
|
toggleAttachmentSidebar,
|
||||||
|
isCommentsSidebarVisible,
|
||||||
|
setCommentsSidebarVisible,
|
||||||
|
toggleCommentsSidebar,
|
||||||
|
highlightCommentRequest,
|
||||||
|
requestCommentFocus,
|
||||||
|
clearHighlightCommentRequest,
|
||||||
|
|
||||||
// Search interface
|
// Search interface
|
||||||
isSearchInterfaceVisible,
|
isSearchInterfaceVisible,
|
||||||
|
|||||||
@@ -671,6 +671,31 @@
|
|||||||
box-shadow: 0 0 0 0.5px var(--pdf-selection-ring);
|
box-shadow: 0 0 0 0.5px var(--pdf-selection-ring);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Flash highlight for comment card (e.g. "View comment" from annotation menu) */
|
||||||
|
@keyframes comment-card-flash {
|
||||||
|
0% {
|
||||||
|
background-color: rgba(255, 235, 59, 0);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
20% {
|
||||||
|
background-color: rgba(255, 235, 59, 0.35);
|
||||||
|
box-shadow: 0 0 20px rgba(255, 235, 59, 0.5);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-color: rgba(255, 235, 59, 0.25);
|
||||||
|
box-shadow: 0 0 15px rgba(255, 235, 59, 0.4);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-color: rgba(255, 235, 59, 0);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-card-flash-highlight {
|
||||||
|
animation: comment-card-flash 1.5s ease-out;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Smooth transitions for theme switching */
|
/* Smooth transitions for theme switching */
|
||||||
* {
|
* {
|
||||||
transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease;
|
transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease;
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ const DRAWING_TOOLS: AnnotationToolId[] = [
|
|||||||
'inkHighlighter',
|
'inkHighlighter',
|
||||||
'text',
|
'text',
|
||||||
'note',
|
'note',
|
||||||
|
'textComment',
|
||||||
|
'insertText',
|
||||||
|
'replaceText',
|
||||||
'square',
|
'square',
|
||||||
'circle',
|
'circle',
|
||||||
'line',
|
'line',
|
||||||
@@ -43,6 +46,9 @@ const KNOWN_ANNOTATION_TOOLS: AnnotationToolId[] = [
|
|||||||
'inkHighlighter',
|
'inkHighlighter',
|
||||||
'text',
|
'text',
|
||||||
'note',
|
'note',
|
||||||
|
'textComment',
|
||||||
|
'insertText',
|
||||||
|
'replaceText',
|
||||||
'square',
|
'square',
|
||||||
'circle',
|
'circle',
|
||||||
'line',
|
'line',
|
||||||
@@ -75,7 +81,7 @@ const Annotate = (_props: BaseToolProps) => {
|
|||||||
const viewerContext = useContext(ViewerContext);
|
const viewerContext = useContext(ViewerContext);
|
||||||
const viewerContextRef = useRef(viewerContext);
|
const viewerContextRef = useRef(viewerContext);
|
||||||
useEffect(() => { viewerContextRef.current = viewerContext; }, [viewerContext]);
|
useEffect(() => { viewerContextRef.current = viewerContext; }, [viewerContext]);
|
||||||
const { getZoomState, registerImmediateZoomUpdate, applyChanges, activeFileIndex, panActions } = useViewer();
|
const { getZoomState, registerImmediateZoomUpdate, applyChanges, activeFileIndex, panActions, setCommentsSidebarVisible } = useViewer();
|
||||||
|
|
||||||
const [activeTool, setActiveTool] = useState<AnnotationToolId>('select');
|
const [activeTool, setActiveTool] = useState<AnnotationToolId>('select');
|
||||||
|
|
||||||
@@ -312,6 +318,11 @@ const Annotate = (_props: BaseToolProps) => {
|
|||||||
annotationApiRef?.current?.setAnnotationStyle?.(toolId, options);
|
annotationApiRef?.current?.setAnnotationStyle?.(toolId, options);
|
||||||
annotationApiRef?.current?.activateAnnotationTool?.(toolId === 'stamp' ? 'stamp' : toolId, options);
|
annotationApiRef?.current?.activateAnnotationTool?.(toolId === 'stamp' ? 'stamp' : toolId, options);
|
||||||
|
|
||||||
|
// Auto-open comments sidebar when a comment tool is selected
|
||||||
|
if (['textComment', 'insertText', 'replaceText'].includes(toolId)) {
|
||||||
|
setCommentsSidebarVisible(true);
|
||||||
|
}
|
||||||
|
|
||||||
// Reset flag after a short delay
|
// Reset flag after a short delay
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
manualToolSwitch.current = false;
|
manualToolSwitch.current = false;
|
||||||
@@ -346,7 +357,7 @@ const Annotate = (_props: BaseToolProps) => {
|
|||||||
const unsubscribe = annotationApiRef?.current?.onAnnotationEvent?.((event: AnnotationEvent) => {
|
const unsubscribe = annotationApiRef?.current?.onAnnotationEvent?.((event: AnnotationEvent) => {
|
||||||
if (event.type === 'create' && event.committed) {
|
if (event.type === 'create' && event.committed) {
|
||||||
const toolId = activeToolRef.current;
|
const toolId = activeToolRef.current;
|
||||||
if (toolId === 'text' || toolId === 'note') {
|
if (['text', 'note', 'textComment', 'insertText', 'replaceText'].includes(toolId)) {
|
||||||
setActiveTool('select');
|
setActiveTool('select');
|
||||||
activeToolRef.current = 'select';
|
activeToolRef.current = 'select';
|
||||||
annotationApiRef?.current?.activateAnnotationTool?.('select');
|
annotationApiRef?.current?.activateAnnotationTool?.('select');
|
||||||
|
|||||||
@@ -209,6 +209,12 @@ export function AnnotationPanel(props: AnnotationPanelProps) {
|
|||||||
{ id: 'polygon', label: t('annotation.polygon', 'Polygon'), icon: 'change-history' },
|
{ id: 'polygon', label: t('annotation.polygon', 'Polygon'), icon: 'change-history' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const commentTools: { id: AnnotationToolId; label: string; icon: string }[] = [
|
||||||
|
{ id: 'textComment', label: t('annotation.comment', 'Comment'), icon: 'comment' },
|
||||||
|
{ id: 'insertText', label: t('annotation.insertText', 'Insert Text'), icon: 'add-comment' },
|
||||||
|
{ id: 'replaceText', label: t('annotation.replaceText', 'Replace Text'), icon: 'find-replace' },
|
||||||
|
];
|
||||||
|
|
||||||
const otherTools: { id: AnnotationToolId; label: string; icon: string }[] = [
|
const otherTools: { id: AnnotationToolId; label: string; icon: string }[] = [
|
||||||
{ id: 'text', label: t('annotation.text', 'Text box'), icon: 'text-fields' },
|
{ id: 'text', label: t('annotation.text', 'Text box'), icon: 'text-fields' },
|
||||||
{ id: 'note', label: t('annotation.note', 'Note'), icon: 'sticky-note-2' },
|
{ id: 'note', label: t('annotation.note', 'Note'), icon: 'sticky-note-2' },
|
||||||
@@ -778,6 +784,11 @@ export function AnnotationPanel(props: AnnotationPanelProps) {
|
|||||||
{renderToolButtons(shapeTools)}
|
{renderToolButtons(shapeTools)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text size="sm" fw={600} mb="xs">{t('annotation.comments', 'Comments')}</Text>
|
||||||
|
{renderToolButtons(commentTools)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text size="sm" fw={600} mb="xs">{t('annotation.notesStamps', 'Notes & Stamps')}</Text>
|
<Text size="sm" fw={600} mb="xs">{t('annotation.notesStamps', 'Notes & Stamps')}</Text>
|
||||||
{renderToolButtons(otherTools)}
|
{renderToolButtons(otherTools)}
|
||||||
|
|||||||
Reference in New Issue
Block a user