mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-14 10:34:06 +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",
|
||||
"@cantoo/pdf-lib": "^2.5.3",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@embedpdf/core": "^2.7.0",
|
||||
"@embedpdf/engines": "^2.7.0",
|
||||
"@embedpdf/models": "^2.7.0",
|
||||
"@embedpdf/plugin-annotation": "^2.7.0",
|
||||
"@embedpdf/plugin-attachment": "^2.7.0",
|
||||
"@embedpdf/plugin-bookmark": "^2.7.0",
|
||||
"@embedpdf/plugin-document-manager": "^2.7.0",
|
||||
"@embedpdf/plugin-export": "^2.7.0",
|
||||
"@embedpdf/plugin-history": "^2.7.0",
|
||||
"@embedpdf/plugin-interaction-manager": "^2.7.0",
|
||||
"@embedpdf/plugin-pan": "^2.7.0",
|
||||
"@embedpdf/plugin-print": "^2.7.0",
|
||||
"@embedpdf/plugin-redaction": "^2.7.0",
|
||||
"@embedpdf/plugin-render": "^2.7.0",
|
||||
"@embedpdf/plugin-rotate": "^2.7.0",
|
||||
"@embedpdf/plugin-scroll": "^2.7.0",
|
||||
"@embedpdf/plugin-search": "^2.7.0",
|
||||
"@embedpdf/plugin-selection": "^2.7.0",
|
||||
"@embedpdf/plugin-spread": "^2.7.0",
|
||||
"@embedpdf/plugin-thumbnail": "^2.7.0",
|
||||
"@embedpdf/plugin-tiling": "^2.7.0",
|
||||
"@embedpdf/plugin-viewport": "^2.7.0",
|
||||
"@embedpdf/plugin-zoom": "^2.7.0",
|
||||
"@embedpdf/core": "^2.8.0",
|
||||
"@embedpdf/engines": "^2.8.0",
|
||||
"@embedpdf/models": "^2.8.0",
|
||||
"@embedpdf/plugin-annotation": "^2.8.0",
|
||||
"@embedpdf/plugin-attachment": "^2.8.0",
|
||||
"@embedpdf/plugin-bookmark": "^2.8.0",
|
||||
"@embedpdf/plugin-document-manager": "^2.8.0",
|
||||
"@embedpdf/plugin-export": "^2.8.0",
|
||||
"@embedpdf/plugin-history": "^2.8.0",
|
||||
"@embedpdf/plugin-interaction-manager": "^2.8.0",
|
||||
"@embedpdf/plugin-pan": "^2.8.0",
|
||||
"@embedpdf/plugin-print": "^2.8.0",
|
||||
"@embedpdf/plugin-redaction": "^2.8.0",
|
||||
"@embedpdf/plugin-render": "^2.8.0",
|
||||
"@embedpdf/plugin-rotate": "^2.8.0",
|
||||
"@embedpdf/plugin-scroll": "^2.8.0",
|
||||
"@embedpdf/plugin-search": "^2.8.0",
|
||||
"@embedpdf/plugin-selection": "^2.8.0",
|
||||
"@embedpdf/plugin-spread": "^2.8.0",
|
||||
"@embedpdf/plugin-thumbnail": "^2.8.0",
|
||||
"@embedpdf/plugin-tiling": "^2.8.0",
|
||||
"@embedpdf/plugin-viewport": "^2.8.0",
|
||||
"@embedpdf/plugin-zoom": "^2.8.0",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@iconify/react": "^6.0.2",
|
||||
@@ -576,14 +576,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/core": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/core/-/core-2.7.0.tgz",
|
||||
"integrity": "sha512-dJ9pCWXVJxh6uSJP4sKuJP4v67+6Vlmw4WqJcv+CKJSUPdX9LhOSTIgTjwKLZ5zXo0c3DihNvrUupovb/DrhgQ==",
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/core/-/core-2.8.0.tgz",
|
||||
"integrity": "sha512-ui0HR4fl7ndiGPw40kMBxXCO9gZHctV1u3Q+/XTd34ONYJ+Pa2LoWNVW2IuPDK7PgKzABPT2axntowlVLPP10g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@embedpdf/engines": "2.7.0",
|
||||
"@embedpdf/models": "2.7.0"
|
||||
"@embedpdf/engines": "2.8.0",
|
||||
"@embedpdf/models": "2.8.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"preact": "^10.26.4",
|
||||
@@ -594,9 +594,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/engines": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/engines/-/engines-2.7.0.tgz",
|
||||
"integrity": "sha512-9BW/0f1EhMtklOXzpq+TtAELpXrvWcgCioFX9tSvPXBwWw/xva9c3UFx8NKehfrAkmcLGNz2c2Kqe8hDlKp9Rw==",
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/engines/-/engines-2.8.0.tgz",
|
||||
"integrity": "sha512-s749nppKxOcgvFraySKrwtiCt2VMXFe8TFuZUV5R7z8TtMagt6o5NOk6VsdvIpggUYxIsiKhLkFvAqvkNgcjng==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/fonts-arabic": "1.0.0",
|
||||
@@ -606,8 +606,8 @@
|
||||
"@embedpdf/fonts-latin": "1.0.0",
|
||||
"@embedpdf/fonts-sc": "1.0.0",
|
||||
"@embedpdf/fonts-tc": "1.0.0",
|
||||
"@embedpdf/models": "2.7.0",
|
||||
"@embedpdf/pdfium": "2.7.0"
|
||||
"@embedpdf/models": "2.8.0",
|
||||
"@embedpdf/pdfium": "2.8.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"preact": "^10.26.4",
|
||||
@@ -660,32 +660,32 @@
|
||||
"license": "OFL-1.1"
|
||||
},
|
||||
"node_modules/@embedpdf/models": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/models/-/models-2.7.0.tgz",
|
||||
"integrity": "sha512-SRtDqYUn6Qw677ECIyipg0qqZR0+iLUU1mGI/X70ad4/qhw+TAOF5/aWddoFdPAw5XviEYVw5zR0oTz+MLBUbw==",
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/models/-/models-2.8.0.tgz",
|
||||
"integrity": "sha512-kk3Fm8exMmEX9Ce7VQePybmo04NQGdpsO3FsX1YOQqHpLVBk7tiTeOdetjBqI+YhQ2zWLa2naNKSOSGGzYLyxA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@embedpdf/pdfium": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/pdfium/-/pdfium-2.7.0.tgz",
|
||||
"integrity": "sha512-PW6JnbBERvRKVrBzeCH/iGorQ8OTTRwr8rciyv2Kstqf3IdIFHjjNWvl6exK1/3K5wbbxYnHSNpRotl0OcsFJA==",
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/pdfium/-/pdfium-2.8.0.tgz",
|
||||
"integrity": "sha512-RlNLRNboF1Y6fNDy4sJ/a/FEYxATZyeM+n25r3KZJjG+RaM6bxBWXvWlFlGBU5Vx2eqQ5AzDAmIE9cn1agFmqA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-annotation": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-annotation/-/plugin-annotation-2.7.0.tgz",
|
||||
"integrity": "sha512-bkn9+91XfcFoKgwZ0kiJLbnvWNyrorhcbX5grsvUw2gJ8mL0RtwHSnCjZVT3WOPleQVwnNkBZUHefJhNT+LEKw==",
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-annotation/-/plugin-annotation-2.8.0.tgz",
|
||||
"integrity": "sha512-h31dT0pvQjFSwsBLytL4BBLf3WDdz9kmAYNKR10filikge7MpgTzgVYFD0C6AOyy2qK1Y/vqworCyh+emVD5aA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "2.7.0",
|
||||
"@embedpdf/utils": "2.7.0"
|
||||
"@embedpdf/models": "2.8.0",
|
||||
"@embedpdf/utils": "2.8.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "2.7.0",
|
||||
"@embedpdf/plugin-history": "2.7.0",
|
||||
"@embedpdf/plugin-interaction-manager": "2.7.0",
|
||||
"@embedpdf/plugin-selection": "2.7.0",
|
||||
"@embedpdf/core": "2.8.0",
|
||||
"@embedpdf/plugin-history": "2.8.0",
|
||||
"@embedpdf/plugin-interaction-manager": "2.8.0",
|
||||
"@embedpdf/plugin-selection": "2.8.0",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -694,15 +694,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-attachment": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-attachment/-/plugin-attachment-2.7.0.tgz",
|
||||
"integrity": "sha512-APH9Y/P4KyawckS68bouQ5aGsmQurgkdBG0oRq9kirDM+yIVGonS2P+gzwu9otgNKve880S5mBImC3FndJL4Tg==",
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-attachment/-/plugin-attachment-2.8.0.tgz",
|
||||
"integrity": "sha512-g2jCwjhQsij9zz2JOxZJkIeLTAUxiBKsFh6K4hcsG45ougw/mI3WCw1f+bZlAPkZuOWPo2/nthsHb+wAlrcykQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "2.7.0"
|
||||
"@embedpdf/models": "2.8.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "2.7.0",
|
||||
"@embedpdf/core": "2.8.0",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -711,15 +711,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-bookmark": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-bookmark/-/plugin-bookmark-2.7.0.tgz",
|
||||
"integrity": "sha512-um+naqyEAhBp4uFIis6K+V0t3dY4Dgry/vW17SwK9lFyMPQNCUT8PZMb7ysvj88vBXaDbzqCbtnlV6AyBEqxEQ==",
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-bookmark/-/plugin-bookmark-2.8.0.tgz",
|
||||
"integrity": "sha512-ab49e17amEshweobU2GbtDEuRDHj89vRYiahkRq9nU1ACI16JIyh4t4fE/m/ucL9YhMMXldjwdtTGHceV566Jg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "2.7.0"
|
||||
"@embedpdf/models": "2.8.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "2.7.0",
|
||||
"@embedpdf/core": "2.8.0",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -728,15 +728,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-document-manager": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-document-manager/-/plugin-document-manager-2.7.0.tgz",
|
||||
"integrity": "sha512-eKM0MEUCPZmNkOkz5n6QWBcKN7m1c3RBZlfXB7faHBaScPyO3f+xgfipAht86UvfYwxT9BWk9ozbAtm2UPBHcQ==",
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-document-manager/-/plugin-document-manager-2.8.0.tgz",
|
||||
"integrity": "sha512-SS+IKJ2+rk4dHM3PvQQrvnfNdb5oOF1INR5r2w0MKo22C9WPD8Ncg1Jak/mCrGbSrJemW3pkIrKZhoREQtSrbA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "2.7.0"
|
||||
"@embedpdf/models": "2.8.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "2.7.0",
|
||||
"@embedpdf/core": "2.8.0",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -745,15 +745,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-export": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-export/-/plugin-export-2.7.0.tgz",
|
||||
"integrity": "sha512-wlP/ZdWPLCnJpsQWNCyZAlicvNiapbo7Dc95HMWHg2YgmkCjDe26z4C1QoubUanNSerGWJCK0qKXxJ/PVkJSnw==",
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-export/-/plugin-export-2.8.0.tgz",
|
||||
"integrity": "sha512-AvqfyhB58HLoZMKyXLLT+1ebE7wrMEnIAjr+OqEuaNnekxvb8atD8EBByzgtXLiNLStlxFn5g1CKyQThpw4Pcw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "2.7.0"
|
||||
"@embedpdf/models": "2.8.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "2.7.0",
|
||||
"@embedpdf/core": "2.8.0",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -762,16 +762,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-history": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-history/-/plugin-history-2.7.0.tgz",
|
||||
"integrity": "sha512-k2c5M2Nwey+j6EtZyf5Sw+cuyNT4MTWdqPQGR9NbmZUiNnkSrg/TmPWZCyaXZKGRjLmoJocc0qRdyaZnaaTpqw==",
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-history/-/plugin-history-2.8.0.tgz",
|
||||
"integrity": "sha512-S6TO7DqMqVtBYsztgvPvq8BOJTMl8rWdGjVMuoxD93HZdSgogoculwCATrJGor/BC+X6Vmtaqg6NJWSdIAeBEQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "2.7.0"
|
||||
"@embedpdf/models": "2.8.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "2.7.0",
|
||||
"@embedpdf/core": "2.8.0",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -780,16 +780,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-interaction-manager": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-interaction-manager/-/plugin-interaction-manager-2.7.0.tgz",
|
||||
"integrity": "sha512-IfJiugr2k9VD8/gryMb22wC5yk4EQwSS3/Co0EdOhzkmkGSgUHA/KcG83WBu8wPEtj5Gwy473xe41fYKsGMlsA==",
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-interaction-manager/-/plugin-interaction-manager-2.8.0.tgz",
|
||||
"integrity": "sha512-xdRTAp1YiXWm+3WVqIN8dkRT3I/dHTumLJy5Kvt7lc1W2XM3M5bfCk5eTTcjY1DPv2buyt44i/4XNWoXAgBXDg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "2.7.0"
|
||||
"@embedpdf/models": "2.8.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "2.7.0",
|
||||
"@embedpdf/core": "2.8.0",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -798,17 +798,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-pan": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-pan/-/plugin-pan-2.7.0.tgz",
|
||||
"integrity": "sha512-/jUyM4K42gd7eoVsKFDSfN4t8CHIf6ZX7kU4BHBBFvOcNB7bToOvibPiznC4oPrHmOuzcs4e+VOfu+yHKn6DrA==",
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-pan/-/plugin-pan-2.8.0.tgz",
|
||||
"integrity": "sha512-EFqTEHk9E7AMRwguRO24jRl0J/5+pG07wlAm5U2rB0LDk30oAqVklFuDygaALdi8ZRBdkWOfD3Wl3gurQvfwAA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "2.7.0"
|
||||
"@embedpdf/models": "2.8.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "2.7.0",
|
||||
"@embedpdf/plugin-interaction-manager": "2.7.0",
|
||||
"@embedpdf/plugin-viewport": "2.7.0",
|
||||
"@embedpdf/core": "2.8.0",
|
||||
"@embedpdf/plugin-interaction-manager": "2.8.0",
|
||||
"@embedpdf/plugin-viewport": "2.8.0",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -817,15 +817,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-print": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-print/-/plugin-print-2.7.0.tgz",
|
||||
"integrity": "sha512-sQqRSCN+r59EjKibQ3yMt1lKyHMxO1UqhZ6iI+w5hUBm9yTIHvkQRTVbawbkCfty8QNldjMQEwN3iYkf8LNHsg==",
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-print/-/plugin-print-2.8.0.tgz",
|
||||
"integrity": "sha512-PUdw2/1GwbewYrxVgsb+3lkYyYYcLU5qpazrBZjjx0v+SEpb2SMQOOPNUILeJGKbYUlxSz3qv/loFwqzALkUQA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "2.7.0"
|
||||
"@embedpdf/models": "2.8.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "2.7.0",
|
||||
"@embedpdf/core": "2.8.0",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=18.0.0",
|
||||
"react-dom": ">=18.0.0",
|
||||
@@ -834,20 +834,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-redaction": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-redaction/-/plugin-redaction-2.7.0.tgz",
|
||||
"integrity": "sha512-N6LD9k35ZCkpHPDW5I6pW6g/YTDXGuNp/kWTHU4bcRGfTwinu6i0PaFJQKus1t1pkVprhmjJgCXbkM2xmIKXAg==",
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-redaction/-/plugin-redaction-2.8.0.tgz",
|
||||
"integrity": "sha512-pIDFNd9rX7cwrhY6rFCBa5MTnGdLZHX7magToqna/Ffs1KEr0CfNF8jIXG0/E6KB6SsppbHwC7AkwRVWz8NoHg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "2.7.0",
|
||||
"@embedpdf/utils": "2.7.0"
|
||||
"@embedpdf/models": "2.8.0",
|
||||
"@embedpdf/utils": "2.8.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "2.7.0",
|
||||
"@embedpdf/plugin-annotation": "2.7.0",
|
||||
"@embedpdf/plugin-history": "2.7.0",
|
||||
"@embedpdf/plugin-interaction-manager": "2.7.0",
|
||||
"@embedpdf/plugin-selection": "2.7.0",
|
||||
"@embedpdf/core": "2.8.0",
|
||||
"@embedpdf/plugin-annotation": "2.8.0",
|
||||
"@embedpdf/plugin-history": "2.8.0",
|
||||
"@embedpdf/plugin-interaction-manager": "2.8.0",
|
||||
"@embedpdf/plugin-selection": "2.8.0",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -856,16 +856,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-render": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-render/-/plugin-render-2.7.0.tgz",
|
||||
"integrity": "sha512-qJcBxl2Kgqbw/Yb4qiX3qNsBKmzI8ijAKix3PoYL+nlhX6iFbcCkUbJgF0rPCBbhIy59Bpp7eJL+RyY5DhQvUg==",
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-render/-/plugin-render-2.8.0.tgz",
|
||||
"integrity": "sha512-jVGSuyg366LmFzbDpqszLbu3G6VOfv1u46D1C0ph6pL3jisTlRswRosaXS4eVE/fAYTryBrI0olCpVYYct4bQw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "2.7.0"
|
||||
"@embedpdf/models": "2.8.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "2.7.0",
|
||||
"@embedpdf/core": "2.8.0",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -874,15 +874,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-rotate": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-rotate/-/plugin-rotate-2.7.0.tgz",
|
||||
"integrity": "sha512-ggqhQzpJB7ydfx9fDcuUrlGS9wxfsLG+rQwf1MMmUMMdEK1NPsHw5abSuQBerAm1vrpZ3Dyz0v2TxPVejvIhjA==",
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-rotate/-/plugin-rotate-2.8.0.tgz",
|
||||
"integrity": "sha512-lWATWEwhkBW77dJaKXlSJbqEMLTSy1lOhn0kDAZ5eyPZRo+7UWe7LHuT5HWzzRlbY9rpvnyixCT6Zp9rKT+WnQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "2.7.0"
|
||||
"@embedpdf/models": "2.8.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "2.7.0",
|
||||
"@embedpdf/core": "2.8.0",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -891,17 +891,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-scroll": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-scroll/-/plugin-scroll-2.7.0.tgz",
|
||||
"integrity": "sha512-uEs5XdHZ3XqH3OWnM8E2eHen+7+MxK9SrlBnfM8ZcPkzHR9n7hT/Aqy05BiLKxKem585HvtbRgFzNQZqmNIGHw==",
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-scroll/-/plugin-scroll-2.8.0.tgz",
|
||||
"integrity": "sha512-l6hFH6lsAI+07ZGuOwbC8qcRNdYzWSIfRszjt9UmhKZbvXLEp6YJeS/XOUC/37Kqi30tsmLDyZhMW1AooGAr1A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "2.7.0"
|
||||
"@embedpdf/models": "2.8.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "2.7.0",
|
||||
"@embedpdf/plugin-viewport": "2.7.0",
|
||||
"@embedpdf/core": "2.8.0",
|
||||
"@embedpdf/plugin-viewport": "2.8.0",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -910,15 +910,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-search": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-search/-/plugin-search-2.7.0.tgz",
|
||||
"integrity": "sha512-TeUMvlp6j4NXkkbPURSkg7fd6l/X8wUnN+ImFiM01VM+YBLQWTPXcP7AAyFzoI5iAh2m5+ZZEB/uGb5qm3Vamg==",
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-search/-/plugin-search-2.8.0.tgz",
|
||||
"integrity": "sha512-HZ7munHdAF2pJ1cT02yc5I0Xcr4b4CQ8GkhD4qhTpZK0yga/tQiDY2Jjstygv7XiZwHIDKvqOuQsQmOXDsEemw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "2.7.0"
|
||||
"@embedpdf/models": "2.8.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "2.7.0",
|
||||
"@embedpdf/core": "2.8.0",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -927,18 +927,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-selection": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-selection/-/plugin-selection-2.7.0.tgz",
|
||||
"integrity": "sha512-MhTUHZ5jBh1jQU66xrtkx2jEtcreCfHy4zpmMY7iCoZtWUxrML+zf4jYNrR/ZP93O4i5HNBSnJNTG+RmRJ5KPw==",
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-selection/-/plugin-selection-2.8.0.tgz",
|
||||
"integrity": "sha512-DcPyOp2WKoVYVpbZIP5t+JsEmCL9Y7bxe8PmiEBtlm1lustuUezdWyA2G4wsGCR4I/5uNlY85qBGCBhL195sSA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "2.7.0",
|
||||
"@embedpdf/utils": "2.7.0"
|
||||
"@embedpdf/models": "2.8.0",
|
||||
"@embedpdf/utils": "2.8.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "2.7.0",
|
||||
"@embedpdf/plugin-interaction-manager": "2.7.0",
|
||||
"@embedpdf/core": "2.8.0",
|
||||
"@embedpdf/plugin-interaction-manager": "2.8.0",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -947,15 +947,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-spread": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-spread/-/plugin-spread-2.7.0.tgz",
|
||||
"integrity": "sha512-FMW6nXlyZhjQKrj8l4gduu0+IWI/H8Sda9iiKKYOGLrDWcIsSo3Gt3mSJgUsjUxA2YS+l0m48VeuSESa7Ab8oA==",
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-spread/-/plugin-spread-2.8.0.tgz",
|
||||
"integrity": "sha512-0Ld5HERaG8cKyWW7ktojG3FK6angkzYnBnw7Rnqf4cQuNdO7nKSTslyxkrLzF3vvmKrB//2ghm/pRVkgci4BOw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "2.7.0"
|
||||
"@embedpdf/models": "2.8.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "2.7.0",
|
||||
"@embedpdf/core": "2.8.0",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -964,16 +964,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-thumbnail": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-thumbnail/-/plugin-thumbnail-2.7.0.tgz",
|
||||
"integrity": "sha512-ZnXt9ebtExFQUMUbWApUK8yC1eXYjDMEzS9631QpbLm0BhW9jYu9cs6U37Sww+zhYi7IFih4zb38lgAsbDgOcA==",
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-thumbnail/-/plugin-thumbnail-2.8.0.tgz",
|
||||
"integrity": "sha512-Dq/Cqsn4GClRXNWerqlXNH8WDWK/TtHxwq6yr2kqgXPo1FyREto4I7GfdO1hygTu0Dl6HK2auEVHBVC8QT53fw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "2.7.0"
|
||||
"@embedpdf/models": "2.8.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "2.7.0",
|
||||
"@embedpdf/plugin-render": "2.7.0",
|
||||
"@embedpdf/core": "2.8.0",
|
||||
"@embedpdf/plugin-render": "2.8.0",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -982,18 +982,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-tiling": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-tiling/-/plugin-tiling-2.7.0.tgz",
|
||||
"integrity": "sha512-/Cd7dR0kA5/Y1gykYLYS3gecStycc4m5Jjm+a0mFzO/7yQ6BLb0g4zkM95YloBAsFoP5GPXKKhBw3pi32pTObw==",
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-tiling/-/plugin-tiling-2.8.0.tgz",
|
||||
"integrity": "sha512-gPe5mG6hyyruki1eSuQYD2KDbo0Z0TxzSk8TcHIdEYYF6NlI2OghuO8Vczz5WLU8clX8xzn8c/c+tQMHEXfbZA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "2.7.0"
|
||||
"@embedpdf/models": "2.8.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "2.7.0",
|
||||
"@embedpdf/plugin-render": "2.7.0",
|
||||
"@embedpdf/plugin-scroll": "2.7.0",
|
||||
"@embedpdf/plugin-viewport": "2.7.0",
|
||||
"@embedpdf/core": "2.8.0",
|
||||
"@embedpdf/plugin-render": "2.8.0",
|
||||
"@embedpdf/plugin-scroll": "2.8.0",
|
||||
"@embedpdf/plugin-viewport": "2.8.0",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -1002,16 +1002,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-viewport": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-viewport/-/plugin-viewport-2.7.0.tgz",
|
||||
"integrity": "sha512-nVDoU86CgHme7f2r7OFhziqHz1Yg8vdE2K+bXzRCmtZaubH+qVo5C+77at9brYCSinA67znQn1yy6zbfxs4FwA==",
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-viewport/-/plugin-viewport-2.8.0.tgz",
|
||||
"integrity": "sha512-E16hc4yPA54XQGHp0Dy3OYyE8ilBaJE7LJirVmha4kMkP7XBu6xHNOJrXtq4GsZmdLQkf+x8ie2DDbWS+tcwnw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "2.7.0"
|
||||
"@embedpdf/models": "2.8.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "2.7.0",
|
||||
"@embedpdf/core": "2.8.0",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -1020,17 +1020,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-zoom": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-zoom/-/plugin-zoom-2.7.0.tgz",
|
||||
"integrity": "sha512-5u2cQcahHkbOfa5Ug5hxXwtIOz4TzrQSgvmXvO7+jV0jXKOp1vFVB9tU26SKwwwW9OL6e4L/P6roKJRPbOnWyw==",
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-zoom/-/plugin-zoom-2.8.0.tgz",
|
||||
"integrity": "sha512-HepSQ7NFYhMsQsgfnC/G3b3LOYtNCWkCME0C0jBLrEYsgqsWozf97jIgzfOAeJsugyjQmojpgLUNt3xE9CtG6Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "2.7.0"
|
||||
"@embedpdf/models": "2.8.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "2.7.0",
|
||||
"@embedpdf/plugin-scroll": "2.7.0",
|
||||
"@embedpdf/plugin-viewport": "2.7.0",
|
||||
"@embedpdf/core": "2.8.0",
|
||||
"@embedpdf/plugin-scroll": "2.8.0",
|
||||
"@embedpdf/plugin-viewport": "2.8.0",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -1039,9 +1039,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/utils": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/utils/-/utils-2.7.0.tgz",
|
||||
"integrity": "sha512-QtHOChToxkemBbJSwBTbgUqQbUO+obRj5HBaA/8KGv1ESCjtrdwjfAHMCBYdRuyvMVvrQGe1bHuSbKpb2JVS5w==",
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/utils/-/utils-2.8.0.tgz",
|
||||
"integrity": "sha512-mt3DiQ8pnPk95q0zv7dXfN+y5fzJT2WtXyST8ziYEgmfhz0l2HT/MHCAwLzB3Whlnd2BdHThLfJyG1UD6pC84g==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"preact": "^10.26.4",
|
||||
|
||||
+23
-23
@@ -8,29 +8,29 @@
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.7.7",
|
||||
"@cantoo/pdf-lib": "^2.5.3",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@embedpdf/core": "^2.7.0",
|
||||
"@embedpdf/engines": "^2.7.0",
|
||||
"@embedpdf/models": "^2.7.0",
|
||||
"@embedpdf/plugin-annotation": "^2.7.0",
|
||||
"@embedpdf/plugin-attachment": "^2.7.0",
|
||||
"@embedpdf/plugin-bookmark": "^2.7.0",
|
||||
"@embedpdf/plugin-document-manager": "^2.7.0",
|
||||
"@embedpdf/plugin-export": "^2.7.0",
|
||||
"@embedpdf/plugin-history": "^2.7.0",
|
||||
"@embedpdf/plugin-interaction-manager": "^2.7.0",
|
||||
"@embedpdf/plugin-pan": "^2.7.0",
|
||||
"@embedpdf/plugin-print": "^2.7.0",
|
||||
"@embedpdf/plugin-redaction": "^2.7.0",
|
||||
"@embedpdf/plugin-render": "^2.7.0",
|
||||
"@embedpdf/plugin-rotate": "^2.7.0",
|
||||
"@embedpdf/plugin-scroll": "^2.7.0",
|
||||
"@embedpdf/plugin-search": "^2.7.0",
|
||||
"@embedpdf/plugin-selection": "^2.7.0",
|
||||
"@embedpdf/plugin-spread": "^2.7.0",
|
||||
"@embedpdf/plugin-thumbnail": "^2.7.0",
|
||||
"@embedpdf/plugin-tiling": "^2.7.0",
|
||||
"@embedpdf/plugin-viewport": "^2.7.0",
|
||||
"@embedpdf/plugin-zoom": "^2.7.0",
|
||||
"@embedpdf/core": "^2.8.0",
|
||||
"@embedpdf/engines": "^2.8.0",
|
||||
"@embedpdf/models": "^2.8.0",
|
||||
"@embedpdf/plugin-annotation": "^2.8.0",
|
||||
"@embedpdf/plugin-attachment": "^2.8.0",
|
||||
"@embedpdf/plugin-bookmark": "^2.8.0",
|
||||
"@embedpdf/plugin-document-manager": "^2.8.0",
|
||||
"@embedpdf/plugin-export": "^2.8.0",
|
||||
"@embedpdf/plugin-history": "^2.8.0",
|
||||
"@embedpdf/plugin-interaction-manager": "^2.8.0",
|
||||
"@embedpdf/plugin-pan": "^2.8.0",
|
||||
"@embedpdf/plugin-print": "^2.8.0",
|
||||
"@embedpdf/plugin-redaction": "^2.8.0",
|
||||
"@embedpdf/plugin-render": "^2.8.0",
|
||||
"@embedpdf/plugin-rotate": "^2.8.0",
|
||||
"@embedpdf/plugin-scroll": "^2.8.0",
|
||||
"@embedpdf/plugin-search": "^2.8.0",
|
||||
"@embedpdf/plugin-selection": "^2.8.0",
|
||||
"@embedpdf/plugin-spread": "^2.8.0",
|
||||
"@embedpdf/plugin-thumbnail": "^2.8.0",
|
||||
"@embedpdf/plugin-tiling": "^2.8.0",
|
||||
"@embedpdf/plugin-viewport": "^2.8.0",
|
||||
"@embedpdf/plugin-zoom": "^2.8.0",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@iconify/react": "^6.0.2",
|
||||
|
||||
@@ -1503,8 +1503,9 @@ editSelected = "Edit Annotation"
|
||||
editSquare = "Edit Square"
|
||||
editStampHint = "To change the image, delete this stamp and add a new one."
|
||||
editSwitchToSelect = "Switch to Select & Edit to edit this annotation."
|
||||
editText = "Edit Text Box"
|
||||
editText = "Edit"
|
||||
editTextMarkup = "Edit Text Markup"
|
||||
annotationStyle = "Annotation style"
|
||||
ellipse = "Ellipse"
|
||||
exit = "Exit annotation mode"
|
||||
fillColor = "Fill Colour"
|
||||
@@ -1515,13 +1516,19 @@ highlight = "Highlight"
|
||||
imagePreview = "Preview"
|
||||
inkHighlighter = "Freehand Highlighter"
|
||||
line = "Line"
|
||||
lineArrow = "Arrow"
|
||||
noBackground = "No background"
|
||||
note = "Note"
|
||||
comment = "Comment"
|
||||
comments = "Comments"
|
||||
insertText = "Insert Text"
|
||||
replaceText = "Replace Text"
|
||||
noteIcon = "Note Icon"
|
||||
notesStamps = "Notes & Stamps"
|
||||
opacity = "Opacity"
|
||||
pen = "Pen"
|
||||
polygon = "Polygon"
|
||||
polyline = "Polyline"
|
||||
properties = "Properties"
|
||||
rectangle = "Rectangle"
|
||||
redo = "Redo"
|
||||
@@ -4463,6 +4470,7 @@ rotateLeft = "Rotate Left"
|
||||
rotateRight = "Rotate Right"
|
||||
toggleSidebar = "Toggle Sidebar"
|
||||
toggleBookmarks = "Toggle Bookmarks"
|
||||
toggleComments = "Comments"
|
||||
print = "Print PDF"
|
||||
ruler = "Ruler / Measure"
|
||||
readAloud = "Read Aloud"
|
||||
@@ -6914,6 +6922,31 @@ loading = "Loading attachments..."
|
||||
empty = "No attachments in this document"
|
||||
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]
|
||||
title = "Form Fields"
|
||||
unsavedBadge = "Unsaved"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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';
|
||||
|
||||
// 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) {
|
||||
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 () => {
|
||||
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>
|
||||
</Tooltip>
|
||||
</Popover.Target>
|
||||
@@ -64,8 +69,9 @@ export function ColorControl({ value, onChange, label, disabled = false }: Color
|
||||
<Stack gap="xs">
|
||||
<MantineColorPicker
|
||||
format="hex"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
value={localColor}
|
||||
onChange={setLocalColor}
|
||||
onChangeEnd={onChange}
|
||||
swatches={[
|
||||
'#000000', '#ffffff', '#ff0000', '#00ff00', '#0000ff',
|
||||
'#ffff00', '#ff00ff', '#00ffff', '#ffa500', 'transparent'
|
||||
@@ -76,7 +82,7 @@ export function ColorControl({ value, onChange, label, disabled = false }: Color
|
||||
{supportsEyeDropper && (
|
||||
<Group justify="flex-end">
|
||||
<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 }} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import { ActionIcon, Tooltip, Popover, Stack, Slider, Text, Group, Button } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
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 FormatAlignLeftIcon from '@mui/icons-material/FormatAlignLeft';
|
||||
import FormatAlignCenterIcon from '@mui/icons-material/FormatAlignCenter';
|
||||
import FormatAlignRightIcon from '@mui/icons-material/FormatAlignRight';
|
||||
|
||||
type AnnotationType = 'text' | 'note' | 'shape';
|
||||
export type PropertiesAnnotationType = 'text' | 'note' | 'shape';
|
||||
|
||||
interface PropertiesPopoverProps {
|
||||
annotationType: AnnotationType;
|
||||
annotation: any;
|
||||
onUpdate: (patch: Record<string, any>) => void;
|
||||
annotationType: PropertiesAnnotationType;
|
||||
annotation: TrackedAnnotation<PdfAnnotationObject> | undefined;
|
||||
onUpdate: (patch: AnnotationPatch) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
@@ -24,7 +27,15 @@ export function PropertiesPopover({
|
||||
const { t } = useTranslation();
|
||||
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
|
||||
const fontSize = obj?.fontSize ?? 14;
|
||||
|
||||
@@ -81,6 +81,18 @@ type AnnotationDefaults =
|
||||
imageSize?: { width: number; height: number };
|
||||
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;
|
||||
|
||||
type AnnotationApiSurface = {
|
||||
@@ -294,6 +306,24 @@ const TOOL_DEFAULT_BUILDERS: Record<AnnotationToolId, ToolDefaultsBuilder> = {
|
||||
stamp: buildStampDefaults,
|
||||
signatureStamp: buildStampDefaults,
|
||||
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) {
|
||||
|
||||
@@ -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 { useTranslation } from 'react-i18next';
|
||||
import { Group } from '@mantine/core';
|
||||
import { createPortal } from 'react-dom';
|
||||
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 type { TrackedAnnotation } from '@embedpdf/plugin-annotation';
|
||||
import type { PdfAnnotationObject } from '@embedpdf/models';
|
||||
import type { AnnotationPatch, AnnotationObject } from '@app/components/viewer/viewerTypes';
|
||||
import { PdfAnnotationSubtype, type PdfAnnotationObject } from '@embedpdf/models';
|
||||
import type { AnnotationObject } from '@app/components/viewer/viewerTypes';
|
||||
import { useActiveDocumentId } from '@app/components/viewer/useActiveDocumentId';
|
||||
import { OpacityControl } from '@app/components/annotation/shared/OpacityControl';
|
||||
import { WidthControl } from '@app/components/annotation/shared/WidthControl';
|
||||
import { PropertiesPopover } from '@app/components/annotation/shared/PropertiesPopover';
|
||||
import { ColorControl } from '@app/components/annotation/shared/ColorControl';
|
||||
import { useViewer } from '@app/contexts/ViewerContext';
|
||||
import { useAnnotationMenuHandlers } from '@app/components/viewer/useAnnotationMenuHandlers';
|
||||
import { AnnotationTypeButtons } from '@app/components/viewer/AnnotationTypeButtons';
|
||||
|
||||
/**
|
||||
* Props interface matching EmbedPDF's annotation selection menu pattern
|
||||
@@ -34,22 +30,10 @@ export interface AnnotationSelectionMenuProps {
|
||||
|
||||
export function AnnotationSelectionMenu(props: AnnotationSelectionMenuProps) {
|
||||
const activeDocumentId = useActiveDocumentId();
|
||||
|
||||
// Don't render until we have a valid document ID
|
||||
if (!activeDocumentId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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({
|
||||
documentId,
|
||||
context,
|
||||
@@ -58,366 +42,56 @@ function AnnotationSelectionMenuInner({
|
||||
}: AnnotationSelectionMenuProps & { documentId: string }) {
|
||||
const annotation = context?.annotation;
|
||||
const pageIndex = context?.pageIndex;
|
||||
const { t } = useTranslation();
|
||||
const { provides } = useAnnotation(documentId);
|
||||
const { scrollActions, requestCommentFocus } = useViewer();
|
||||
const wrapperRef = useRef<HTMLDivElement>(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) => {
|
||||
wrapperRef.current = node;
|
||||
// Call the EmbedPDF ref callback
|
||||
menuWrapperProps?.ref?.(node);
|
||||
}, [menuWrapperProps]);
|
||||
|
||||
// Type detection
|
||||
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
|
||||
// Track menu position via MutationObserver (handles drag repositioning)
|
||||
useEffect(() => {
|
||||
if (!selected || !annotation || !wrapperRef.current) {
|
||||
setMenuPosition(null);
|
||||
@@ -426,24 +100,15 @@ function AnnotationSelectionMenuInner({
|
||||
|
||||
const updatePosition = () => {
|
||||
const wrapper = wrapperRef.current;
|
||||
if (!wrapper) {
|
||||
setMenuPosition(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const wrapperRect = wrapper.getBoundingClientRect();
|
||||
setMenuPosition({
|
||||
top: wrapperRect.bottom + 8,
|
||||
left: wrapperRect.left + wrapperRect.width / 2,
|
||||
});
|
||||
if (!wrapper) { setMenuPosition(null); return; }
|
||||
const rect = wrapper.getBoundingClientRect();
|
||||
setMenuPosition({ top: rect.bottom + 8, left: rect.left + rect.width / 2 });
|
||||
};
|
||||
|
||||
updatePosition();
|
||||
|
||||
// MutationObserver catches EmbedPDF updating the wrapper's inline style during drag
|
||||
const observer = new MutationObserver(updatePosition);
|
||||
observer.observe(wrapperRef.current, { attributes: true, attributeFilter: ['style'] });
|
||||
|
||||
window.addEventListener('scroll', updatePosition, true);
|
||||
window.addEventListener('resize', updatePosition);
|
||||
|
||||
@@ -452,9 +117,8 @@ function AnnotationSelectionMenuInner({
|
||||
window.removeEventListener('scroll', updatePosition, true);
|
||||
window.removeEventListener('resize', updatePosition);
|
||||
};
|
||||
}, [selected, annotation]);
|
||||
}, [selected]);
|
||||
|
||||
// Early return AFTER all hooks have been called
|
||||
if (!selected || !annotation) return null;
|
||||
|
||||
const menuContent = menuPosition ? (
|
||||
@@ -466,39 +130,34 @@ function AnnotationSelectionMenuInner({
|
||||
left: `${menuPosition.left}px`,
|
||||
transform: 'translateX(-50%)',
|
||||
pointerEvents: 'auto',
|
||||
zIndex: 10000, // Very high z-index to appear above everything
|
||||
zIndex: 10000,
|
||||
backgroundColor: 'var(--mantine-color-body)',
|
||||
borderRadius: 8,
|
||||
padding: '8px 12px',
|
||||
boxShadow: '0 2px 12px rgba(0, 0, 0, 0.25)',
|
||||
border: '1px solid var(--mantine-color-default-border)',
|
||||
fontSize: '14px',
|
||||
minWidth: `${calculateWidth(annotationType)}px`,
|
||||
minWidth: `${handlers.menuWidth}px`,
|
||||
transition: 'min-width 0.2s ease',
|
||||
}}
|
||||
>
|
||||
<Group gap="sm" wrap="nowrap" justify="center">
|
||||
{renderButtons()}
|
||||
<AnnotationTypeButtons
|
||||
{...handlers}
|
||||
annotation={annotation}
|
||||
documentId={documentId}
|
||||
pageIndex={pageIndex}
|
||||
annotationId={handlers.annotationId}
|
||||
/>
|
||||
</Group>
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Invisible wrapper that provides positioning - uses EmbedPDF's menuWrapperProps.
|
||||
Must stay pointerEvents:none so EmbedPDF's internal drag handlers receive events.
|
||||
Edit Text button dispatches dblclick on this wrapper's parent to use EmbedPDF's built-in inline editing. */}
|
||||
<div
|
||||
ref={setRef}
|
||||
style={{
|
||||
...menuWrapperProps?.style,
|
||||
opacity: 0,
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
/>
|
||||
{typeof document !== 'undefined' && menuContent
|
||||
? createPortal(menuContent, document.body)
|
||||
: null}
|
||||
{/* Invisible wrapper for EmbedPDF positioning — pointer-events:none so drag still works */}
|
||||
<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 {
|
||||
visible: boolean;
|
||||
thumbnailVisible: boolean;
|
||||
bookmarkVisible: boolean;
|
||||
documentCacheKey?: string;
|
||||
preloadCacheKeys?: string[];
|
||||
}
|
||||
@@ -32,7 +33,7 @@ const createEntry = (overrides: Partial<AttachmentCacheEntry> = {}): AttachmentC
|
||||
...overrides,
|
||||
});
|
||||
|
||||
export const AttachmentSidebar = ({ visible, thumbnailVisible, documentCacheKey, preloadCacheKeys = [] }: AttachmentSidebarProps) => {
|
||||
export const AttachmentSidebar = ({ visible, thumbnailVisible, bookmarkVisible, documentCacheKey, preloadCacheKeys = [] }: AttachmentSidebarProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { attachmentActions, hasAttachmentSupport } = useViewer();
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
@@ -286,7 +287,7 @@ export const AttachmentSidebar = ({ visible, thumbnailVisible, documentCacheKey,
|
||||
className="attachment-sidebar"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
right: thumbnailVisible ? SIDEBAR_WIDTH : 0,
|
||||
right: `${(thumbnailVisible ? 15 : 0) + (bookmarkVisible ? 15 : 0)}rem`,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
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,
|
||||
isBookmarkSidebarVisible,
|
||||
isAttachmentSidebarVisible,
|
||||
isCommentsSidebarVisible,
|
||||
isSearchInterfaceVisible,
|
||||
searchInterfaceActions,
|
||||
zoomActions,
|
||||
@@ -201,8 +202,9 @@ const EmbedPdfViewerContent = ({
|
||||
const isSignatureMode = isInAnnotationTool;
|
||||
const isManualRedactMode = selectedTool === 'redact';
|
||||
|
||||
// Enable annotations only when annotation tool is selected
|
||||
const shouldEnableAnnotations = selectedTool === 'annotate' || isSignatureMode;
|
||||
// Enable annotations when annotation tool is selected OR when annotations are visible
|
||||
// (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
|
||||
const shouldEnableRedaction = selectedTool === 'redact';
|
||||
@@ -910,10 +912,12 @@ const EmbedPdfViewerContent = ({
|
||||
}, [isFormFillToolActive, currentFile, currentFileId, fetchFormFields]);
|
||||
|
||||
const sidebarWidthRem = 15;
|
||||
const commentsSidebarWidthRem = 18;
|
||||
const totalRightMargin =
|
||||
(isThumbnailSidebarVisible ? sidebarWidthRem : 0) +
|
||||
(isBookmarkSidebarVisible ? sidebarWidthRem : 0) +
|
||||
(isAttachmentSidebarVisible ? sidebarWidthRem : 0);
|
||||
(isAttachmentSidebarVisible ? sidebarWidthRem : 0) +
|
||||
(isCommentsSidebarVisible ? commentsSidebarWidthRem : 0);
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -969,6 +973,7 @@ const EmbedPdfViewerContent = ({
|
||||
(effectiveFile?.file instanceof File ? effectiveFile.file.name : undefined))
|
||||
}
|
||||
enableAnnotations={shouldEnableAnnotations}
|
||||
isSignMode={selectedTool === 'sign'}
|
||||
showBakedAnnotations={isAnnotationsVisible}
|
||||
enableRedaction={shouldEnableRedaction}
|
||||
enableFormFill={shouldEnableFormFill}
|
||||
@@ -978,6 +983,8 @@ const EmbedPdfViewerContent = ({
|
||||
historyApiRef={historyApiRef as React.RefObject<any>}
|
||||
redactionTrackerRef={redactionTrackerRef as React.RefObject<RedactionPendingTrackerAPI>}
|
||||
fileId={currentFileId}
|
||||
isCommentsSidebarVisible={isCommentsSidebarVisible}
|
||||
commentsSidebarRightOffset={`${(isThumbnailSidebarVisible ? sidebarWidthRem : 0) + (isBookmarkSidebarVisible ? sidebarWidthRem : 0) + (isAttachmentSidebarVisible ? sidebarWidthRem : 0)}rem`}
|
||||
onSignatureAdded={() => {
|
||||
// Handle signature added - for debugging, enable console logs as needed
|
||||
// Future: Handle signature completion
|
||||
@@ -1043,6 +1050,7 @@ const EmbedPdfViewerContent = ({
|
||||
<AttachmentSidebar
|
||||
visible={isAttachmentSidebarVisible}
|
||||
thumbnailVisible={isThumbnailSidebarVisible}
|
||||
bookmarkVisible={isBookmarkSidebarVisible}
|
||||
documentCacheKey={bookmarkCacheKey}
|
||||
preloadCacheKeys={allBookmarkCacheKeys}
|
||||
/>
|
||||
|
||||
@@ -61,6 +61,9 @@ import { DocumentReadyWrapper } from '@app/components/viewer/DocumentReadyWrappe
|
||||
import { ActiveDocumentProvider } from '@app/components/viewer/ActiveDocumentContext';
|
||||
import { absoluteWithBasePath } from '@app/constants/app';
|
||||
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 {
|
||||
file?: File | Blob;
|
||||
@@ -78,12 +81,24 @@ interface LocalEmbedPDFProps {
|
||||
redactionTrackerRef?: React.RefObject<RedactionPendingTrackerAPI>;
|
||||
/** File identity passed through to FormFieldOverlay for stale-field guards */
|
||||
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 [pdfUrl, setPdfUrl] = useState<string | null>(null);
|
||||
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
|
||||
useEffect(() => {
|
||||
@@ -655,6 +670,27 @@ export function LocalEmbedPDF({ file, url, fileName, enableAnnotations = false,
|
||||
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) {
|
||||
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 */}
|
||||
{(enableRedaction || isManualRedactionMode) && <RedactionAPIBridge />}
|
||||
{/* 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} />}
|
||||
{enableAnnotations && <AnnotationAPIBridge ref={annotationApiRef} />}
|
||||
|
||||
@@ -699,6 +735,7 @@ export function LocalEmbedPDF({ file, url, fileName, enableAnnotations = false,
|
||||
}
|
||||
>
|
||||
{(documentId) => (
|
||||
<>
|
||||
<GlobalPointerProvider documentId={documentId}>
|
||||
<Viewport
|
||||
documentId={documentId}
|
||||
@@ -793,6 +830,16 @@ export function LocalEmbedPDF({ file, url, fileName, enableAnnotations = false,
|
||||
/>
|
||||
</Viewport>
|
||||
</GlobalPointerProvider>
|
||||
{enableAnnotations && (
|
||||
<CommentAuthorProvider displayName={commentAuthorName}>
|
||||
<CommentsSidebar
|
||||
documentId={documentId}
|
||||
visible={isCommentsSidebarVisible}
|
||||
rightOffset={commentsSidebarRightOffset}
|
||||
/>
|
||||
</CommentAuthorProvider>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</DocumentReadyWrapper>
|
||||
</ActiveDocumentProvider>
|
||||
|
||||
@@ -97,11 +97,13 @@ export function PdfViewerToolbar({
|
||||
};
|
||||
|
||||
const handlePreviousPage = () => {
|
||||
scrollActions.scrollToPreviousPage();
|
||||
const { currentPage: cur } = getScrollState();
|
||||
if (cur > 1) scrollActions.scrollToPage(cur - 1);
|
||||
};
|
||||
|
||||
const handleNextPage = () => {
|
||||
scrollActions.scrollToNextPage();
|
||||
const { currentPage: cur, totalPages: tot } = getScrollState();
|
||||
if (cur < tot) scrollActions.scrollToPage(cur + 1);
|
||||
};
|
||||
|
||||
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 { signatureConfig, storeImageData, isPlacementMode, placementPreviewSize, setSignaturesApplied } = useSignature();
|
||||
const { getZoomState, registerImmediateZoomUpdate } = useViewer();
|
||||
@@ -151,6 +156,14 @@ export const SignatureAPIBridge = forwardRef<SignatureAPI>(function SignatureAPI
|
||||
};
|
||||
}, [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(
|
||||
(size: { width: number; height: number }) => {
|
||||
const zoom = currentZoom || 1;
|
||||
@@ -302,13 +315,12 @@ export const SignatureAPIBridge = forwardRef<SignatureAPI>(function SignatureAPI
|
||||
activateDrawMode: () => {
|
||||
if (!annotationApi) return;
|
||||
|
||||
// Activate the built-in ink tool for drawing
|
||||
annotationApi.setActiveTool('ink');
|
||||
// Use signatureInk (not ink) so the annotation pen tool stays unaffected
|
||||
annotationApi.setActiveTool('signatureInk');
|
||||
|
||||
// Set default ink tool properties (black color, 2px width)
|
||||
const activeTool = annotationApi.getActiveTool();
|
||||
if (activeTool && activeTool.id === 'ink') {
|
||||
annotationApi.setToolDefaults('ink', {
|
||||
if (activeTool && activeTool.id === 'signatureInk') {
|
||||
annotationApi.setToolDefaults('signatureInk', {
|
||||
color: '#000000',
|
||||
thickness: 2,
|
||||
lineWidth: 2,
|
||||
@@ -338,11 +350,16 @@ export const SignatureAPIBridge = forwardRef<SignatureAPI>(function SignatureAPI
|
||||
width: size
|
||||
});
|
||||
|
||||
// Force reactivate ink tool to ensure new settings take effect
|
||||
annotationApi.setActiveTool(null); // Deactivate first
|
||||
// Only reactivate ink if it was already the active tool — never auto-activate it.
|
||||
// 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(() => {
|
||||
annotationApi.setActiveTool('ink'); // Reactivate with new settings
|
||||
annotationApi.setActiveTool('ink');
|
||||
}, 50);
|
||||
}
|
||||
},
|
||||
|
||||
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 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 { sidebarRefs } = useSidebarContext();
|
||||
const { position: tooltipPosition } = useRightRailTooltipSide(sidebarRefs, 12);
|
||||
@@ -63,10 +63,8 @@ export function useViewerRightRailButtons(
|
||||
if (selectedTool === 'annotate') {
|
||||
setIsAnnotationsActive(true);
|
||||
} else if (selectedTool) {
|
||||
// Any other tool is active — annotate button should not be highlighted
|
||||
setIsAnnotationsActive(false);
|
||||
} else {
|
||||
// No tool selected — fall back to URL path check
|
||||
setIsAnnotationsActive(isAnnotationsPath());
|
||||
}
|
||||
}, [selectedTool, isAnnotationsPath]);
|
||||
@@ -85,6 +83,7 @@ export function useViewerRightRailButtons(
|
||||
const sidebarLabel = t('rightRail.toggleSidebar', 'Toggle Sidebar');
|
||||
const bookmarkLabel = t('rightRail.toggleBookmarks', 'Toggle Bookmarks');
|
||||
const attachmentLabel = t('rightRail.toggleAttachments', 'Toggle Attachments');
|
||||
const commentsLabel = t('rightRail.toggleComments', 'Comments');
|
||||
const printLabel = t('rightRail.print', 'Print PDF');
|
||||
const annotationsLabel = t('rightRail.annotations', 'Annotations');
|
||||
const formFillLabel = t('rightRail.formFill', 'Fill Form');
|
||||
@@ -237,13 +236,25 @@ export function useViewerRightRailButtons(
|
||||
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',
|
||||
icon: <LocalIcon icon="print" width="1.5rem" height="1.5rem" />,
|
||||
tooltip: printLabel,
|
||||
ariaLabel: printLabel,
|
||||
section: 'top' as const,
|
||||
order: 56,
|
||||
order: 57,
|
||||
onClick: () => {
|
||||
viewer.printActions.print();
|
||||
}
|
||||
|
||||
@@ -71,6 +71,9 @@ export type AnnotationToolId =
|
||||
| 'inkHighlighter'
|
||||
| 'text'
|
||||
| 'note'
|
||||
| 'textComment'
|
||||
| 'insertText'
|
||||
| 'replaceText'
|
||||
| 'square'
|
||||
| 'circle'
|
||||
| 'line'
|
||||
@@ -84,7 +87,6 @@ export type AnnotationToolId =
|
||||
// Import for internal use within this file, and re-export for external consumers
|
||||
import 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>;
|
||||
|
||||
@@ -142,4 +144,5 @@ export interface AnnotationToolOptions {
|
||||
icon?: 'Comment' | 'Key' | 'Note' | 'Help' | 'NewParagraph' | 'Paragraph' | 'Insert';
|
||||
contents?: string;
|
||||
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;
|
||||
isAttachmentSidebarVisible: boolean;
|
||||
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
|
||||
isSearchInterfaceVisible: boolean;
|
||||
@@ -170,6 +183,13 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
|
||||
const [isThumbnailSidebarVisible, setIsThumbnailSidebarVisible] = useState(false);
|
||||
const [isBookmarkSidebarVisible, setIsBookmarkSidebarVisible] = 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 [isAnnotationsVisible, setIsAnnotationsVisible] = useState(true);
|
||||
const [isAnnotationMode, setIsAnnotationModeState] = useState(false);
|
||||
@@ -261,6 +281,31 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
|
||||
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 = {
|
||||
open: () => setSearchInterfaceVisible(true),
|
||||
close: () => setSearchInterfaceVisible(false),
|
||||
@@ -392,6 +437,12 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
|
||||
toggleBookmarkSidebar,
|
||||
isAttachmentSidebarVisible,
|
||||
toggleAttachmentSidebar,
|
||||
isCommentsSidebarVisible,
|
||||
setCommentsSidebarVisible,
|
||||
toggleCommentsSidebar,
|
||||
highlightCommentRequest,
|
||||
requestCommentFocus,
|
||||
clearHighlightCommentRequest,
|
||||
|
||||
// Search interface
|
||||
isSearchInterfaceVisible,
|
||||
|
||||
@@ -671,6 +671,31 @@
|
||||
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 */
|
||||
* {
|
||||
transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease;
|
||||
|
||||
@@ -22,6 +22,9 @@ const DRAWING_TOOLS: AnnotationToolId[] = [
|
||||
'inkHighlighter',
|
||||
'text',
|
||||
'note',
|
||||
'textComment',
|
||||
'insertText',
|
||||
'replaceText',
|
||||
'square',
|
||||
'circle',
|
||||
'line',
|
||||
@@ -43,6 +46,9 @@ const KNOWN_ANNOTATION_TOOLS: AnnotationToolId[] = [
|
||||
'inkHighlighter',
|
||||
'text',
|
||||
'note',
|
||||
'textComment',
|
||||
'insertText',
|
||||
'replaceText',
|
||||
'square',
|
||||
'circle',
|
||||
'line',
|
||||
@@ -75,7 +81,7 @@ const Annotate = (_props: BaseToolProps) => {
|
||||
const viewerContext = useContext(ViewerContext);
|
||||
const viewerContextRef = useRef(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');
|
||||
|
||||
@@ -312,6 +318,11 @@ const Annotate = (_props: BaseToolProps) => {
|
||||
annotationApiRef?.current?.setAnnotationStyle?.(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
|
||||
setTimeout(() => {
|
||||
manualToolSwitch.current = false;
|
||||
@@ -346,7 +357,7 @@ const Annotate = (_props: BaseToolProps) => {
|
||||
const unsubscribe = annotationApiRef?.current?.onAnnotationEvent?.((event: AnnotationEvent) => {
|
||||
if (event.type === 'create' && event.committed) {
|
||||
const toolId = activeToolRef.current;
|
||||
if (toolId === 'text' || toolId === 'note') {
|
||||
if (['text', 'note', 'textComment', 'insertText', 'replaceText'].includes(toolId)) {
|
||||
setActiveTool('select');
|
||||
activeToolRef.current = 'select';
|
||||
annotationApiRef?.current?.activateAnnotationTool?.('select');
|
||||
|
||||
@@ -209,6 +209,12 @@ export function AnnotationPanel(props: AnnotationPanelProps) {
|
||||
{ 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 }[] = [
|
||||
{ id: 'text', label: t('annotation.text', 'Text box'), icon: 'text-fields' },
|
||||
{ id: 'note', label: t('annotation.note', 'Note'), icon: 'sticky-note-2' },
|
||||
@@ -778,6 +784,11 @@ export function AnnotationPanel(props: AnnotationPanelProps) {
|
||||
{renderToolButtons(shapeTools)}
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text size="sm" fw={600} mb="xs">{t('annotation.comments', 'Comments')}</Text>
|
||||
{renderToolButtons(commentTools)}
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text size="sm" fw={600} mb="xs">{t('annotation.notesStamps', 'Notes & Stamps')}</Text>
|
||||
{renderToolButtons(otherTools)}
|
||||
|
||||
Reference in New Issue
Block a user