Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14ef08e8f4 | ||
|
|
c46156f37f | ||
|
|
41945543e0 | ||
|
|
e5f6180dbe | ||
|
|
57c810ab9a | ||
|
|
b012f18a40 | ||
|
|
9e8606cab4 | ||
|
|
55bcb92810 | ||
|
|
a7f2abcb22 |
@@ -159,7 +159,7 @@ jobs:
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
|
||||
with:
|
||||
gradle-version: 8.14
|
||||
gradle-version: 9.3.1
|
||||
|
||||
- name: Run Gradle Command
|
||||
run: |
|
||||
|
||||
@@ -80,7 +80,7 @@ jobs:
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
|
||||
with:
|
||||
gradle-version: 8.14
|
||||
gradle-version: 9.3.1
|
||||
cache-disabled: true
|
||||
|
||||
- name: Build with Gradle and spring security ${{ matrix.spring-security }}
|
||||
@@ -165,7 +165,7 @@ jobs:
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
|
||||
with:
|
||||
gradle-version: 8.14
|
||||
gradle-version: 9.3.1
|
||||
cache-disabled: true
|
||||
|
||||
- name: Generate OpenAPI documentation
|
||||
@@ -247,7 +247,7 @@ jobs:
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
|
||||
with:
|
||||
gradle-version: 8.14
|
||||
gradle-version: 9.3.1
|
||||
cache-disabled: true
|
||||
|
||||
- name: check the licenses for compatibility
|
||||
@@ -320,7 +320,7 @@ jobs:
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
|
||||
with:
|
||||
gradle-version: 8.14
|
||||
gradle-version: 9.3.1
|
||||
cache-disabled: true
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
@@ -427,7 +427,7 @@ jobs:
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
|
||||
with:
|
||||
gradle-version: 8.14
|
||||
gradle-version: 9.3.1
|
||||
cache-disabled: true
|
||||
|
||||
- name: Build application
|
||||
|
||||
@@ -339,7 +339,7 @@ jobs:
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
|
||||
with:
|
||||
gradle-version: 8.14
|
||||
gradle-version: 9.3.1
|
||||
|
||||
- name: Check licenses and generate report
|
||||
id: license-check
|
||||
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
|
||||
with:
|
||||
gradle-version: 8.14
|
||||
gradle-version: 9.3.1
|
||||
|
||||
- name: Get version number
|
||||
id: versionNumber
|
||||
@@ -134,7 +134,7 @@ jobs:
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
|
||||
with:
|
||||
gradle-version: 8.14
|
||||
gradle-version: 9.3.1
|
||||
|
||||
- name: Setup Node.js
|
||||
if: matrix.variant.build_frontend == true
|
||||
@@ -214,7 +214,7 @@ jobs:
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
|
||||
with:
|
||||
gradle-version: 8.14
|
||||
gradle-version: 9.3.1
|
||||
|
||||
- name: Build Java backend with JLink
|
||||
working-directory: ./
|
||||
|
||||
@@ -68,7 +68,7 @@ jobs:
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
|
||||
with:
|
||||
gradle-version: 8.14
|
||||
gradle-version: 9.3.1
|
||||
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew build
|
||||
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
|
||||
with:
|
||||
gradle-version: 8.14
|
||||
gradle-version: 9.3.1
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
|
||||
with:
|
||||
gradle-version: 8.14
|
||||
gradle-version: 9.3.1
|
||||
|
||||
- name: Generate Swagger documentation
|
||||
run: ./gradlew :stirling-pdf:generateOpenApiDocs
|
||||
|
||||
@@ -126,7 +126,7 @@ jobs:
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
|
||||
with:
|
||||
gradle-version: 8.14
|
||||
gradle-version: 9.3.1
|
||||
|
||||
- name: Build Java backend with JLink
|
||||
working-directory: ./
|
||||
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
|
||||
with:
|
||||
gradle-version: 8.14
|
||||
gradle-version: 9.3.1
|
||||
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew build
|
||||
|
||||
@@ -55,6 +55,8 @@ Development for the AI engine happens in the `engine/` folder. It's built with L
|
||||
#### Import Paths - CRITICAL
|
||||
**ALWAYS use `@app/*` for imports.** Do not use `@core/*` or `@proprietary/*` unless explicitly wrapping/extending a lower layer implementation.
|
||||
|
||||
For a broader explanation of the frontend layering and override architecture, see [frontend/DeveloperGuide.md](frontend/DeveloperGuide.md).
|
||||
|
||||
```typescript
|
||||
// ✅ CORRECT - Use @app/* for all imports
|
||||
import { AppLayout } from "@app/components/AppLayout";
|
||||
|
||||
+14
-1
@@ -226,10 +226,11 @@ public class SsrfProtectionService {
|
||||
}
|
||||
|
||||
private boolean isPrivateIPv4Range(String ip) {
|
||||
// Includes RFC1918, loopback, link-local, and unspecified addresses
|
||||
// Includes RFC1918, RFC6598, loopback, link-local, and unspecified addresses
|
||||
return ip.startsWith("10.")
|
||||
|| ip.startsWith("192.168.")
|
||||
|| (ip.startsWith("172.") && isInRange172(ip))
|
||||
|| (ip.startsWith("100.") && isInRange100(ip))
|
||||
|| ip.startsWith("169.254.")
|
||||
|| ip.startsWith("127.")
|
||||
|| "0.0.0.0".equals(ip);
|
||||
@@ -247,6 +248,18 @@ public class SsrfProtectionService {
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isInRange100(String ip) {
|
||||
String[] parts = ip.split("\\.");
|
||||
if (parts.length >= 2) {
|
||||
try {
|
||||
int secondOctet = Integer.parseInt(parts[1]);
|
||||
return secondOctet >= 64 && secondOctet <= 127;
|
||||
} catch (NumberFormatException e) {
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isCloudMetadataAddress(String ip) {
|
||||
String normalizedIp = normalizeIpv4MappedAddress(ip);
|
||||
// Cloud metadata endpoints for AWS, GCP, Azure, Oracle Cloud, and IBM Cloud
|
||||
|
||||
@@ -13,11 +13,17 @@ public class EmlToPdf {
|
||||
|
||||
public static String convertEmlToHtml(byte[] emlBytes, EmlToPdfRequest request)
|
||||
throws IOException {
|
||||
return convertEmlToHtml(emlBytes, request, null);
|
||||
}
|
||||
|
||||
public static String convertEmlToHtml(
|
||||
byte[] emlBytes, EmlToPdfRequest request, CustomHtmlSanitizer customHtmlSanitizer)
|
||||
throws IOException {
|
||||
EmlProcessingUtils.validateEmlInput(emlBytes);
|
||||
|
||||
EmlParser.EmailContent emailContent =
|
||||
EmlParser.extractEmailContent(emlBytes, request, null);
|
||||
return EmlProcessingUtils.generateEnhancedEmailHtml(emailContent, request, null);
|
||||
EmlParser.extractEmailContent(emlBytes, request, customHtmlSanitizer);
|
||||
return EmlProcessingUtils.generateEnhancedEmailHtml(emailContent, request, customHtmlSanitizer);
|
||||
}
|
||||
|
||||
public static byte[] convertEmlToPdf(
|
||||
|
||||
+2
-1
@@ -81,7 +81,8 @@ public class ConvertEmlToPDF {
|
||||
|
||||
if (request.isDownloadHtml()) {
|
||||
try {
|
||||
String htmlContent = EmlToPdf.convertEmlToHtml(fileBytes, request);
|
||||
String htmlContent =
|
||||
EmlToPdf.convertEmlToHtml(fileBytes, request, customHtmlSanitizer);
|
||||
log.info("Successfully converted email to HTML: {}", originalFilename);
|
||||
return WebResponseUtils.bytesToWebResponse(
|
||||
htmlContent.getBytes(StandardCharsets.UTF_8),
|
||||
|
||||
+10
@@ -230,6 +230,16 @@ public class ConfigController {
|
||||
}
|
||||
configData.put("isNewUser", isNewUser);
|
||||
|
||||
String username = null;
|
||||
if (userService != null) {
|
||||
try {
|
||||
username = userService.getCurrentUsername();
|
||||
} catch (Exception e) {
|
||||
// Not authenticated or security disabled
|
||||
}
|
||||
}
|
||||
configData.put("username", username);
|
||||
|
||||
// System settings
|
||||
configData.put(
|
||||
"enableAlphaFunctionality",
|
||||
|
||||
+3
-3
@@ -488,14 +488,14 @@ public class DatabaseService implements DatabaseServiceInterface {
|
||||
private void executeDatabaseScript(Path scriptPath) {
|
||||
if (isH2Database()) {
|
||||
|
||||
// Validate SQL content BEFORE execution to prevent injection attacks
|
||||
validateSqlContent(scriptPath);
|
||||
|
||||
if (!verifyBackup(scriptPath)) {
|
||||
log.error("Backup verification failed for: {}", scriptPath);
|
||||
throw new IllegalArgumentException("Backup verification failed for: " + scriptPath);
|
||||
}
|
||||
|
||||
// Validate SQL content before execution to prevent injection attacks
|
||||
validateSqlContent(scriptPath);
|
||||
|
||||
String query = "RUNSCRIPT from ?;";
|
||||
|
||||
try (Connection conn = dataSource.getConnection();
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
# Frontend Developer Guide
|
||||
|
||||
This document is a guide to the main frontend architectural rules in Stirling-PDF.
|
||||
|
||||
## Mode-Specific Code
|
||||
|
||||
There are several different builds of the frontend, each with their own mode-specific code.
|
||||
The frontend uses [TypeScript Path Aliases](https://www.typescriptlang.org/tsconfig/#paths) to ensure that only relevant code for the configured app version will be present in the build.
|
||||
Refer to the various `tsconfig.*.json` files to see the specific path alias order.
|
||||
|
||||
The vast majority of the code is in the `src/core` folder, which is the open-source app.
|
||||
Other builds, such as the desktop app, use `src/core` as the base layer, and then override various files to change behaviour.
|
||||
If an import is `from '@app/a/b'`, this will refer to `src/core/a/b.ts` in the core build of the app, but may refer to `src/desktop/a/b.ts` in the desktop app if that file exists.
|
||||
|
||||
It is important to try to minimise the amount of overridden code in the app.
|
||||
Often, just one function needs to behave differently in a specific mode.
|
||||
For example:
|
||||
|
||||
```ts
|
||||
// core/file1.ts
|
||||
function f1() { /* ... */ }
|
||||
function f2() { /* ... */ } // Needs to be overridden in desktop
|
||||
function f3() { /* ... */ }
|
||||
```
|
||||
|
||||
In cases like this, instead of duplicating the entire file, create a new extension module for the core app and override _that_ in the desktop app.
|
||||
|
||||
```ts
|
||||
// core/file1.ts
|
||||
import { f2 } from '@app/file1Extensions';
|
||||
|
||||
function f1() { /* ... */ }
|
||||
function f3() { /* ... */ }
|
||||
```
|
||||
|
||||
```ts
|
||||
// core/file1Extensions.ts
|
||||
export function f2() { /* ... */ } // Original core implementation
|
||||
```
|
||||
|
||||
```ts
|
||||
// desktop/file1Extensions.ts
|
||||
export function f2() { /* ... */ } // Custom desktop implementation
|
||||
```
|
||||
|
||||
Building with this pattern minimises the duplicated code in the system and greatly reduces the chances that changing the core app will break the desktop app.
|
||||
|
||||
In general, all imports for app code should come via `@app` because it allows for other builds of the app to override behaviour if necessary.
|
||||
The only time that it is beneficial to import via a specific folder (e.g. `@core`) is when you want to reduce duplication **in the file you are overriding**. For example:
|
||||
|
||||
```ts
|
||||
// core/file2.ts
|
||||
|
||||
export interface MyProps {
|
||||
// Lots of properties that we don't want to duplicate
|
||||
}
|
||||
|
||||
export function f1(props: MyProps) { /* ... */ } // Original core implementation
|
||||
```
|
||||
|
||||
```ts
|
||||
// desktop/file2.ts
|
||||
|
||||
import { type MyProps } from '@core/file2';
|
||||
export { type MyProps }; // Re-export so anything importing file2 can still access MyProps
|
||||
|
||||
export function f1(props: MyProps) { /* ... */ } // Custom desktop implementation
|
||||
```
|
||||
Generated
+212
-183
@@ -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",
|
||||
@@ -115,7 +115,7 @@
|
||||
"postcss-preset-mantine": "^1.18.0",
|
||||
"postcss-simple-vars": "^7.0.1",
|
||||
"puppeteer": "^24.25.0",
|
||||
"tsx": "^4.19.4",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^5.9.2",
|
||||
"typescript-eslint": "^8.44.1",
|
||||
"vite": "^7.1.7",
|
||||
@@ -476,6 +476,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
@@ -516,6 +517,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
}
|
||||
@@ -574,13 +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",
|
||||
@@ -591,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",
|
||||
@@ -603,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",
|
||||
@@ -657,31 +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",
|
||||
@@ -690,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",
|
||||
@@ -707,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",
|
||||
@@ -724,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",
|
||||
@@ -741,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",
|
||||
@@ -758,15 +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",
|
||||
@@ -775,15 +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",
|
||||
@@ -792,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",
|
||||
@@ -811,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",
|
||||
@@ -828,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",
|
||||
@@ -850,15 +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",
|
||||
@@ -867,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",
|
||||
@@ -884,16 +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",
|
||||
@@ -902,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",
|
||||
@@ -919,17 +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",
|
||||
@@ -938,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",
|
||||
@@ -955,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",
|
||||
@@ -973,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",
|
||||
@@ -993,15 +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",
|
||||
@@ -1010,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",
|
||||
@@ -1029,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",
|
||||
@@ -1099,6 +1109,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
|
||||
"integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.18.3",
|
||||
"@emotion/babel-plugin": "^11.13.5",
|
||||
@@ -1142,6 +1153,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz",
|
||||
"integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.18.3",
|
||||
"@emotion/babel-plugin": "^11.13.5",
|
||||
@@ -2000,6 +2012,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@mantine/core/-/core-8.3.15.tgz",
|
||||
"integrity": "sha512-wBn/GogB4x7a2Uj7Ztt3amRaApjED+9XqfE4wyCLh88R7KV55k9vnTdCx+irI/GLOOu9tXNUGm3a4t5sTajwkQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@floating-ui/react": "^0.27.16",
|
||||
"clsx": "^2.1.1",
|
||||
@@ -2050,6 +2063,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-8.3.15.tgz",
|
||||
"integrity": "sha512-AUSnpUlzttHzJht3CJ1YWi16iy6NWRwtyWO5RLGHHsmiW05DyG0qOPKF8+R5dLHuOCnl3XOu4roI2Y1ku9U04Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"react": "^18.x || ^19.x"
|
||||
}
|
||||
@@ -2126,6 +2140,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.8.tgz",
|
||||
"integrity": "sha512-QKd1RhDXE1hf2sQDNayA9ic9jGkEgvZOf0tTkJxlBPG8ns8aS4rS8WwYURw2x5y3739p0HauUXX9WbH7UufFLw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.6",
|
||||
"@mui/core-downloads-tracker": "^7.3.8",
|
||||
@@ -2573,6 +2588,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
|
||||
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
@@ -3488,6 +3504,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-7.9.0.tgz",
|
||||
"integrity": "sha512-ggs5k+/0FUJcIgNY08aZTqpBTtbExkJMYMLSMwyucrhtWexVOEY1KJmhBsxf+E/Q15f5rbwBpj+t0t2AW2oCsQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12.16"
|
||||
}
|
||||
@@ -3577,7 +3594,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.9.tgz",
|
||||
"integrity": "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"acorn": "^8.9.0"
|
||||
}
|
||||
@@ -4369,6 +4385,7 @@
|
||||
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.10.4",
|
||||
"@babel/runtime": "^7.12.5",
|
||||
@@ -4962,6 +4979,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
|
||||
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
@@ -4972,6 +4990,7 @@
|
||||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.2.0"
|
||||
}
|
||||
@@ -5052,6 +5071,7 @@
|
||||
"integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.56.1",
|
||||
"@typescript-eslint/types": "8.56.1",
|
||||
@@ -5501,7 +5521,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.29.tgz",
|
||||
"integrity": "sha512-zcrANcrRdcLtmGZETBxWqIkoQei8HaFpZWx/GHKxx79JZsiZ8j1du0VUJtu4eJjgFvU/iKL5lRXFXksVmI+5DA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/shared": "3.5.29"
|
||||
}
|
||||
@@ -5511,7 +5530,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.29.tgz",
|
||||
"integrity": "sha512-8DpW2QfdwIWOLqtsNcds4s+QgwSaHSJY/SUe04LptianUQ/0xi6KVsu/pYVh+HO3NTVvVJjIPL2t6GdeKbS4Lg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.5.29",
|
||||
"@vue/shared": "3.5.29"
|
||||
@@ -5522,7 +5540,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.29.tgz",
|
||||
"integrity": "sha512-AHvvJEtcY9tw/uk+s/YRLSlxxQnqnAkjqvK25ZiM4CllCZWzElRAoQnCM42m9AHRLNJ6oe2kC5DCgD4AUdlvXg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.5.29",
|
||||
"@vue/runtime-core": "3.5.29",
|
||||
@@ -5535,7 +5552,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.29.tgz",
|
||||
"integrity": "sha512-G/1k6WK5MusLlbxSE2YTcqAAezS+VuwHhOvLx2KnQU7G2zCH6KIb+5Wyt6UjMq7a3qPzNEjJXs1hvAxDclQH+g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-ssr": "3.5.29",
|
||||
"@vue/shared": "3.5.29"
|
||||
@@ -5562,6 +5578,7 @@
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
|
||||
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -5813,7 +5830,6 @@
|
||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
|
||||
"integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
@@ -6100,6 +6116,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
@@ -7011,6 +7028,7 @@
|
||||
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
@@ -7473,15 +7491,15 @@
|
||||
"version": "5.6.3",
|
||||
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.3.tgz",
|
||||
"integrity": "sha512-nc7XjUU/2Lb+SvEFVGcWLiKkzfw8+qHI7zn8WYXKkLMgfGSHbgCEaR6bJpev8Cm6Rmrb19Gfd/tZvGqx9is3wg==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/devtools-protocol": {
|
||||
"version": "0.0.1566079",
|
||||
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1566079.tgz",
|
||||
"integrity": "sha512-MJfAEA1UfVhSs7fbSQOG4czavUp1ajfg6prlAN0+cmfa2zNjaIbvq8VneP7do1WAQQIvgNJWSMeP6UyI90gIlQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause"
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/dezalgo": {
|
||||
"version": "1.0.4",
|
||||
@@ -7869,6 +7887,7 @@
|
||||
"integrity": "sha512-uYixubwmqJZH+KLVYIVKY1JQt7tysXhtj21WSvjcSmU5SVNzMus1bgLe+pAt816yQ8opKfheVVoPLqvVMGejYw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.2",
|
||||
@@ -7978,8 +7997,7 @@
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
|
||||
"integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/espree": {
|
||||
"version": "11.1.1",
|
||||
@@ -8044,7 +8062,6 @@
|
||||
"resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.3.tgz",
|
||||
"integrity": "sha512-8fOS+GIGCQZl/ZIlhl59htOlms6U8NvX6ZYgYHpRU/b6tVSh3uHkOHZikl3D4cMbYM0JlpBe+p/BkZEi8J9XIQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.4.15"
|
||||
}
|
||||
@@ -8866,6 +8883,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.4"
|
||||
},
|
||||
@@ -9146,7 +9164,6 @@
|
||||
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
|
||||
"integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.6"
|
||||
}
|
||||
@@ -9322,6 +9339,7 @@
|
||||
"integrity": "sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@acemir/cssom": "^0.9.28",
|
||||
"@asamuzakjp/dom-selector": "^6.7.6",
|
||||
@@ -9883,8 +9901,7 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",
|
||||
"integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/locate-path": {
|
||||
"version": "6.0.0",
|
||||
@@ -10904,6 +10921,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -11164,6 +11182,7 @@
|
||||
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.354.0.tgz",
|
||||
"integrity": "sha512-qrpToz7mN1PmEfo+Ob4Z8euX4z2p17LA0EAtFeyod3IVnlwnu+Ybea/oxVsPiq5YAPo+p5z73FcjF2yEJ7oZnA==",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/api-logs": "^0.208.0",
|
||||
@@ -11185,6 +11204,7 @@
|
||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.28.4.tgz",
|
||||
"integrity": "sha512-uKFfOHWuSNpRFVTnljsCluEFq57OKT+0QdOiQo8XWnQ/pSvg7OpX5eNOejELXJMWy+BwM2nobz0FkvzmnpCNsQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/preact"
|
||||
@@ -11559,6 +11579,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
||||
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -11568,6 +11589,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
|
||||
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
@@ -11659,7 +11681,8 @@
|
||||
"version": "19.2.4",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.4.tgz",
|
||||
"integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/react-number-format": {
|
||||
"version": "5.4.4",
|
||||
@@ -11676,6 +11699,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
||||
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/use-sync-external-store": "^0.0.6",
|
||||
"use-sync-external-store": "^1.4.0"
|
||||
@@ -12057,7 +12081,8 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/redux-thunk": {
|
||||
"version": "3.1.0",
|
||||
@@ -12907,7 +12932,6 @@
|
||||
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz",
|
||||
"integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
@@ -13235,6 +13259,7 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -13428,6 +13453,7 @@
|
||||
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "~0.27.0",
|
||||
"get-tsconfig": "^4.7.5"
|
||||
@@ -13495,6 +13521,7 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -13745,6 +13772,7 @@
|
||||
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.27.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -13919,6 +13947,7 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -13932,6 +13961,7 @@
|
||||
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/chai": "^5.2.2",
|
||||
"@vitest/expect": "3.2.4",
|
||||
@@ -14442,8 +14472,7 @@
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz",
|
||||
"integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "3.25.76",
|
||||
|
||||
+25
-25
@@ -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",
|
||||
@@ -40,6 +40,7 @@
|
||||
"@mantine/hooks": "^8.3.1",
|
||||
"@mui/icons-material": "^7.3.2",
|
||||
"@mui/material": "^7.3.2",
|
||||
"@posthog/react": "^1.8.2",
|
||||
"@reactour/tour": "^3.8.0",
|
||||
"@stripe/react-stripe-js": "^4.0.2",
|
||||
"@stripe/stripe-js": "^7.9.0",
|
||||
@@ -63,7 +64,6 @@
|
||||
"license-report": "^6.8.0",
|
||||
"pdfjs-dist": "^5.4.149",
|
||||
"peerjs": "^1.5.5",
|
||||
"@posthog/react": "^1.8.2",
|
||||
"posthog-js": "^1.268.0",
|
||||
"qrcode.react": "^4.2.0",
|
||||
"react": "^19.1.1",
|
||||
@@ -174,8 +174,8 @@
|
||||
"postcss-preset-mantine": "^1.18.0",
|
||||
"postcss-simple-vars": "^7.0.1",
|
||||
"puppeteer": "^24.25.0",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^5.9.2",
|
||||
"tsx": "^4.19.4",
|
||||
"typescript-eslint": "^8.44.1",
|
||||
"vite": "^7.1.7",
|
||||
"vite-plugin-static-copy": "^3.1.4",
|
||||
|
||||
@@ -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"
|
||||
@@ -4475,6 +4483,7 @@ exitRedaction = "Exit Redaction Mode"
|
||||
save = "Save"
|
||||
downloadAll = "Download All"
|
||||
saveAll = "Save All"
|
||||
saveAs = "Save As"
|
||||
|
||||
[textAlign]
|
||||
left = "Left"
|
||||
@@ -6913,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"
|
||||
|
||||
@@ -142,6 +142,10 @@ welcome = "Bienvenido"
|
||||
white = "Blanco"
|
||||
WorkInProgess = "Trabajo en curso. Puede no funcionar o tener errores. ¡Por favor, informa de cualquier problema!"
|
||||
yes = "Sí"
|
||||
insufficientCredits = "Créditos insuficientes. Requeridos: {{requiredCredits}}, Disponibles: {{currentBalance}}, Faltan: {{shortfall}}"
|
||||
loadingCredits = "Comprobando créditos..."
|
||||
loadingProStatus = "Comprobando estado de suscripción..."
|
||||
noticeTopUpOrPlan = "No hay suficientes créditos, por favor recarga o actualiza tu plan"
|
||||
|
||||
[account]
|
||||
accountSettings = "Configuración de la cuenta"
|
||||
@@ -227,7 +231,7 @@ title = "Añadir imagen"
|
||||
failed = "Se produjo un error al añadir la imagen al PDF."
|
||||
|
||||
[addImage.image]
|
||||
hint = "Supports PNG, JPEG, GIF, BMP, TIFF, and SVG files. SVG files will be converted to PNG for embedding."
|
||||
hint = "Soporta archivos PNG, JPEG, GIF, BMP, TIFF y SVG. Los archivos SVG se convertirán a PNG para su incrustación."
|
||||
label = "Archivo de imagen"
|
||||
name = "Imagen"
|
||||
placeholder = "Suba una imagen"
|
||||
@@ -275,8 +279,8 @@ preview = "Selección de posición"
|
||||
previewDisclaimer = "La vista previa es aproximada. El resultado final puede variar debido a las métricas de fuentes del PDF."
|
||||
submit = "Añadir Números de Página"
|
||||
title = "Añadir Números de Página"
|
||||
zeroPad = "Zero‑pad Width (Bates Stamping)"
|
||||
zeroPadTooltip = "Zero‑pad (Bates Stamp) page numbers to this width (e.g., 3 ⇒ 001). Set 0 to disable."
|
||||
zeroPad = "Ancho de relleno de ceros (Sello Bates)"
|
||||
zeroPadTooltip = "Número de páginas con relleno de ceros (Sello Bates) para este ancho (ej., 3 ⇒ 001). Establezca 0 para desactivar."
|
||||
|
||||
[addPageNumbers.error]
|
||||
failed = "La operación de añadir números de página falló"
|
||||
@@ -345,53 +349,53 @@ title = "Cambiar permisos"
|
||||
|
||||
[AddStampRequest]
|
||||
alphabet = "Alfabeto"
|
||||
clickToExpand = "Click to expand"
|
||||
clickToExpand = "Haz clic para expandir"
|
||||
customColor = "Personalizar color de texto"
|
||||
customDateDesc = "Custom format"
|
||||
customDateDesc = "Formato personalizado"
|
||||
customMargin = "Personalizar margen"
|
||||
customPosition = "Arrastra el sello a la ubicación deseada en la ventana de vista previa."
|
||||
dateDesc = "Current date"
|
||||
datetimeDesc = "Date and time combined"
|
||||
dateTimeVars = "Date & Time"
|
||||
dynamicVariables = "Dynamic Variables"
|
||||
examples = "Examples"
|
||||
filenameDesc = "Filename without extension"
|
||||
filenameFullDesc = "Filename with extension"
|
||||
fileVars = "File Information"
|
||||
dateDesc = "Fecha actual"
|
||||
datetimeDesc = "Fecha y hora combinadas"
|
||||
dateTimeVars = "Fecha y hora"
|
||||
dynamicVariables = "Variables dinámicas"
|
||||
examples = "Ejemplos"
|
||||
filenameDesc = "Nombre de archivo sin extensión"
|
||||
filenameFullDesc = "Nombre de archivo con extensión"
|
||||
fileVars = "Información del archivo"
|
||||
fontSize = "Tamaño de fuente/imagen"
|
||||
header = "Sellar PDF"
|
||||
imageSize = "Tamaño de la imagen"
|
||||
margin = "Margen"
|
||||
metadataDesc = "From PDF document properties"
|
||||
metadataVars = "Document Metadata"
|
||||
multiLine = "multi-line"
|
||||
metadataDesc = "Desde las propiedades del documento PDF"
|
||||
metadataVars = "Metadatos del documento"
|
||||
multiLine = "multilínea"
|
||||
noStampSelected = "No se ha seleccionado ningún sello. Vuelva al Paso 1."
|
||||
opacity = "Opacidad"
|
||||
otherVars = "Other"
|
||||
otherVars = "Otros"
|
||||
overrideX = "Forzar coordenada X"
|
||||
overrideY = "Forzar coordenada Y"
|
||||
pageNumberDesc = "Current page number"
|
||||
pageVars = "Page Information"
|
||||
pageNumberDesc = "Número de página actual"
|
||||
pageVars = "Información de la página"
|
||||
position = "Posición"
|
||||
positionAndFormatting = "Posición y formato"
|
||||
preview = "Preview:"
|
||||
preview = "Vista previa:"
|
||||
quickPosition = "Selecciona una posición en la página para colocar el sello."
|
||||
rotation = "Rotación"
|
||||
selectTemplate = "Select a template..."
|
||||
selectTemplate = "Seleccione una plantilla..."
|
||||
stampImage = "Imagen de sello"
|
||||
stampSetup = "Configuración de sello"
|
||||
stampText = "Texto del sello"
|
||||
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
|
||||
stampTextDescription = "Usa las variables dinámicas a continuación. Usa @@ para el @ literal. Usa \\n para nuevas líneas."
|
||||
stampType = "Tipo de sello"
|
||||
submit = "Enviar"
|
||||
tags = "Sello, Añadir imagen, centrar imagen, Marca de agua, PDF, Incrustar, Personalizar"
|
||||
timeDesc = "Current time"
|
||||
timeDesc = "Hora actual"
|
||||
title = "Sellar PDF"
|
||||
totalPagesDesc = "Total number of pages"
|
||||
useTemplate = "Use Template"
|
||||
uuidDesc = "Short unique identifier (8 chars)"
|
||||
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
|
||||
yearMonthDayDesc = "Individual date parts"
|
||||
totalPagesDesc = "Número total de páginas"
|
||||
useTemplate = "Usar plantilla"
|
||||
uuidDesc = "Identificador único corto (8 caracteres)"
|
||||
variablesHelp = "Haz clic en cualquier variable para insertarla en el texto de tu sello. Usa @@ para el @ literal."
|
||||
yearMonthDayDesc = "Partes de la fecha individuales"
|
||||
|
||||
[AddStampRequest.error]
|
||||
failed = "Se produjo un error al añadir el sello al PDF."
|
||||
@@ -400,17 +404,17 @@ failed = "Se produjo un error al añadir el sello al PDF."
|
||||
title = "Resultados del sello"
|
||||
|
||||
[AddStampRequest.template]
|
||||
custom = "Custom"
|
||||
dateHeader = "Date Header"
|
||||
doc-info = "Document Info"
|
||||
draft = "Draft Watermark"
|
||||
draftWatermark = "Draft Watermark"
|
||||
european-date = "European Date (DD/MM/YYYY)"
|
||||
europeanDate = "European Date"
|
||||
legal-footer = "Legal Footer"
|
||||
page-numbers = "Page Numbers"
|
||||
pageNumberFooter = "Page Number Footer"
|
||||
timestamp = "Timestamp"
|
||||
custom = "Personalizado"
|
||||
dateHeader = "Encabezado de fecha"
|
||||
doc-info = "Información del documento"
|
||||
draft = "Marca de agua de borrador"
|
||||
draftWatermark = "Marca de agua de borrador"
|
||||
european-date = "Fecha europea (DD/MM/AAAA)"
|
||||
europeanDate = "Fecha europea"
|
||||
legal-footer = "Pie de página legal"
|
||||
page-numbers = "Números de página"
|
||||
pageNumberFooter = "Pie de número de página"
|
||||
timestamp = "Marca de tiempo"
|
||||
|
||||
[addText]
|
||||
applySignatures = "Aplicar texto"
|
||||
@@ -668,7 +672,7 @@ mobileScannerStretchToFit = "Ajustar para encajar"
|
||||
mobileScannerStretchToFitDesc = "Estirar las imágenes para llenar toda la página. Si se desactiva, las imágenes se centran preservando la relación de aspecto."
|
||||
pageFormatA4 = "A4 (210×297mm)"
|
||||
pageFormatKeep = "Mantener (dimensiones originales)"
|
||||
pageFormatLetter = "Letter (8.5×11in)"
|
||||
pageFormatLetter = "Carta (8.5×11in)"
|
||||
title = "Conexiones"
|
||||
unlinkedServices = "Servicios no vinculados"
|
||||
|
||||
@@ -690,7 +694,7 @@ mobileScannerStretchToFitDesc = "Estirar las imágenes para llenar toda la pági
|
||||
note = "Nota: Requiere configurar la Frontend URL. "
|
||||
pageFormatA4 = "A4 (210×297mm)"
|
||||
pageFormatKeep = "Mantener (dimensiones originales)"
|
||||
pageFormatLetter = "Letter (8.5×11in)"
|
||||
pageFormatLetter = "Carta (8.5×11in)"
|
||||
|
||||
[admin.settings.connections.oauth2]
|
||||
label = "OAuth2"
|
||||
@@ -949,16 +953,16 @@ description = "Directorio donde se generan los PDFs procesados (dejar vacío par
|
||||
label = "Directorio de carpetas finalizadas"
|
||||
|
||||
[admin.settings.general.customPaths.pipeline.pipelineDir]
|
||||
description = "Base directory for pipeline resources (leave empty for default: /pipeline)"
|
||||
label = "Pipeline Directory"
|
||||
description = "Directorio base para los recursos del pipeline (dejar vacío para el valor por defecto: /pipeline)"
|
||||
label = "Directorio del Pipeline"
|
||||
|
||||
[admin.settings.general.customPaths.pipeline.watchedFoldersDir]
|
||||
description = "Directorio donde el pipeline supervisa la llegada de PDFs (dejar vacío para el valor predeterminado: /pipeline/watchedFolders)"
|
||||
label = "Directorio de carpetas vigiladas"
|
||||
|
||||
[admin.settings.general.customPaths.pipeline.watchedFoldersDirs]
|
||||
description = "Directories where pipeline monitors for incoming PDFs (one per line or comma-separated; leave empty for default: /pipeline/watchedFolders)"
|
||||
label = "Watched Folders Directories"
|
||||
description = "Directorios donde el pipeline monitoriza los PDFs entrantes (uno por línea o separados por comas; dejar vacío para el valor por defecto: /pipeline/watchedFolders)"
|
||||
label = "Directorios de Carpetas Monitorizadas"
|
||||
|
||||
[admin.settings.general.defaultLocale]
|
||||
description = "Idioma predeterminado para nuevos usuarios (p. ej., en_US, es_ES)"
|
||||
@@ -982,7 +986,7 @@ label = "Ocultar escáner móvil"
|
||||
|
||||
[admin.settings.general.frontendUrl]
|
||||
description = "URL base para el frontend (p. ej., https://pdf.example.com). Se usa para enlaces de invitación por correo y subidas por código QR desde el móvil. Déjalo vacío para usar la URL del backend."
|
||||
label = "Frontend URL"
|
||||
label = "URL del Frontend"
|
||||
|
||||
[admin.settings.general.homeDescription]
|
||||
description = "Texto descriptivo mostrado en la página de inicio"
|
||||
@@ -1321,11 +1325,11 @@ message = "Los proveedores de autenticación OAuth2 y SAML2 se han movido al men
|
||||
title = "¿Buscas la configuración de SSO/SAML?"
|
||||
|
||||
[admin.settings.security.xFrameOptions]
|
||||
deny = "Deny (Prevents all framing)"
|
||||
description = "Controls whether the application can be embedded in iframes"
|
||||
disabled = "Disabled (No X-Frame-Options header)"
|
||||
deny = "Denegar (Impide cualquier enmarcado)"
|
||||
description = "Controla si la aplicación puede incrustarse en iframes"
|
||||
disabled = "Desactivado (Sin encabezado X-Frame-Options)"
|
||||
label = "X-Frame-Options"
|
||||
sameorigin = "Same Origin (Allow framing from same domain)"
|
||||
sameorigin = "Mismo Origen (Permite enmarcado desde el mismo dominio)"
|
||||
|
||||
[admin.settings.telegram]
|
||||
description = "Configura la conectividad del bot de Telegram, controles de acceso y comportamiento de retroalimentación."
|
||||
@@ -1347,7 +1351,7 @@ placeholder = "Añade el ID de usuario y pulsa Enter"
|
||||
|
||||
[admin.settings.telegram.botToken]
|
||||
description = "Token de API proporcionado por BotFather para tu bot de Telegram."
|
||||
label = "Bot Token"
|
||||
label = "Token del Bot"
|
||||
|
||||
[admin.settings.telegram.botUsername]
|
||||
description = "El nombre de usuario público de tu bot de Telegram."
|
||||
@@ -2441,7 +2445,7 @@ title = "Estos PDF parecen muy diferentes"
|
||||
|
||||
[compare.edited]
|
||||
label = "PDF editado"
|
||||
placeholder = "Select the edited PDF"
|
||||
placeholder = "Seleccionar el PDF editado"
|
||||
|
||||
[compare.error]
|
||||
filesMissing = "No se pueden localizar los archivos seleccionados. Vuelve a seleccionarlos."
|
||||
@@ -2574,10 +2578,22 @@ title = "Ajuste de Calidad"
|
||||
[compressPdfs]
|
||||
tags = "aplastar,pequeño,diminuto"
|
||||
|
||||
[config]
|
||||
plan = "Plan"
|
||||
|
||||
[config.account.overview]
|
||||
guestDescription = "Ha iniciado sesión como invitado. Considere actualizar su cuenta arriba."
|
||||
manageAccountPreferences = "Administre las preferencias de su cuenta"
|
||||
title = "Configuración de Cuenta"
|
||||
confirmDelete = "Eliminar Mi Cuenta"
|
||||
deleteAccount = "Eliminar Cuenta"
|
||||
deleteAccountTitle = "Eliminar Cuenta"
|
||||
deleteFailed = "Error al eliminar la cuenta."
|
||||
deleteFailedTitle = "No se puede eliminar la cuenta"
|
||||
deleteWarning = "Esta acción es permanente y no se puede deshacer. Se eliminarán todos tus datos."
|
||||
enterEmailConfirm = "Para confirmar la eliminación, introduce tu dirección de correo electrónico ({{email}}) a continuación:"
|
||||
label = "Resumen"
|
||||
signedInAs = "Sesión iniciada como"
|
||||
|
||||
[config.account.upgrade]
|
||||
description = "¡Vincule su cuenta para preservar su historial y acceder a más funciones!"
|
||||
@@ -2592,6 +2608,32 @@ socialLogin = "Actualizar con Cuenta Social"
|
||||
title = "Actualizar Cuenta de Invitado"
|
||||
upgradeButton = "Actualizar Cuenta"
|
||||
|
||||
[config.account.profilePicture]
|
||||
description = "Sube una imagen para personalizar tu cuenta."
|
||||
help = "PNG, JPG, o WebP hasta 2MB."
|
||||
remove = "Eliminar"
|
||||
sizeError = "Por favor, selecciona una imagen de menos de 2MB."
|
||||
switchedToCustom = "Cambiada a foto personalizada. Ahora puedes subir la tuya."
|
||||
title = "Foto de perfil"
|
||||
upload = "Subir"
|
||||
useCustom = "Usar foto personalizada"
|
||||
usingProvider = "Usando la foto de perfil de {{provider}}"
|
||||
|
||||
[config.account.profilePicture.cropper]
|
||||
cropError = "Error al recortar la imagen. Por favor, inténtalo de nuevo."
|
||||
invalidImage = "Archivo de imagen inválido. Por favor, selecciona un archivo PNG, JPG o WebP válido."
|
||||
processing = "Procesando recorte..."
|
||||
save = "Guardar Imagen Recortada"
|
||||
sizeErrorAfterCrop = "La imagen recortada es demasiado grande. Por favor, aleja el zoom o recorta un área más pequeña."
|
||||
title = "Recortar Foto de Perfil"
|
||||
zoom = "Zoom"
|
||||
|
||||
[config.account.security]
|
||||
changePassword = "Cambiar contraseña"
|
||||
description = "Gestiona tu contraseña y los ajustes de seguridad."
|
||||
title = "Contraseñas y Seguridad"
|
||||
update = "Actualizar contraseña"
|
||||
|
||||
[config.apiKeys]
|
||||
chartAriaLabel = "Uso de créditos: incluidos {{includedUsed}} de {{includedTotal}}, comprados {{purchasedUsed}} de {{purchasedTotal}}"
|
||||
copyKeyAriaLabel = "Copiar clave API"
|
||||
@@ -2614,6 +2656,7 @@ refreshAriaLabel = "Actualizar clave API"
|
||||
schemaLink = "Referencia del esquema de la API"
|
||||
totalCredits = "Créditos Totales"
|
||||
usage = "Incluye esta clave en el encabezado X-API-KEY en todas las solicitudes a la API."
|
||||
creditsRemaining = "Créditos Restantes"
|
||||
|
||||
[config.apiKeys.alert]
|
||||
apiKeyErrorTitle = "Error de clave de API"
|
||||
@@ -2658,8 +2701,8 @@ color = "Color"
|
||||
colorType = "Tipo de Color"
|
||||
combineImages = "Combinar Imágenes"
|
||||
combineImagesDescription = "Combinar todas las imágenes en un PDF, o crear PDFs separados para cada imagen"
|
||||
combineSvgs = "Combine SVGs into single PDF"
|
||||
combineSvgsDescription = "Combine all SVG files into one PDF with multiple pages, or create separate PDFs for each SVG"
|
||||
combineSvgs = "Combinar SVGs en un único PDF"
|
||||
combineSvgsDescription = "Combina todos los archivos SVG en un PDF con múltiples páginas, o crea PDFs separados para cada SVG"
|
||||
conversionCompleted = "Conversión completada"
|
||||
conversionResults = "Resultados de Conversión"
|
||||
convertFiles = "Convertir Archivos"
|
||||
@@ -2716,10 +2759,10 @@ selectSourceFormatFirst = "Seleccione primero un formato de origen"
|
||||
settings = "Configuración"
|
||||
single = "Individual"
|
||||
sourceFormatPlaceholder = "Formato de origen"
|
||||
strictMode = "Strict Mode"
|
||||
strictModeDesc = "Error if conversion is not perfect (uses VeraPDF verification)"
|
||||
svgPdfOptions = "SVG to PDF Options"
|
||||
svgVectorNote = "SVG files are rendered as vector graphics for crisp output at any resolution. Dimensions from the SVG determine the PDF page size."
|
||||
strictMode = "Modo Estricto"
|
||||
strictModeDesc = "Error si la conversión no es perfecta (usa verificación VeraPDF)"
|
||||
svgPdfOptions = "Opciones SVG a PDF"
|
||||
svgVectorNote = "Los archivos SVG se renderizan como gráficos vectoriales para una salida nítida en cualquier resolución. Las dimensiones del SVG determinan el tamaño de página del PDF."
|
||||
targetFormatPlaceholder = "Formato de destino"
|
||||
textRtf = "Texto/RTF"
|
||||
title = "Convertir"
|
||||
@@ -2728,6 +2771,15 @@ webOptions = "Opciones de Web a PDF"
|
||||
wordDoc = "Documento de Word"
|
||||
wordDocExt = "Documento de Word (.docx)"
|
||||
zoomLevel = "Nivel de Zoom"
|
||||
cbrToPdf = "CBR → PDF"
|
||||
cbzToPdf = "CBZ → PDF"
|
||||
ebookToPdf = "eBook → PDF"
|
||||
emlToPdf = "Correo Electrónico → PDF"
|
||||
fileToPdf = "Ofimática/Documento → PDF"
|
||||
pdfToCbr = "PDF → CBR"
|
||||
pdfToCbz = "PDF → CBZ"
|
||||
pdfToEpub = "PDF → EPUB"
|
||||
svgToPdf = "SVG → PDF"
|
||||
|
||||
[convert.ebookOptions]
|
||||
ebookOptions = "Opciones de eBook a PDF"
|
||||
@@ -3441,7 +3493,7 @@ title = "Numerar páginas"
|
||||
|
||||
[home.addPassword]
|
||||
desc = "Cifrar documento PDF con contraseña"
|
||||
tags = "encrypt,password,lock,secure,protect,security,encryption,safeguard,confidential,private,restrict access"
|
||||
tags = "encriptar,contraseña,bloquear,seguro,proteger,seguridad,encriptación,salvaguardar,confidencial,privado,restringir acceso"
|
||||
title = "Proteger con contraseña"
|
||||
|
||||
[home.addStamp]
|
||||
@@ -3505,7 +3557,7 @@ title = "Cambiar metadatos"
|
||||
|
||||
[home.changePermissions]
|
||||
desc = "Modificar restricciones y permisos del documento"
|
||||
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
|
||||
tags = "permisos,restricciones,derechos,control de acceso,permitir,denegar,impresión,copia,edición,modificar permisos,ajustes de seguridad,derechos de usuario"
|
||||
title = "Cambiar permisos"
|
||||
|
||||
[home.compare]
|
||||
@@ -3530,7 +3582,7 @@ title = "Recortar PDF"
|
||||
|
||||
[home.devAirgapped]
|
||||
desc = "Enlace a la guía de configuración aislada"
|
||||
tags = "air-gapped,offline,isolated,no internet,disconnected,secure,network isolation,standalone"
|
||||
tags = "air-gapped,offline,aislado,sin internet,desconectado,seguro,aislamiento de red,independiente"
|
||||
title = "Configuración Aislada"
|
||||
|
||||
[home.devApi]
|
||||
@@ -3545,7 +3597,7 @@ title = "Escaneo Automatizado de Carpetas"
|
||||
|
||||
[home.devSsoGuide]
|
||||
desc = "Enlace a la guía de SSO"
|
||||
tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP"
|
||||
tags = "SSO,inicio de sesión único,autenticación,SAML,OAuth,OIDC,inicio de sesión,empresarial,proveedor de identidad,IdP"
|
||||
title = "Guía de SSO"
|
||||
|
||||
[home.editTableOfContents]
|
||||
@@ -3605,7 +3657,7 @@ title = "OCR / Limpiar escaneos"
|
||||
|
||||
[home.overlay-pdfs]
|
||||
desc = "Superponer PDFs encima de otro PDF"
|
||||
tags = "overlay,combine,merge,layer,superimpose,overlay PDF,layer PDFs,combine PDFs,stack,overlay pages,background,foreground,composite"
|
||||
tags = "superponer,combinar,fusionar,capa,superposición de PDF,capas de PDF,combinar PDFs,apilar,superponer páginas,fondo,primer plano,compuesto"
|
||||
title = "Superponer PDFs"
|
||||
|
||||
[home.pageLayout]
|
||||
@@ -3620,7 +3672,7 @@ title = "Organizador"
|
||||
|
||||
[home.pdfTextEditor]
|
||||
desc = "Revise y edite exportaciones JSON de Stirling PDF con edición de texto agrupada y regeneración de PDF"
|
||||
tags = "edit text,modify text,change text,edit content,update text,rewrite,correct,amend,redline,revise,text editor,content editor"
|
||||
tags = "editar texto,modificar texto,cambiar texto,editar contenido,actualizar texto,reescribir,corregir,modificar,revisar,editor de texto,editor de contenido"
|
||||
title = "Editor de texto PDF"
|
||||
|
||||
[home.pdfToSinglePage]
|
||||
@@ -3680,7 +3732,7 @@ title = "Reparar"
|
||||
|
||||
[home.replaceColor]
|
||||
desc = "Reemplace o invierta colores en documentos PDF"
|
||||
tags = "replace color,invert color,color replacement,swap colors,change color,invert,negative,color swap,find and replace color,convert color,color change"
|
||||
tags = "reemplazar color,invertir color,reemplazo de color,intercambiar colores,cambiar color,invertir,negativo,intercambio de color,buscar y reemplazar color,convertir color,cambio de color"
|
||||
title = "Reemplazar e Invertir Color"
|
||||
|
||||
[home.rotate]
|
||||
@@ -3920,6 +3972,13 @@ username = "Nombre de usuario"
|
||||
verifyingMfa = "Verificando..."
|
||||
verifyMfa = "Verificar código"
|
||||
youAreLoggedIn = "¡Ha iniciado sesión!"
|
||||
backToSignIn = "Volver a iniciar sesión"
|
||||
passwordUpdatedSuccess = "Tu contraseña se ha actualizado correctamente."
|
||||
resetHelp = "Introduce tu correo electrónico para recibir un enlace seguro para restablecer tu contraseña. Si el enlace ha caducado, por favor solicita uno nuevo."
|
||||
resetYourPassword = "Restablecer tu contraseña"
|
||||
sendResetLink = "Enviar enlace de restablecimiento"
|
||||
subtitle = "Vuelve a iniciar sesión en Stirling PDF"
|
||||
updatePassword = "Actualizar contraseña"
|
||||
|
||||
[login.slides.edit]
|
||||
alt = "Editar PDFs"
|
||||
@@ -4199,7 +4258,7 @@ title = "Salida"
|
||||
|
||||
[onboarding]
|
||||
activeFiles = "La vista de <strong>Archivos activos</strong> muestra todos los PDFs que has cargado en la herramienta y te permite seleccionar cuáles procesar."
|
||||
allTools = "This is the <strong>All Tools</strong> panel, where you can browse and select from all available PDF tools."
|
||||
allTools = "Este es el panel de <strong>Todas las herramientas</strong>, donde puedes buscar y seleccionar entre todas las herramientas PDF disponibles."
|
||||
cropSettings = "Ahora que hemos seleccionado el archivo que queremos recortar, podemos configurar la herramienta Recortar para elegir el área a la que queremos recortar el PDF."
|
||||
fileCheckbox = "Al hacer clic en uno de los archivos lo seleccionas para procesarlo. Puedes seleccionar varios archivos para operaciones por lotes."
|
||||
fileReplacement = "El archivo modificado reemplazará automáticamente al original en el Workbench, lo que te permitirá pasarlo fácilmente por más herramientas."
|
||||
@@ -4280,6 +4339,15 @@ rightRail = "El <strong>Panel derecho</strong> contiene acciones rápidas para s
|
||||
topBar = "La barra superior te permite cambiar entre <strong>Visor</strong>, <strong>Editor de páginas</strong> y <strong>Archivos activos</strong>."
|
||||
wrapUp = "Eso es lo nuevo en V2. Abre el menú de <strong>Recorridos</strong> en cualquier momento para repetir este, el de Herramientas o el de Administración."
|
||||
|
||||
[onboarding.freeTrial]
|
||||
afterTrialWithPayment = "Tu suscripción Pro comenzará automáticamente cuando finalice la prueba."
|
||||
afterTrialWithoutPayment = "Tras finalizar tu prueba, continuarás en nuestro plan gratuito. Añade un método de pago para mantener el acceso Pro."
|
||||
body = "Tienes acceso completo a las funciones de Stirling PDF Pro durante tu prueba. Disfruta de conversiones ilimitadas, tamaños de archivo mayores y procesamiento prioritario."
|
||||
daysRemaining = "{{days}} días restantes"
|
||||
daysRemainingSingular = "{{days}} día restante"
|
||||
title = "Tu Prueba Pro de 30 Días"
|
||||
trialEnds = "La prueba finaliza el {{date}}"
|
||||
|
||||
[overlay-pdfs]
|
||||
desc = "Superponer un PDF encima de otro"
|
||||
header = "Superponer archivos PDF"
|
||||
@@ -4433,6 +4501,10 @@ exitRedaction = "Salir del modo de censura"
|
||||
save = "Guardar"
|
||||
downloadAll = "Descargar Todo"
|
||||
saveAll = "Guardar todo"
|
||||
readAloud = "Leer en Voz Alta"
|
||||
readAloudLanguage = "Idioma"
|
||||
readAloudSpeed = "Velocidad"
|
||||
selectLanguage = "Seleccionar idioma"
|
||||
|
||||
[textAlign]
|
||||
left = "Izquierda"
|
||||
@@ -4588,7 +4660,7 @@ closeLater = "Lo haré más tarde"
|
||||
continue = "Continuar"
|
||||
description = "Lo usaremos para enviar su clave de licencia y recibos."
|
||||
emailLabel = "Dirección de correo electrónico"
|
||||
emailPlaceholder = "your@email.com"
|
||||
emailPlaceholder = "tu@correo.com"
|
||||
modalTitle = "Comenzar - {{planName}}"
|
||||
title = "Introduzca su correo electrónico"
|
||||
|
||||
@@ -4980,6 +5052,9 @@ selectPlan = "Seleccionar plan"
|
||||
showComparison = "Comparar todas las funciones"
|
||||
upgrade = "Actualizar"
|
||||
withServer = "+ Plan de servidor"
|
||||
purchase = "Comprar"
|
||||
selectCredits = "Seleccionar Cantidad de Créditos"
|
||||
totalCost = "Costo Total"
|
||||
|
||||
[plan.activePlan]
|
||||
subtitle = "Detalles de tu suscripción actual"
|
||||
@@ -5069,6 +5144,30 @@ successMessage = "Tu licencia se ha activado correctamente. Ya puedes cerrar est
|
||||
name = "Team"
|
||||
siteLicense = "Licencia de sitio"
|
||||
|
||||
[plan.api]
|
||||
large = "5.000 Créditos"
|
||||
medium = "1.000 Créditos"
|
||||
small = "500 Créditos"
|
||||
xsmall = "100 Créditos"
|
||||
|
||||
[plan.apiPackages]
|
||||
subtitle = "Compra créditos de API para tus aplicaciones"
|
||||
title = "Paquetes de Créditos de API"
|
||||
|
||||
[plan.trial]
|
||||
badge = "Prueba"
|
||||
continueWithFree = "Continuar con Plan Gratuito"
|
||||
daysRemaining = "Tu prueba termina en {{days}} días"
|
||||
endDate = "Caduca: {{date}}"
|
||||
expired = "Tu Prueba ha Finalizado"
|
||||
expiredMessage = "Tu prueba Pro de 30 días ha caducado. Suscríbete a Pro para seguir accediendo a funciones premium, o continúa con nuestro plan gratuito."
|
||||
freeTierLimitations = "El plan gratuito incluye herramientas básicas de PDF con límites de uso."
|
||||
message = ""
|
||||
subscribe = "Suscribirse a Pro"
|
||||
subscribeToPro = "Suscribirse a Pro"
|
||||
subscriptionScheduled = "Suscripción programada - comienza el {{date}}"
|
||||
title = "Prueba Gratuita Activa"
|
||||
|
||||
[credits]
|
||||
enableOverageBilling = "Habilitar facturación por excedente"
|
||||
maybeLater = "Quizás más tarde"
|
||||
@@ -5206,7 +5305,7 @@ failed = "Se produjo un error al censurar el PDF."
|
||||
[redact.manual]
|
||||
apply = "Aplicar"
|
||||
applyChanges = "Aplicar Cambios"
|
||||
applyRedactions = "Apply Redactions"
|
||||
applyRedactions = "Aplicar Censuras"
|
||||
applyWarning = "⚠️ Aplicación permanente, no se puede deshacer y los datos subyacentes se eliminarán"
|
||||
boxRedaction = "Censura de dibujo de caja"
|
||||
colourPicker = "Selector de Color"
|
||||
@@ -5810,7 +5909,7 @@ trial = "Prueba"
|
||||
[settings.planBilling.tier]
|
||||
enterprise = "Enterprise"
|
||||
enterpriseDescription = "Funciones y soporte empresariales personalizados"
|
||||
free = "Free"
|
||||
free = "Gratis"
|
||||
freeDescription = "50 créditos al mes"
|
||||
team = "Team"
|
||||
teamBadge = "Team"
|
||||
@@ -5884,13 +5983,13 @@ sidebar = "Barra lateral"
|
||||
|
||||
[settings.general.updates]
|
||||
checkForUpdates = "Buscar actualizaciones"
|
||||
currentBackendVersion = "Current Backend Version"
|
||||
currentFrontendVersion = "Current Frontend Version"
|
||||
currentBackendVersion = "Versión Actual del Backend"
|
||||
currentFrontendVersion = "Versión Actual del Frontend"
|
||||
description = "Compruebe actualizaciones y vea la información de versión"
|
||||
latestVersion = "Última versión"
|
||||
serverNeedsUpdate = "El servidor debe ser actualizado por el administrador"
|
||||
title = "Actualizaciones de software"
|
||||
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
|
||||
versionMismatch = "Advertencia: Se ha detectado una discrepancia entre la versión del cliente y la versión de AppConfig. El uso de diferentes versiones puede provocar problemas de compatibilidad, errores y riesgos de seguridad. Por favor, asegúrese de que el servidor y el cliente utilizan la misma versión."
|
||||
viewDetails = "Ver detalles"
|
||||
|
||||
[settings.general.versionInfo]
|
||||
@@ -5966,7 +6065,10 @@ teams = "Equipos"
|
||||
title = "Espacio de trabajo"
|
||||
|
||||
[settings.team]
|
||||
title = "Team"
|
||||
title = "Equipo"
|
||||
|
||||
[settings.search]
|
||||
placeholder = "Buscar en las páginas de ajustes..."
|
||||
|
||||
[setup]
|
||||
description = "Empiece eligiendo cómo quiere usar Stirling PDF"
|
||||
@@ -5990,7 +6092,7 @@ subtitle = "Introduzca sus credenciales para continuar"
|
||||
title = "Iniciar sesión"
|
||||
|
||||
[setup.login.email]
|
||||
label = "Email"
|
||||
label = "Correo Electrónico"
|
||||
placeholder = "Introduzca su email"
|
||||
|
||||
[setup.login.error]
|
||||
@@ -6028,7 +6130,7 @@ title = "Inicie sesión en el servidor"
|
||||
subtitle = "Introduzca la URL de su servidor autoalojado"
|
||||
testing = "Probando la conexión..."
|
||||
title = "Conectar con el servidor"
|
||||
useLast = "Last used server: {{serverUrl}}"
|
||||
useLast = "Último servidor usado: {{serverUrl}}"
|
||||
|
||||
[setup.server.error]
|
||||
configFetch = "No se pudo obtener la configuración del servidor. Compruebe la URL e inténtelo de nuevo."
|
||||
@@ -6561,6 +6663,7 @@ showDetails = "Mostrar detalles"
|
||||
unavailable = "Desactivado por el administrador del servidor:"
|
||||
unavailableDependency = "No disponible - falta la herramienta requerida en el servidor:"
|
||||
unfavorite = "Quitar de favoritos"
|
||||
selfHostedOffline = "Requiere tu servidor Stirling-PDF (actualmente sin conexión):"
|
||||
|
||||
[toolPanel.modePrompt]
|
||||
chooseFullscreen = "Usar modo pantalla completa"
|
||||
@@ -7048,7 +7151,7 @@ team = "Equipo (opcional)"
|
||||
teamPlaceholder = "Selecciona un equipo"
|
||||
title = "Añadir miembro"
|
||||
username = "Nombre de usuario (correo)"
|
||||
usernamePlaceholder = "user@example.com"
|
||||
usernamePlaceholder = "usuario@ejemplo.com"
|
||||
usernameRequired = "Se requieren nombre de usuario y contraseña"
|
||||
|
||||
[workspace.people.authType]
|
||||
@@ -7104,7 +7207,7 @@ title = "Editar miembro"
|
||||
allFailed = "No se pudo invitar a los usuarios"
|
||||
description = "Escribe o pega correos a continuación, separados por comas. Los usuarios recibirán credenciales de inicio de sesión por correo electrónico."
|
||||
emails = "Direcciones de correo electrónico"
|
||||
emailsPlaceholder = "user1@example.com, user2@example.com"
|
||||
emailsPlaceholder = "usuario1@ejemplo.com, usuario2@ejemplo.com"
|
||||
emailsRequired = "Se requiere al menos una dirección de correo electrónico"
|
||||
error = "No se pudieron enviar las invitaciones"
|
||||
partialFailure = "Algunas invitaciones fallaron"
|
||||
@@ -7534,3 +7637,11 @@ description = "Clave de API de Google para Google Picker API de Google Cloud Con
|
||||
[provider.googledrive.appId]
|
||||
label = "ID de aplicación"
|
||||
description = "ID de aplicación de Google Drive de Google Cloud Console"
|
||||
|
||||
[selfHosted.offline]
|
||||
hideTools = "Ocultar herramientas no disponibles ▴"
|
||||
messageNoFallback = "Las herramientas no estarán disponibles hasta que tu servidor vuelva a estar en línea."
|
||||
messageWithFallback = "Alguna herramientas requieren conexión con el servidor."
|
||||
showTools = "Ver herramientas no disponibles ▾"
|
||||
title = "Tu servidor de Stirling-PDF está inaccesible"
|
||||
toolNotAvailableLocally = "Tu servidor Stirling-PDF está sin conexión y \"{{endpoint}}\" no está disponible en el backend local."
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -134,68 +134,71 @@ export default function RightRail() {
|
||||
[actions, allButtonsDisabled, disableForFullscreen, tooltipPosition, tooltipOffset]
|
||||
);
|
||||
|
||||
const handleExportAll = useCallback(async () => {
|
||||
if (currentView === 'viewer') {
|
||||
const buffer = await viewerContext?.exportActions?.saveAsCopy?.();
|
||||
if (!buffer) return;
|
||||
const fileToExport = selectedFiles.length > 0 ? selectedFiles[0] : activeFiles[0];
|
||||
if (!fileToExport) return;
|
||||
const stub = isStirlingFile(fileToExport) ? selectors.getStirlingFileStub(fileToExport.fileId) : undefined;
|
||||
try {
|
||||
const result = await downloadFile({
|
||||
data: new Blob([buffer], { type: 'application/pdf' }),
|
||||
filename: fileToExport.name,
|
||||
localPath: stub?.localFilePath,
|
||||
});
|
||||
if (!result.cancelled && stub && result.savedPath) {
|
||||
fileActions.updateStirlingFileStub(stub.id, {
|
||||
localFilePath: stub.localFilePath ?? result.savedPath,
|
||||
isDirty: false,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[RightRail] Failed to export viewer file:', error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentView === 'pageEditor') {
|
||||
pageEditorFunctions?.onExportAll?.();
|
||||
return;
|
||||
}
|
||||
|
||||
const filesToExport = selectedFiles.length > 0 ? selectedFiles : activeFiles;
|
||||
|
||||
if (filesToExport.length > 0) {
|
||||
for (const file of filesToExport) {
|
||||
const stub = isStirlingFile(file) ? selectors.getStirlingFileStub(file.fileId) : undefined;
|
||||
const handleExportAll = useCallback(
|
||||
async (forceNewFile = false) => {
|
||||
if (currentView === 'viewer') {
|
||||
const buffer = await viewerContext?.exportActions?.saveAsCopy?.();
|
||||
if (!buffer) return;
|
||||
const fileToExport = selectedFiles.length > 0 ? selectedFiles[0] : activeFiles[0];
|
||||
if (!fileToExport) return;
|
||||
const stub = isStirlingFile(fileToExport) ? selectors.getStirlingFileStub(fileToExport.fileId) : undefined;
|
||||
try {
|
||||
const result = await downloadFile({
|
||||
data: file,
|
||||
filename: file.name,
|
||||
localPath: stub?.localFilePath
|
||||
data: new Blob([buffer], { type: 'application/pdf' }),
|
||||
filename: fileToExport.name,
|
||||
localPath: forceNewFile ? undefined : stub?.localFilePath,
|
||||
});
|
||||
if (result.cancelled) continue;
|
||||
if (stub && result.savedPath) {
|
||||
if (!forceNewFile && !result.cancelled && stub && result.savedPath) {
|
||||
fileActions.updateStirlingFileStub(stub.id, {
|
||||
localFilePath: stub.localFilePath ?? result.savedPath,
|
||||
isDirty: false
|
||||
isDirty: false,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[RightRail] Failed to export file:', file.name, error);
|
||||
console.error('[RightRail] Failed to export viewer file:', error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentView === 'pageEditor') {
|
||||
pageEditorFunctions?.onExportAll?.();
|
||||
return;
|
||||
}
|
||||
|
||||
const filesToExport = selectedFiles.length > 0 ? selectedFiles : activeFiles;
|
||||
|
||||
if (filesToExport.length > 0) {
|
||||
for (const file of filesToExport) {
|
||||
const stub = isStirlingFile(file) ? selectors.getStirlingFileStub(file.fileId) : undefined;
|
||||
try {
|
||||
const result = await downloadFile({
|
||||
data: file,
|
||||
filename: file.name,
|
||||
localPath: forceNewFile ? undefined : stub?.localFilePath,
|
||||
});
|
||||
if (result.cancelled) continue;
|
||||
if (!forceNewFile && stub && result.savedPath) {
|
||||
fileActions.updateStirlingFileStub(stub.id, {
|
||||
localFilePath: stub.localFilePath ?? result.savedPath,
|
||||
isDirty: false,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[RightRail] Failed to export file:', file.name, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [
|
||||
currentView,
|
||||
selectedFiles,
|
||||
activeFiles,
|
||||
pageEditorFunctions,
|
||||
viewerContext,
|
||||
selectors,
|
||||
fileActions,
|
||||
]);
|
||||
},
|
||||
[
|
||||
currentView,
|
||||
selectedFiles,
|
||||
activeFiles,
|
||||
pageEditorFunctions,
|
||||
viewerContext,
|
||||
selectors,
|
||||
fileActions,
|
||||
]
|
||||
);
|
||||
|
||||
const downloadTooltip = useMemo(() => {
|
||||
if (currentView === 'pageEditor') {
|
||||
@@ -264,7 +267,7 @@ export default function RightRail() {
|
||||
variant="subtle"
|
||||
radius="md"
|
||||
className="right-rail-icon"
|
||||
onClick={handleExportAll}
|
||||
onClick={() => handleExportAll()}
|
||||
disabled={
|
||||
disableForFullscreen ||
|
||||
(currentView !== 'viewer' && (totalItems === 0 || allButtonsDisabled))
|
||||
@@ -276,6 +279,24 @@ export default function RightRail() {
|
||||
tooltipPosition,
|
||||
tooltipOffset
|
||||
)}
|
||||
{icons.saveAsIconName &&
|
||||
renderWithTooltip(
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
radius="md"
|
||||
className="right-rail-icon"
|
||||
onClick={() => handleExportAll(true)}
|
||||
disabled={
|
||||
disableForFullscreen ||
|
||||
(currentView !== 'viewer' && (totalItems === 0 || allButtonsDisabled))
|
||||
}
|
||||
>
|
||||
<LocalIcon icon={icons.saveAsIconName} width="1.5rem" height="1.5rem" />
|
||||
</ActionIcon>,
|
||||
t('rightRail.saveAs', 'Save As'),
|
||||
tooltipPosition,
|
||||
tooltipOffset
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="right-rail-spacer" />
|
||||
|
||||
@@ -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';
|
||||
@@ -397,16 +399,6 @@ const EmbedPdfViewerContent = ({
|
||||
event.preventDefault();
|
||||
printActions.print();
|
||||
return;
|
||||
case 'r':
|
||||
case 'R':
|
||||
// Ctrl+R: Rotate forward; Ctrl+Shift+R: Rotate backward
|
||||
event.preventDefault();
|
||||
if (event.shiftKey) {
|
||||
rotationActions.rotateBackward();
|
||||
} else {
|
||||
rotationActions.rotateForward();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -920,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
|
||||
@@ -979,6 +973,7 @@ const EmbedPdfViewerContent = ({
|
||||
(effectiveFile?.file instanceof File ? effectiveFile.file.name : undefined))
|
||||
}
|
||||
enableAnnotations={shouldEnableAnnotations}
|
||||
isSignMode={selectedTool === 'sign'}
|
||||
showBakedAnnotations={isAnnotationsVisible}
|
||||
enableRedaction={shouldEnableRedaction}
|
||||
enableFormFill={shouldEnableFormFill}
|
||||
@@ -988,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
|
||||
@@ -1053,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 { useAppConfig } from '@app/contexts/AppConfigContext';
|
||||
|
||||
interface LocalEmbedPDFProps {
|
||||
file?: File | Blob;
|
||||
@@ -78,12 +81,19 @@ 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 { config } = useAppConfig();
|
||||
const [pdfUrl, setPdfUrl] = useState<string | null>(null);
|
||||
const [, setAnnotations] = useState<Array<{id: string, pageIndex: number, rect: Rect}>>([]);
|
||||
const commentAuthorName = config?.username ?? 'Guest';
|
||||
|
||||
// Convert File to URL if needed
|
||||
useEffect(() => {
|
||||
@@ -655,6 +665,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 +709,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 +730,7 @@ export function LocalEmbedPDF({ file, url, fileName, enableAnnotations = false,
|
||||
}
|
||||
>
|
||||
{(documentId) => (
|
||||
<>
|
||||
<GlobalPointerProvider documentId={documentId}>
|
||||
<Viewport
|
||||
documentId={documentId}
|
||||
@@ -793,6 +825,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
|
||||
setTimeout(() => {
|
||||
annotationApi.setActiveTool('ink'); // Reactivate with new settings
|
||||
}, 50);
|
||||
// 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');
|
||||
}, 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,
|
||||
|
||||
@@ -2,8 +2,9 @@ import UploadIcon from '@mui/icons-material/Upload';
|
||||
import DownloadOutlinedIcon from '@mui/icons-material/DownloadOutlined';
|
||||
|
||||
/**
|
||||
* File action icons for web builds
|
||||
* Desktop builds override this with different icons
|
||||
* File action icons for web builds.
|
||||
* Desktop builds override this file via TypeScript path aliases to provide
|
||||
* different icons (e.g. Save icon instead of Download, and a Save As icon).
|
||||
*/
|
||||
export function useFileActionIcons() {
|
||||
return {
|
||||
@@ -11,5 +12,8 @@ export function useFileActionIcons() {
|
||||
download: DownloadOutlinedIcon,
|
||||
uploadIconName: 'upload' as const,
|
||||
downloadIconName: 'download' as const,
|
||||
// Web builds do not expose a Save As icon — the button is hidden when this is undefined.
|
||||
// Desktop builds override this file and return a real icon name.
|
||||
saveAsIconName: undefined as string | undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -42,6 +42,7 @@ export interface AppConfig {
|
||||
error?: string;
|
||||
isNewServer?: boolean;
|
||||
isNewUser?: boolean;
|
||||
username?: string | null;
|
||||
defaultHideUnavailableTools?: boolean;
|
||||
defaultHideUnavailableConversions?: boolean;
|
||||
hideDisabledToolsGoogleDrive?: boolean;
|
||||
|
||||
@@ -2,8 +2,9 @@ import FolderOpenOutlinedIcon from '@mui/icons-material/FolderOpenOutlined';
|
||||
import SaveOutlinedIcon from '@mui/icons-material/SaveOutlined';
|
||||
|
||||
/**
|
||||
* File action icons for desktop builds
|
||||
* Overrides core implementation with desktop-appropriate icons
|
||||
* File action icons for desktop builds.
|
||||
* Overrides the core implementation with desktop-appropriate icons.
|
||||
* The presence of `saveAsIconName` signals RightRail to show the Save As button.
|
||||
*/
|
||||
export function useFileActionIcons() {
|
||||
return {
|
||||
@@ -11,5 +12,8 @@ export function useFileActionIcons() {
|
||||
download: SaveOutlinedIcon,
|
||||
uploadIconName: 'folder-rounded' as const,
|
||||
downloadIconName: 'save-rounded' as const,
|
||||
// Returning this icon name causes RightRail to render the Save As button.
|
||||
// On desktop, downloadFile() without a localPath shows a native save dialog.
|
||||
saveAsIconName: 'save-as-rounded' as string | undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -38,12 +38,14 @@ export async function showSaveDialog(
|
||||
try {
|
||||
const { save } = await import("@tauri-apps/plugin-dialog");
|
||||
|
||||
// Derive the file type filter from the filename extension so the dialog
|
||||
// doesn't force a .pdf extension when saving non-PDF outputs (e.g. .docx).
|
||||
const ext = defaultFilename.split('.').pop()?.toLowerCase() ?? '';
|
||||
const filters = ext ? [{ name: ext.toUpperCase(), extensions: [ext] }] : [];
|
||||
|
||||
const selectedPath = await save({
|
||||
defaultPath: defaultDirectory ? `${defaultDirectory}/${defaultFilename}` : defaultFilename,
|
||||
filters: [{
|
||||
name: 'PDF',
|
||||
extensions: ['pdf']
|
||||
}],
|
||||
filters,
|
||||
title: 'Save As'
|
||||
});
|
||||
|
||||
|
||||
Vendored
BIN
Binary file not shown.
+1
-1
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
# Copyright © 2015 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -114,7 +114,6 @@ case "$( uname )" in #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH="\\\"\\\""
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
@@ -172,7 +171,6 @@ fi
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
@@ -212,7 +210,6 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
|
||||
Vendored
+1
-2
@@ -70,11 +70,10 @@ goto fail
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
|
||||
Reference in New Issue
Block a user