@semiont/react-ui 0.2.32 → 0.2.33-build.74
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/PdfAnnotationCanvas.client-7FE67FUX.css +106 -0
- package/dist/PdfAnnotationCanvas.client-7FE67FUX.css.map +1 -0
- package/dist/PdfAnnotationCanvas.client-UW5TZJL2.mjs +485 -0
- package/dist/PdfAnnotationCanvas.client-UW5TZJL2.mjs.map +1 -0
- package/dist/index.css +106 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.mts +4 -2
- package/dist/index.mjs +56 -10
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/styles/utilities/semantic-indicators.css +2 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/* src/components/pdf-annotation/PdfAnnotationCanvas.css */
|
|
2
|
+
.semiont-pdf-annotation-canvas {
|
|
3
|
+
display: flex;
|
|
4
|
+
flex-direction: column;
|
|
5
|
+
height: 100%;
|
|
6
|
+
width: 100%;
|
|
7
|
+
}
|
|
8
|
+
.semiont-pdf-annotation-canvas__loading,
|
|
9
|
+
.semiont-pdf-annotation-canvas__error {
|
|
10
|
+
display: flex;
|
|
11
|
+
align-items: center;
|
|
12
|
+
justify-content: center;
|
|
13
|
+
padding: 2rem;
|
|
14
|
+
}
|
|
15
|
+
.semiont-pdf-annotation-canvas__error {
|
|
16
|
+
color: var(--error-color);
|
|
17
|
+
}
|
|
18
|
+
[data-theme=dark] .semiont-pdf-annotation-canvas__error {
|
|
19
|
+
color: var(--error-color);
|
|
20
|
+
}
|
|
21
|
+
.semiont-pdf-annotation-canvas__container {
|
|
22
|
+
position: relative;
|
|
23
|
+
flex: 1;
|
|
24
|
+
overflow: auto;
|
|
25
|
+
display: flex;
|
|
26
|
+
justify-content: center;
|
|
27
|
+
align-items: flex-start;
|
|
28
|
+
padding: 1rem;
|
|
29
|
+
}
|
|
30
|
+
.semiont-pdf-annotation-canvas__container[data-drawing-mode=rectangle] {
|
|
31
|
+
cursor: crosshair;
|
|
32
|
+
}
|
|
33
|
+
.semiont-pdf-annotation-canvas__container[data-drawing-mode=none] {
|
|
34
|
+
cursor: default;
|
|
35
|
+
}
|
|
36
|
+
.semiont-pdf-annotation-canvas__image {
|
|
37
|
+
display: block;
|
|
38
|
+
max-width: 100%;
|
|
39
|
+
height: auto;
|
|
40
|
+
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
|
41
|
+
-webkit-user-select: none;
|
|
42
|
+
-moz-user-select: none;
|
|
43
|
+
user-select: none;
|
|
44
|
+
}
|
|
45
|
+
[data-theme=dark] .semiont-pdf-annotation-canvas__image {
|
|
46
|
+
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.3), 0 1px 2px -1px rgb(0 0 0 / 0.3);
|
|
47
|
+
}
|
|
48
|
+
.semiont-pdf-annotation-canvas__overlay-container {
|
|
49
|
+
position: absolute;
|
|
50
|
+
top: 0;
|
|
51
|
+
left: 0;
|
|
52
|
+
width: 100%;
|
|
53
|
+
height: 100%;
|
|
54
|
+
display: flex;
|
|
55
|
+
justify-content: center;
|
|
56
|
+
align-items: flex-start;
|
|
57
|
+
padding: 1rem;
|
|
58
|
+
pointer-events: none;
|
|
59
|
+
}
|
|
60
|
+
.semiont-pdf-annotation-canvas__overlay {
|
|
61
|
+
position: relative;
|
|
62
|
+
pointer-events: none;
|
|
63
|
+
}
|
|
64
|
+
.semiont-pdf-annotation-canvas__svg {
|
|
65
|
+
pointer-events: none;
|
|
66
|
+
}
|
|
67
|
+
.semiont-pdf-annotation-canvas__svg rect {
|
|
68
|
+
pointer-events: auto;
|
|
69
|
+
}
|
|
70
|
+
.semiont-pdf-annotation-canvas__controls {
|
|
71
|
+
display: flex;
|
|
72
|
+
align-items: center;
|
|
73
|
+
justify-content: center;
|
|
74
|
+
gap: 1rem;
|
|
75
|
+
padding: 1rem;
|
|
76
|
+
border-top: 1px solid var(--border-color);
|
|
77
|
+
}
|
|
78
|
+
.semiont-pdf-annotation-canvas__button {
|
|
79
|
+
padding: 0.5rem 1rem;
|
|
80
|
+
border: 1px solid var(--border-color);
|
|
81
|
+
border-radius: 4px;
|
|
82
|
+
background: transparent;
|
|
83
|
+
cursor: pointer;
|
|
84
|
+
}
|
|
85
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
86
|
+
.semiont-pdf-annotation-canvas__button {
|
|
87
|
+
transition: background-color 0.2s;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
.semiont-pdf-annotation-canvas__button:hover:not(:disabled) {
|
|
91
|
+
background-color: var(--hover-color, rgba(0, 0, 0, 0.05));
|
|
92
|
+
}
|
|
93
|
+
.semiont-pdf-annotation-canvas__button:focus-visible {
|
|
94
|
+
outline: 2px solid var(--focus-color, #0066cc);
|
|
95
|
+
outline-offset: 2px;
|
|
96
|
+
}
|
|
97
|
+
.semiont-pdf-annotation-canvas__button:disabled {
|
|
98
|
+
opacity: 0.5;
|
|
99
|
+
cursor: not-allowed;
|
|
100
|
+
}
|
|
101
|
+
.semiont-pdf-annotation-canvas__page-info {
|
|
102
|
+
font-size: 0.875rem;
|
|
103
|
+
min-width: 7rem;
|
|
104
|
+
text-align: center;
|
|
105
|
+
}
|
|
106
|
+
/*# sourceMappingURL=PdfAnnotationCanvas.client-7FE67FUX.css.map */
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/pdf-annotation/PdfAnnotationCanvas.css"],"sourcesContent":["/**\n * PDF Annotation Canvas Styles\n *\n * Provides consistent styling for PDF annotation interface,\n * following the same pattern as image annotation (SvgDrawingCanvas).\n */\n\n.semiont-pdf-annotation-canvas {\n display: flex;\n flex-direction: column;\n height: 100%;\n width: 100%;\n}\n\n.semiont-pdf-annotation-canvas__loading,\n.semiont-pdf-annotation-canvas__error {\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 2rem;\n}\n\n/* stylelint-disable semiont/accessibility -- Error indicator added by semantic-indicators.css */\n\n.semiont-pdf-annotation-canvas__error {\n color: var(--error-color);\n}\n\n[data-theme=\"dark\"] .semiont-pdf-annotation-canvas__error {\n color: var(--error-color);\n}\n\n/* stylelint-enable semiont/accessibility */\n\n.semiont-pdf-annotation-canvas__container {\n position: relative;\n flex: 1;\n overflow: auto;\n display: flex;\n justify-content: center;\n align-items: flex-start;\n padding: 1rem;\n}\n\n.semiont-pdf-annotation-canvas__container[data-drawing-mode=\"rectangle\"] {\n cursor: crosshair;\n}\n\n.semiont-pdf-annotation-canvas__container[data-drawing-mode=\"none\"] {\n cursor: default;\n}\n\n.semiont-pdf-annotation-canvas__image {\n display: block;\n max-width: 100%;\n height: auto;\n box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);\n -webkit-user-select: none;\n -moz-user-select: none;\n user-select: none;\n}\n\n[data-theme=\"dark\"] .semiont-pdf-annotation-canvas__image {\n box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.3), 0 1px 2px -1px rgb(0 0 0 / 0.3);\n}\n\n.semiont-pdf-annotation-canvas__overlay-container {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n display: flex;\n justify-content: center;\n align-items: flex-start;\n padding: 1rem;\n pointer-events: none;\n}\n\n.semiont-pdf-annotation-canvas__overlay {\n position: relative;\n pointer-events: none;\n}\n\n.semiont-pdf-annotation-canvas__svg {\n pointer-events: none;\n}\n\n.semiont-pdf-annotation-canvas__svg rect {\n pointer-events: auto;\n}\n\n.semiont-pdf-annotation-canvas__controls {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 1rem;\n padding: 1rem;\n border-top: 1px solid var(--border-color);\n}\n\n.semiont-pdf-annotation-canvas__button {\n padding: 0.5rem 1rem;\n border: 1px solid var(--border-color);\n border-radius: 4px;\n background: transparent;\n cursor: pointer;\n}\n\n@media (prefers-reduced-motion: no-preference) {\n .semiont-pdf-annotation-canvas__button {\n transition: background-color 0.2s;\n }\n}\n\n.semiont-pdf-annotation-canvas__button:hover:not(:disabled) {\n background-color: var(--hover-color, rgba(0, 0, 0, 0.05));\n}\n\n.semiont-pdf-annotation-canvas__button:focus-visible {\n outline: 2px solid var(--focus-color, #0066cc);\n outline-offset: 2px;\n}\n\n.semiont-pdf-annotation-canvas__button:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n\n.semiont-pdf-annotation-canvas__page-info {\n font-size: 0.875rem;\n min-width: 7rem;\n text-align: center;\n}\n\n/*# sourceMappingURL=src/components/pdf-annotation/PdfAnnotationCanvas.css.map */"],"mappings":";AAOA,CAAC;AACC,WAAS;AACT,kBAAgB;AAChB,UAAQ;AACR,SAAO;AACT;AAEA,CAAC;AACD,CAAC;AACC,WAAS;AACT,eAAa;AACb,mBAAiB;AACjB,WAAS;AACX;AAIA,CATC;AAUC,SAAO,IAAI;AACb;AAEA,CAAC,iBAAmB,CAbnB;AAcC,SAAO,IAAI;AACb;AAIA,CAAC;AACC,YAAU;AACV,QAAM;AACN,YAAU;AACV,WAAS;AACT,mBAAiB;AACjB,eAAa;AACb,WAAS;AACX;AAEA,CAVC,wCAUwC,CAAC;AACxC,UAAQ;AACV;AAEA,CAdC,wCAcwC,CAAC;AACxC,UAAQ;AACV;AAEA,CAAC;AACC,WAAS;AACT,aAAW;AACX,UAAQ;AACR,cAAY,EAAE,IAAI,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,IAAI,IAAI,KAAK,IAAI,EAAE,EAAE,EAAE,EAAE;AACrE,uBAAqB;AAClB,oBAAkB;AACb,eAAa;AACvB;AAEA,CAAC,iBAAmB,CAVnB;AAWC,cAAY,EAAE,IAAI,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,IAAI,IAAI,KAAK,IAAI,EAAE,EAAE,EAAE,EAAE;AACvE;AAEA,CAAC;AACC,YAAU;AACV,OAAK;AACL,QAAM;AACN,SAAO;AACP,UAAQ;AACR,WAAS;AACT,mBAAiB;AACjB,eAAa;AACb,WAAS;AACT,kBAAgB;AAClB;AAEA,CAAC;AACC,YAAU;AACV,kBAAgB;AAClB;AAEA,CAAC;AACC,kBAAgB;AAClB;AAEA,CAJC,mCAImC;AAClC,kBAAgB;AAClB;AAEA,CAAC;AACC,WAAS;AACT,eAAa;AACb,mBAAiB;AACjB,OAAK;AACL,WAAS;AACT,cAAY,IAAI,MAAM,IAAI;AAC5B;AAEA,CAAC;AACC,WAAS,OAAO;AAChB,UAAQ,IAAI,MAAM,IAAI;AACtB,iBAAe;AACf,cAAY;AACZ,UAAQ;AACV;AAEA,QAAO,wBAAyB;AAC9B,GATD;AAUG,gBAAY,iBAAiB;AAC/B;AACF;AAEA,CAdC,qCAcqC,MAAM,KAAK;AAC/C,oBAAkB,IAAI,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE;AACrD;AAEA,CAlBC,qCAkBqC;AACpC,WAAS,IAAI,MAAM,IAAI,aAAa,EAAE;AACtC,kBAAgB;AAClB;AAEA,CAvBC,qCAuBqC;AACpC,WAAS;AACT,UAAQ;AACV;AAEA,CAAC;AACC,aAAW;AACX,aAAW;AACX,cAAY;AACd;","names":[]}
|
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
"use client";
|
|
3
|
+
import "./chunk-3JTO27MH.mjs";
|
|
4
|
+
|
|
5
|
+
// src/components/pdf-annotation/PdfAnnotationCanvas.tsx
|
|
6
|
+
import { useRef, useState, useCallback, useEffect } from "react";
|
|
7
|
+
import { getTargetSelector } from "@semiont/api-client";
|
|
8
|
+
|
|
9
|
+
// src/lib/pdf-coordinates.ts
|
|
10
|
+
function canvasToPdfCoordinates(canvasRect, page, pageWidth, pageHeight, scale = 1) {
|
|
11
|
+
const x1 = Math.min(canvasRect.startX, canvasRect.endX);
|
|
12
|
+
const y1 = Math.min(canvasRect.startY, canvasRect.endY);
|
|
13
|
+
const x2 = Math.max(canvasRect.startX, canvasRect.endX);
|
|
14
|
+
const y2 = Math.max(canvasRect.startY, canvasRect.endY);
|
|
15
|
+
const pdfX = x1 / scale;
|
|
16
|
+
const pdfWidth = (x2 - x1) / scale;
|
|
17
|
+
const canvasHeight = pageHeight * scale;
|
|
18
|
+
const pdfY = pageHeight - y2 / scale;
|
|
19
|
+
const pdfHeight = (y2 - y1) / scale;
|
|
20
|
+
return {
|
|
21
|
+
page,
|
|
22
|
+
x: Math.round(pdfX),
|
|
23
|
+
y: Math.round(pdfY),
|
|
24
|
+
width: Math.round(pdfWidth),
|
|
25
|
+
height: Math.round(pdfHeight)
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function pdfToCanvasCoordinates(pdfCoord, pageHeight, scale = 1) {
|
|
29
|
+
const canvasX = pdfCoord.x * scale;
|
|
30
|
+
const canvasWidth = pdfCoord.width * scale;
|
|
31
|
+
const canvasY = (pageHeight - pdfCoord.y - pdfCoord.height) * scale;
|
|
32
|
+
const canvasHeight = pdfCoord.height * scale;
|
|
33
|
+
return {
|
|
34
|
+
x: canvasX,
|
|
35
|
+
y: canvasY,
|
|
36
|
+
width: canvasWidth,
|
|
37
|
+
height: canvasHeight
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function createFragmentSelector(coord) {
|
|
41
|
+
return `page=${coord.page}&viewrect=${coord.x},${coord.y},${coord.width},${coord.height}`;
|
|
42
|
+
}
|
|
43
|
+
function parseFragmentSelector(fragment) {
|
|
44
|
+
try {
|
|
45
|
+
const pageMatch = fragment.match(/page=(\d+)/);
|
|
46
|
+
if (!pageMatch) return null;
|
|
47
|
+
const page = parseInt(pageMatch[1], 10);
|
|
48
|
+
const viewrectMatch = fragment.match(/viewrect=([\d.]+),([\d.]+),([\d.]+),([\d.]+)/);
|
|
49
|
+
if (!viewrectMatch) return null;
|
|
50
|
+
return {
|
|
51
|
+
page,
|
|
52
|
+
x: parseFloat(viewrectMatch[1]),
|
|
53
|
+
y: parseFloat(viewrectMatch[2]),
|
|
54
|
+
width: parseFloat(viewrectMatch[3]),
|
|
55
|
+
height: parseFloat(viewrectMatch[4])
|
|
56
|
+
};
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function getPageFromFragment(fragment) {
|
|
62
|
+
const match = fragment.match(/page=(\d+)/);
|
|
63
|
+
return match ? parseInt(match[1], 10) : null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// src/lib/browser-pdfjs.ts
|
|
67
|
+
async function ensurePdfJs() {
|
|
68
|
+
if (typeof window !== "undefined" && window.pdfjsLib) {
|
|
69
|
+
return window.pdfjsLib;
|
|
70
|
+
}
|
|
71
|
+
return new Promise((resolve, reject) => {
|
|
72
|
+
const script = document.createElement("script");
|
|
73
|
+
script.src = "/pdfjs/pdf.min.mjs";
|
|
74
|
+
script.type = "module";
|
|
75
|
+
script.onload = () => {
|
|
76
|
+
const pdfjsLib = window.pdfjsLib;
|
|
77
|
+
if (!pdfjsLib) {
|
|
78
|
+
reject(new Error("PDF.js loaded but pdfjsLib not available"));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
pdfjsLib.GlobalWorkerOptions.workerSrc = "/pdfjs/pdf.worker.min.mjs";
|
|
82
|
+
resolve(pdfjsLib);
|
|
83
|
+
};
|
|
84
|
+
script.onerror = () => {
|
|
85
|
+
reject(new Error("Failed to load PDF.js from /pdfjs/pdf.min.mjs"));
|
|
86
|
+
};
|
|
87
|
+
document.head.appendChild(script);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
async function loadPdfDocument(source) {
|
|
91
|
+
const pdfjsLib = await ensurePdfJs();
|
|
92
|
+
if (typeof source === "string") {
|
|
93
|
+
const response = await fetch(source, {
|
|
94
|
+
credentials: "include",
|
|
95
|
+
headers: {
|
|
96
|
+
"Accept": "application/pdf"
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
if (!response.ok) {
|
|
100
|
+
throw new Error(`Failed to fetch PDF: ${response.status} ${response.statusText}`);
|
|
101
|
+
}
|
|
102
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
103
|
+
const loadingTask = pdfjsLib.getDocument({ data: arrayBuffer });
|
|
104
|
+
return loadingTask.promise;
|
|
105
|
+
} else {
|
|
106
|
+
const loadingTask = pdfjsLib.getDocument({ data: source });
|
|
107
|
+
return loadingTask.promise;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async function renderPdfPageToDataUrl(page, scale = 1) {
|
|
111
|
+
const viewport = page.getViewport({ scale });
|
|
112
|
+
const canvas = document.createElement("canvas");
|
|
113
|
+
const context = canvas.getContext("2d");
|
|
114
|
+
if (!context) {
|
|
115
|
+
throw new Error("Failed to get 2D context");
|
|
116
|
+
}
|
|
117
|
+
canvas.width = viewport.width;
|
|
118
|
+
canvas.height = viewport.height;
|
|
119
|
+
const renderTask = page.render({
|
|
120
|
+
canvasContext: context,
|
|
121
|
+
viewport
|
|
122
|
+
});
|
|
123
|
+
await renderTask.promise;
|
|
124
|
+
return {
|
|
125
|
+
dataUrl: canvas.toDataURL("image/png"),
|
|
126
|
+
width: viewport.width,
|
|
127
|
+
height: viewport.height
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// src/components/pdf-annotation/PdfAnnotationCanvas.tsx
|
|
132
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
133
|
+
function getMotivationColor(motivation) {
|
|
134
|
+
if (!motivation) {
|
|
135
|
+
return { stroke: "rgb(156, 163, 175)", fill: "rgba(156, 163, 175, 0.2)" };
|
|
136
|
+
}
|
|
137
|
+
switch (motivation) {
|
|
138
|
+
case "highlighting":
|
|
139
|
+
return { stroke: "rgb(250, 204, 21)", fill: "rgba(250, 204, 21, 0.3)" };
|
|
140
|
+
case "linking":
|
|
141
|
+
return { stroke: "rgb(59, 130, 246)", fill: "rgba(59, 130, 246, 0.2)" };
|
|
142
|
+
case "assessing":
|
|
143
|
+
return { stroke: "rgb(239, 68, 68)", fill: "rgba(239, 68, 68, 0.2)" };
|
|
144
|
+
case "commenting":
|
|
145
|
+
return { stroke: "rgb(255, 255, 255)", fill: "rgba(255, 255, 255, 0.2)" };
|
|
146
|
+
default:
|
|
147
|
+
return { stroke: "rgb(156, 163, 175)", fill: "rgba(156, 163, 175, 0.2)" };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function PdfAnnotationCanvas({
|
|
151
|
+
resourceUri,
|
|
152
|
+
existingAnnotations = [],
|
|
153
|
+
drawingMode,
|
|
154
|
+
selectedMotivation,
|
|
155
|
+
onAnnotationCreate,
|
|
156
|
+
onAnnotationClick,
|
|
157
|
+
onAnnotationHover,
|
|
158
|
+
hoveredAnnotationId,
|
|
159
|
+
selectedAnnotationId
|
|
160
|
+
}) {
|
|
161
|
+
const resourceId = resourceUri.split("/").pop();
|
|
162
|
+
const pdfUrl = `/api/resources/${resourceId}`;
|
|
163
|
+
const [pdfDoc, setPdfDoc] = useState(null);
|
|
164
|
+
const [numPages, setNumPages] = useState(0);
|
|
165
|
+
const [pageNumber, setPageNumber] = useState(1);
|
|
166
|
+
const [currentPage, setCurrentPage] = useState(null);
|
|
167
|
+
const [pageImageUrl, setPageImageUrl] = useState(null);
|
|
168
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
169
|
+
const [error, setError] = useState(null);
|
|
170
|
+
const [pageDimensions, setPageDimensions] = useState(null);
|
|
171
|
+
const [displayDimensions, setDisplayDimensions] = useState(null);
|
|
172
|
+
const [scale] = useState(1.5);
|
|
173
|
+
const [isDrawing, setIsDrawing] = useState(false);
|
|
174
|
+
const [selection, setSelection] = useState(null);
|
|
175
|
+
const containerRef = useRef(null);
|
|
176
|
+
const imageRef = useRef(null);
|
|
177
|
+
useEffect(() => {
|
|
178
|
+
let cancelled = false;
|
|
179
|
+
async function loadPdf() {
|
|
180
|
+
try {
|
|
181
|
+
setIsLoading(true);
|
|
182
|
+
setError(null);
|
|
183
|
+
const doc = await loadPdfDocument(pdfUrl);
|
|
184
|
+
if (cancelled) return;
|
|
185
|
+
setPdfDoc(doc);
|
|
186
|
+
setNumPages(doc.numPages);
|
|
187
|
+
setIsLoading(false);
|
|
188
|
+
} catch (err) {
|
|
189
|
+
if (cancelled) return;
|
|
190
|
+
console.error("Error loading PDF:", err);
|
|
191
|
+
setError("Failed to load PDF");
|
|
192
|
+
setIsLoading(false);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
loadPdf();
|
|
196
|
+
return () => {
|
|
197
|
+
cancelled = true;
|
|
198
|
+
};
|
|
199
|
+
}, [pdfUrl]);
|
|
200
|
+
useEffect(() => {
|
|
201
|
+
if (!pdfDoc) return;
|
|
202
|
+
let cancelled = false;
|
|
203
|
+
const doc = pdfDoc;
|
|
204
|
+
async function loadPage() {
|
|
205
|
+
try {
|
|
206
|
+
const page = await doc.getPage(pageNumber);
|
|
207
|
+
if (cancelled) return;
|
|
208
|
+
setCurrentPage(page);
|
|
209
|
+
const viewport = page.getViewport({ scale: 1 });
|
|
210
|
+
setPageDimensions({
|
|
211
|
+
width: viewport.width,
|
|
212
|
+
height: viewport.height
|
|
213
|
+
});
|
|
214
|
+
const { dataUrl } = await renderPdfPageToDataUrl(page, scale);
|
|
215
|
+
if (cancelled) return;
|
|
216
|
+
setPageImageUrl(dataUrl);
|
|
217
|
+
} catch (err) {
|
|
218
|
+
if (cancelled) return;
|
|
219
|
+
console.error("Error loading page:", err);
|
|
220
|
+
setError("Failed to load page");
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
loadPage();
|
|
224
|
+
return () => {
|
|
225
|
+
cancelled = true;
|
|
226
|
+
};
|
|
227
|
+
}, [pdfDoc, pageNumber, scale]);
|
|
228
|
+
useEffect(() => {
|
|
229
|
+
const updateDisplayDimensions = () => {
|
|
230
|
+
if (imageRef.current) {
|
|
231
|
+
setDisplayDimensions({
|
|
232
|
+
width: imageRef.current.clientWidth,
|
|
233
|
+
height: imageRef.current.clientHeight
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
updateDisplayDimensions();
|
|
238
|
+
let resizeObserver = null;
|
|
239
|
+
try {
|
|
240
|
+
resizeObserver = new ResizeObserver(updateDisplayDimensions);
|
|
241
|
+
if (imageRef.current) {
|
|
242
|
+
resizeObserver.observe(imageRef.current);
|
|
243
|
+
}
|
|
244
|
+
} catch (error2) {
|
|
245
|
+
console.warn("ResizeObserver not supported, falling back to window resize listener");
|
|
246
|
+
window.addEventListener("resize", updateDisplayDimensions);
|
|
247
|
+
}
|
|
248
|
+
return () => {
|
|
249
|
+
if (resizeObserver) {
|
|
250
|
+
resizeObserver.disconnect();
|
|
251
|
+
} else {
|
|
252
|
+
window.removeEventListener("resize", updateDisplayDimensions);
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
}, [pageImageUrl]);
|
|
256
|
+
const handleMouseDown = useCallback((e) => {
|
|
257
|
+
if (!drawingMode || drawingMode !== "rectangle") return;
|
|
258
|
+
if (!imageRef.current) return;
|
|
259
|
+
const rect = imageRef.current.getBoundingClientRect();
|
|
260
|
+
const x = e.clientX - rect.left;
|
|
261
|
+
const y = e.clientY - rect.top;
|
|
262
|
+
setIsDrawing(true);
|
|
263
|
+
setSelection({
|
|
264
|
+
startX: x,
|
|
265
|
+
startY: y,
|
|
266
|
+
endX: x,
|
|
267
|
+
endY: y
|
|
268
|
+
});
|
|
269
|
+
}, [drawingMode]);
|
|
270
|
+
const handleMouseMove = useCallback((e) => {
|
|
271
|
+
if (!isDrawing || !selection || !imageRef.current) return;
|
|
272
|
+
const rect = imageRef.current.getBoundingClientRect();
|
|
273
|
+
setSelection({
|
|
274
|
+
...selection,
|
|
275
|
+
endX: e.clientX - rect.left,
|
|
276
|
+
endY: e.clientY - rect.top
|
|
277
|
+
});
|
|
278
|
+
}, [isDrawing, selection]);
|
|
279
|
+
const handleMouseUp = useCallback(() => {
|
|
280
|
+
if (!isDrawing || !selection || !pageDimensions || !displayDimensions || !onAnnotationCreate) {
|
|
281
|
+
setIsDrawing(false);
|
|
282
|
+
setSelection(null);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
const dragDistance = Math.sqrt(
|
|
286
|
+
Math.pow(selection.endX - selection.startX, 2) + Math.pow(selection.endY - selection.startY, 2)
|
|
287
|
+
);
|
|
288
|
+
const MIN_DRAG_DISTANCE = 10;
|
|
289
|
+
if (dragDistance < MIN_DRAG_DISTANCE) {
|
|
290
|
+
if (onAnnotationClick && existingAnnotations.length > 0) {
|
|
291
|
+
const clickedAnnotation = pageAnnotations.find((ann) => {
|
|
292
|
+
const fragmentSel = getFragmentSelector(ann.target);
|
|
293
|
+
if (!fragmentSel) return false;
|
|
294
|
+
const pdfCoord2 = parseFragmentSelector(fragmentSel.value);
|
|
295
|
+
if (!pdfCoord2) return false;
|
|
296
|
+
const rect = pdfToCanvasCoordinates(pdfCoord2, pageDimensions.height, 1);
|
|
297
|
+
const scaleX2 = displayDimensions.width / pageDimensions.width;
|
|
298
|
+
const scaleY2 = displayDimensions.height / pageDimensions.height;
|
|
299
|
+
const displayX = rect.x * scaleX2;
|
|
300
|
+
const displayY = rect.y * scaleY2;
|
|
301
|
+
const displayWidth = rect.width * scaleX2;
|
|
302
|
+
const displayHeight = rect.height * scaleY2;
|
|
303
|
+
return selection.endX >= displayX && selection.endX <= displayX + displayWidth && selection.endY >= displayY && selection.endY <= displayY + displayHeight;
|
|
304
|
+
});
|
|
305
|
+
if (clickedAnnotation) {
|
|
306
|
+
onAnnotationClick(clickedAnnotation);
|
|
307
|
+
setIsDrawing(false);
|
|
308
|
+
setSelection(null);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
setIsDrawing(false);
|
|
313
|
+
setSelection(null);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
const scaleX = pageDimensions.width / displayDimensions.width;
|
|
317
|
+
const scaleY = pageDimensions.height / displayDimensions.height;
|
|
318
|
+
const nativeSelection = {
|
|
319
|
+
startX: selection.startX * scaleX,
|
|
320
|
+
startY: selection.startY * scaleY,
|
|
321
|
+
endX: selection.endX * scaleX,
|
|
322
|
+
endY: selection.endY * scaleY
|
|
323
|
+
};
|
|
324
|
+
const pdfCoord = canvasToPdfCoordinates(
|
|
325
|
+
nativeSelection,
|
|
326
|
+
pageNumber,
|
|
327
|
+
pageDimensions.width,
|
|
328
|
+
pageDimensions.height,
|
|
329
|
+
1
|
|
330
|
+
// Use scale 1.0 since we already scaled to native coords
|
|
331
|
+
);
|
|
332
|
+
const fragmentSelector = createFragmentSelector(pdfCoord);
|
|
333
|
+
onAnnotationCreate(fragmentSelector);
|
|
334
|
+
setIsDrawing(false);
|
|
335
|
+
setSelection(null);
|
|
336
|
+
}, [isDrawing, selection, pageNumber, pageDimensions, displayDimensions, onAnnotationCreate, onAnnotationClick, existingAnnotations]);
|
|
337
|
+
const getFragmentSelector = (target) => {
|
|
338
|
+
const selector = getTargetSelector(target);
|
|
339
|
+
if (!selector) return null;
|
|
340
|
+
const selectors = Array.isArray(selector) ? selector : [selector];
|
|
341
|
+
const found = selectors.find((s) => s.type === "FragmentSelector");
|
|
342
|
+
if (!found || found.type !== "FragmentSelector") return null;
|
|
343
|
+
return found;
|
|
344
|
+
};
|
|
345
|
+
const pageAnnotations = existingAnnotations.filter((ann) => {
|
|
346
|
+
const fragmentSel = getFragmentSelector(ann.target);
|
|
347
|
+
if (!fragmentSel) return false;
|
|
348
|
+
const page = getPageFromFragment(fragmentSel.value);
|
|
349
|
+
return page === pageNumber;
|
|
350
|
+
});
|
|
351
|
+
const { stroke, fill } = getMotivationColor(selectedMotivation ?? null);
|
|
352
|
+
if (error) {
|
|
353
|
+
return /* @__PURE__ */ jsx("div", { className: "semiont-pdf-annotation-canvas__error", children: error });
|
|
354
|
+
}
|
|
355
|
+
return /* @__PURE__ */ jsxs("div", { className: "semiont-pdf-annotation-canvas", children: [
|
|
356
|
+
isLoading && /* @__PURE__ */ jsx("div", { className: "semiont-pdf-annotation-canvas__loading", children: "Loading PDF..." }),
|
|
357
|
+
/* @__PURE__ */ jsxs(
|
|
358
|
+
"div",
|
|
359
|
+
{
|
|
360
|
+
ref: containerRef,
|
|
361
|
+
className: "semiont-pdf-annotation-canvas__container",
|
|
362
|
+
onMouseDown: handleMouseDown,
|
|
363
|
+
onMouseMove: handleMouseMove,
|
|
364
|
+
onMouseUp: handleMouseUp,
|
|
365
|
+
onMouseLeave: () => {
|
|
366
|
+
if (isDrawing) {
|
|
367
|
+
setIsDrawing(false);
|
|
368
|
+
setSelection(null);
|
|
369
|
+
}
|
|
370
|
+
},
|
|
371
|
+
"data-drawing-mode": drawingMode || "none",
|
|
372
|
+
children: [
|
|
373
|
+
pageImageUrl && /* @__PURE__ */ jsx(
|
|
374
|
+
"img",
|
|
375
|
+
{
|
|
376
|
+
ref: imageRef,
|
|
377
|
+
src: pageImageUrl,
|
|
378
|
+
alt: `PDF page ${pageNumber}`,
|
|
379
|
+
className: "semiont-pdf-annotation-canvas__image",
|
|
380
|
+
draggable: false
|
|
381
|
+
}
|
|
382
|
+
),
|
|
383
|
+
displayDimensions && pageDimensions && /* @__PURE__ */ jsx("div", { className: "semiont-pdf-annotation-canvas__overlay-container", children: /* @__PURE__ */ jsx(
|
|
384
|
+
"div",
|
|
385
|
+
{
|
|
386
|
+
className: "semiont-pdf-annotation-canvas__overlay",
|
|
387
|
+
style: {
|
|
388
|
+
width: displayDimensions.width,
|
|
389
|
+
height: displayDimensions.height
|
|
390
|
+
},
|
|
391
|
+
children: /* @__PURE__ */ jsxs(
|
|
392
|
+
"svg",
|
|
393
|
+
{
|
|
394
|
+
className: "semiont-pdf-annotation-canvas__svg",
|
|
395
|
+
style: {
|
|
396
|
+
width: displayDimensions.width,
|
|
397
|
+
height: displayDimensions.height
|
|
398
|
+
},
|
|
399
|
+
children: [
|
|
400
|
+
pageAnnotations.map((ann) => {
|
|
401
|
+
const fragmentSel = getFragmentSelector(ann.target);
|
|
402
|
+
if (!fragmentSel) return null;
|
|
403
|
+
const pdfCoord = parseFragmentSelector(fragmentSel.value);
|
|
404
|
+
if (!pdfCoord) return null;
|
|
405
|
+
const rect = pdfToCanvasCoordinates(pdfCoord, pageDimensions.height, 1);
|
|
406
|
+
const scaleX = displayDimensions.width / pageDimensions.width;
|
|
407
|
+
const scaleY = displayDimensions.height / pageDimensions.height;
|
|
408
|
+
const isHovered = ann.id === hoveredAnnotationId;
|
|
409
|
+
const isSelected = ann.id === selectedAnnotationId;
|
|
410
|
+
return /* @__PURE__ */ jsx(
|
|
411
|
+
"rect",
|
|
412
|
+
{
|
|
413
|
+
x: rect.x * scaleX,
|
|
414
|
+
y: rect.y * scaleY,
|
|
415
|
+
width: rect.width * scaleX,
|
|
416
|
+
height: rect.height * scaleY,
|
|
417
|
+
stroke,
|
|
418
|
+
strokeWidth: isSelected ? 4 : isHovered ? 3 : 2,
|
|
419
|
+
fill,
|
|
420
|
+
style: {
|
|
421
|
+
pointerEvents: "auto",
|
|
422
|
+
cursor: "pointer",
|
|
423
|
+
opacity: isSelected ? 1 : isHovered ? 0.9 : 0.7
|
|
424
|
+
},
|
|
425
|
+
onClick: () => onAnnotationClick?.(ann),
|
|
426
|
+
onMouseEnter: () => onAnnotationHover?.(ann.id),
|
|
427
|
+
onMouseLeave: () => onAnnotationHover?.(null)
|
|
428
|
+
},
|
|
429
|
+
ann.id
|
|
430
|
+
);
|
|
431
|
+
}),
|
|
432
|
+
selection && isDrawing && /* @__PURE__ */ jsx(
|
|
433
|
+
"rect",
|
|
434
|
+
{
|
|
435
|
+
x: Math.min(selection.startX, selection.endX),
|
|
436
|
+
y: Math.min(selection.startY, selection.endY),
|
|
437
|
+
width: Math.abs(selection.endX - selection.startX),
|
|
438
|
+
height: Math.abs(selection.endY - selection.startY),
|
|
439
|
+
stroke,
|
|
440
|
+
strokeWidth: 2,
|
|
441
|
+
strokeDasharray: "5,5",
|
|
442
|
+
fill,
|
|
443
|
+
pointerEvents: "none"
|
|
444
|
+
}
|
|
445
|
+
)
|
|
446
|
+
]
|
|
447
|
+
}
|
|
448
|
+
)
|
|
449
|
+
}
|
|
450
|
+
) })
|
|
451
|
+
]
|
|
452
|
+
}
|
|
453
|
+
),
|
|
454
|
+
numPages > 0 && /* @__PURE__ */ jsxs("div", { className: "semiont-pdf-annotation-canvas__controls", children: [
|
|
455
|
+
/* @__PURE__ */ jsx(
|
|
456
|
+
"button",
|
|
457
|
+
{
|
|
458
|
+
disabled: pageNumber <= 1,
|
|
459
|
+
onClick: () => setPageNumber(pageNumber - 1),
|
|
460
|
+
className: "semiont-pdf-annotation-canvas__button",
|
|
461
|
+
children: "Previous"
|
|
462
|
+
}
|
|
463
|
+
),
|
|
464
|
+
/* @__PURE__ */ jsxs("span", { className: "semiont-pdf-annotation-canvas__page-info", children: [
|
|
465
|
+
"Page ",
|
|
466
|
+
pageNumber,
|
|
467
|
+
" of ",
|
|
468
|
+
numPages
|
|
469
|
+
] }),
|
|
470
|
+
/* @__PURE__ */ jsx(
|
|
471
|
+
"button",
|
|
472
|
+
{
|
|
473
|
+
disabled: pageNumber >= numPages,
|
|
474
|
+
onClick: () => setPageNumber(pageNumber + 1),
|
|
475
|
+
className: "semiont-pdf-annotation-canvas__button",
|
|
476
|
+
children: "Next"
|
|
477
|
+
}
|
|
478
|
+
)
|
|
479
|
+
] })
|
|
480
|
+
] });
|
|
481
|
+
}
|
|
482
|
+
export {
|
|
483
|
+
PdfAnnotationCanvas
|
|
484
|
+
};
|
|
485
|
+
//# sourceMappingURL=PdfAnnotationCanvas.client-UW5TZJL2.mjs.map
|