@papyrus-sdk/ui-react 0.2.8 → 0.2.9
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/base.css +24 -0
- package/dist/index.d.mts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +651 -45
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +652 -46
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -28,16 +28,31 @@ var Topbar = ({
|
|
|
28
28
|
triggerScrollToPage
|
|
29
29
|
} = useViewerStore();
|
|
30
30
|
const fileInputRef = useRef(null);
|
|
31
|
+
const zoomTimerRef = useRef(null);
|
|
32
|
+
const pendingZoomRef = useRef(null);
|
|
31
33
|
const [pageInput, setPageInput] = useState(currentPage.toString());
|
|
34
|
+
const pageDigits = Math.max(2, String(pageCount || 1).length);
|
|
32
35
|
const [showPageThemes, setShowPageThemes] = useState(false);
|
|
33
36
|
const isDark = uiTheme === "dark";
|
|
34
37
|
useEffect(() => {
|
|
35
38
|
setPageInput(currentPage.toString());
|
|
36
39
|
}, [currentPage]);
|
|
40
|
+
useEffect(() => () => {
|
|
41
|
+
if (zoomTimerRef.current) clearTimeout(zoomTimerRef.current);
|
|
42
|
+
}, []);
|
|
37
43
|
const handleZoom = (delta) => {
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
44
|
+
const baseZoom = pendingZoomRef.current ?? zoom;
|
|
45
|
+
const nextZoom = Math.max(0.2, Math.min(5, baseZoom + delta));
|
|
46
|
+
pendingZoomRef.current = nextZoom;
|
|
47
|
+
if (zoomTimerRef.current) return;
|
|
48
|
+
zoomTimerRef.current = setTimeout(() => {
|
|
49
|
+
zoomTimerRef.current = null;
|
|
50
|
+
const targetZoom = pendingZoomRef.current;
|
|
51
|
+
pendingZoomRef.current = null;
|
|
52
|
+
if (targetZoom == null) return;
|
|
53
|
+
engine.setZoom(targetZoom);
|
|
54
|
+
setDocumentState({ zoom: targetZoom });
|
|
55
|
+
}, 80);
|
|
41
56
|
};
|
|
42
57
|
const handlePageChange = (page) => {
|
|
43
58
|
if (pageCount <= 0) return;
|
|
@@ -73,7 +88,8 @@ var Topbar = ({
|
|
|
73
88
|
"input",
|
|
74
89
|
{
|
|
75
90
|
type: "text",
|
|
76
|
-
className: "papyrus-input
|
|
91
|
+
className: "papyrus-input text-center bg-transparent focus:outline-none font-bold text-sm shrink-0",
|
|
92
|
+
style: { width: `${pageDigits + 1.75}ch` },
|
|
77
93
|
value: pageInput,
|
|
78
94
|
onChange: (e) => setPageInput(e.target.value),
|
|
79
95
|
onKeyDown: (e) => e.key === "Enter" && handlePageChange(parseInt(pageInput)),
|
|
@@ -165,22 +181,44 @@ var withAlpha = (hex, alpha) => {
|
|
|
165
181
|
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
166
182
|
};
|
|
167
183
|
var Thumbnail = ({ engine, pageIndex, active, isDark, accentColor, onClick }) => {
|
|
184
|
+
const wrapperRef = useRef2(null);
|
|
168
185
|
const canvasRef = useRef2(null);
|
|
169
186
|
const htmlRef = useRef2(null);
|
|
170
187
|
const accentSoft = withAlpha(accentColor, 0.12);
|
|
171
188
|
const renderTargetType = engine.getRenderTargetType?.() ?? "canvas";
|
|
189
|
+
const [isVisible, setIsVisible] = useState2(false);
|
|
190
|
+
useEffect2(() => {
|
|
191
|
+
const target = wrapperRef.current;
|
|
192
|
+
if (!target) return;
|
|
193
|
+
if (typeof IntersectionObserver === "undefined") {
|
|
194
|
+
setIsVisible(true);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const root = target.closest(".custom-scrollbar");
|
|
198
|
+
const observer = new IntersectionObserver((entries) => {
|
|
199
|
+
entries.forEach((entry) => {
|
|
200
|
+
if (entry.isIntersecting) {
|
|
201
|
+
setIsVisible(true);
|
|
202
|
+
observer.disconnect();
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
}, { root: root ?? null, rootMargin: "200px" });
|
|
206
|
+
observer.observe(target);
|
|
207
|
+
return () => observer.disconnect();
|
|
208
|
+
}, []);
|
|
172
209
|
useEffect2(() => {
|
|
173
|
-
if (renderTargetType === "element") return;
|
|
210
|
+
if (renderTargetType === "element" || !isVisible) return;
|
|
174
211
|
const target = canvasRef.current;
|
|
175
212
|
if (target) {
|
|
176
213
|
engine.renderPage(pageIndex, target, 0.15).catch((err) => {
|
|
177
214
|
console.error("[Papyrus] Thumbnail render failed:", err);
|
|
178
215
|
});
|
|
179
216
|
}
|
|
180
|
-
}, [engine, pageIndex, renderTargetType]);
|
|
217
|
+
}, [engine, pageIndex, renderTargetType, isVisible]);
|
|
181
218
|
return /* @__PURE__ */ jsx2(
|
|
182
219
|
"div",
|
|
183
220
|
{
|
|
221
|
+
ref: wrapperRef,
|
|
184
222
|
onClick,
|
|
185
223
|
className: `p-3 cursor-pointer transition-all rounded-lg border-2 ${active ? "shadow-sm" : "border-transparent"}`,
|
|
186
224
|
style: active ? { borderColor: accentColor, backgroundColor: accentSoft } : void 0,
|
|
@@ -353,6 +391,7 @@ var SidebarRight = ({ engine }) => {
|
|
|
353
391
|
const searchService = new SearchService(engine);
|
|
354
392
|
const isDark = uiTheme === "dark";
|
|
355
393
|
const accentSoft = withAlpha2(accentColor, 0.12);
|
|
394
|
+
const resultsCount = searchResults.length;
|
|
356
395
|
const handleSearch = async (e) => {
|
|
357
396
|
e.preventDefault();
|
|
358
397
|
if (!query.trim()) {
|
|
@@ -392,7 +431,7 @@ var SidebarRight = ({ engine }) => {
|
|
|
392
431
|
}
|
|
393
432
|
)
|
|
394
433
|
] }),
|
|
395
|
-
/* @__PURE__ */ jsx3("button", { onClick: () => toggleSidebarRight(), className: "text-gray-400 hover:text-red-500 transition-colors", children: /* @__PURE__ */ jsx3("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx3("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) }) })
|
|
434
|
+
/* @__PURE__ */ jsx3("button", { onClick: () => toggleSidebarRight(), className: "papyrus-unstyled-button text-gray-400 hover:text-red-500 transition-colors", children: /* @__PURE__ */ jsx3("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx3("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) }) })
|
|
396
435
|
] }),
|
|
397
436
|
/* @__PURE__ */ jsx3("div", { className: "flex-1 overflow-y-auto p-4 custom-scrollbar bg-opacity-50", children: sidebarRightTab === "search" ? /* @__PURE__ */ jsxs3("div", { className: "space-y-4", children: [
|
|
398
437
|
/* @__PURE__ */ jsxs3("form", { onSubmit: handleSearch, className: "relative mb-6", children: [
|
|
@@ -406,7 +445,8 @@ var SidebarRight = ({ engine }) => {
|
|
|
406
445
|
onChange: (e) => setQuery(e.target.value)
|
|
407
446
|
}
|
|
408
447
|
),
|
|
409
|
-
/* @__PURE__ */ jsx3("
|
|
448
|
+
resultsCount > 0 && /* @__PURE__ */ jsx3("span", { className: "absolute right-9 top-2.5 text-[10px] font-bold text-gray-400", children: resultsCount }),
|
|
449
|
+
/* @__PURE__ */ jsx3("button", { type: "submit", className: "papyrus-unstyled-button absolute right-3 top-2.5 text-gray-400 transition-colors", style: { color: accentColor }, children: /* @__PURE__ */ jsx3("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx3("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2.5, d: "M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" }) }) })
|
|
410
450
|
] }),
|
|
411
451
|
isSearching && /* @__PURE__ */ jsxs3("div", { className: "flex flex-col items-center justify-center py-12 space-y-3", children: [
|
|
412
452
|
/* @__PURE__ */ jsx3("div", { className: "w-6 h-6 border-2 border-t-transparent rounded-full animate-spin", style: { borderColor: accentColor } }),
|
|
@@ -416,7 +456,9 @@ var SidebarRight = ({ engine }) => {
|
|
|
416
456
|
"div",
|
|
417
457
|
{
|
|
418
458
|
onClick: () => {
|
|
419
|
-
|
|
459
|
+
const page = res.pageIndex + 1;
|
|
460
|
+
engine.goToPage(page);
|
|
461
|
+
setDocumentState({ activeSearchIndex: idx, currentPage: page });
|
|
420
462
|
triggerScrollToPage(res.pageIndex);
|
|
421
463
|
},
|
|
422
464
|
className: `p-4 rounded-xl border-2 cursor-pointer transition-all group hover:scale-[1.02] ${idx === activeSearchIndex ? "shadow-lg" : isDark ? "border-[#333] hover:border-[#555] bg-[#222]" : "border-gray-50 hover:border-gray-200 bg-gray-50/50 hover:bg-white"}`,
|
|
@@ -484,7 +526,7 @@ var SidebarRight = ({ engine }) => {
|
|
|
484
526
|
var SidebarRight_default = SidebarRight;
|
|
485
527
|
|
|
486
528
|
// components/Viewer.tsx
|
|
487
|
-
import { useEffect as useEffect4, useRef as useRef4, useState as useState5 } from "react";
|
|
529
|
+
import { useEffect as useEffect4, useMemo as useMemo2, useRef as useRef4, useState as useState5 } from "react";
|
|
488
530
|
import { useViewerStore as useViewerStore5 } from "@papyrus-sdk/core";
|
|
489
531
|
|
|
490
532
|
// components/PageRenderer.tsx
|
|
@@ -492,7 +534,7 @@ import { useEffect as useEffect3, useMemo, useRef as useRef3, useState as useSta
|
|
|
492
534
|
import { useViewerStore as useViewerStore4, papyrusEvents } from "@papyrus-sdk/core";
|
|
493
535
|
import { PapyrusEventType } from "@papyrus-sdk/types";
|
|
494
536
|
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
495
|
-
var PageRenderer = ({ engine, pageIndex, availableWidth }) => {
|
|
537
|
+
var PageRenderer = ({ engine, pageIndex, availableWidth, onMeasuredSize }) => {
|
|
496
538
|
const containerRef = useRef3(null);
|
|
497
539
|
const canvasRef = useRef3(null);
|
|
498
540
|
const htmlLayerRef = useRef3(null);
|
|
@@ -502,6 +544,10 @@ var PageRenderer = ({ engine, pageIndex, availableWidth }) => {
|
|
|
502
544
|
const [isDragging, setIsDragging] = useState4(false);
|
|
503
545
|
const [startPos, setStartPos] = useState4({ x: 0, y: 0 });
|
|
504
546
|
const [currentRect, setCurrentRect] = useState4({ x: 0, y: 0, w: 0, h: 0 });
|
|
547
|
+
const [textLayerVersion, setTextLayerVersion] = useState4(0);
|
|
548
|
+
const [selectionMenu, setSelectionMenu] = useState4(null);
|
|
549
|
+
const [isInkDrawing, setIsInkDrawing] = useState4(false);
|
|
550
|
+
const [inkPoints, setInkPoints] = useState4([]);
|
|
505
551
|
const {
|
|
506
552
|
zoom,
|
|
507
553
|
rotation,
|
|
@@ -514,10 +560,20 @@ var PageRenderer = ({ engine, pageIndex, availableWidth }) => {
|
|
|
514
560
|
removeAnnotation,
|
|
515
561
|
selectedAnnotationId,
|
|
516
562
|
setSelectedAnnotation,
|
|
517
|
-
accentColor
|
|
563
|
+
accentColor,
|
|
564
|
+
annotationColor,
|
|
565
|
+
searchQuery,
|
|
566
|
+
searchResults,
|
|
567
|
+
activeSearchIndex
|
|
518
568
|
} = useViewerStore4();
|
|
519
569
|
const renderTargetType = engine.getRenderTargetType?.() ?? "canvas";
|
|
520
570
|
const isElementRender = renderTargetType === "element";
|
|
571
|
+
const textMarkupTools = /* @__PURE__ */ new Set(["highlight", "underline", "squiggly", "strikeout"]);
|
|
572
|
+
const canSelectText = activeTool === "select" || textMarkupTools.has(activeTool);
|
|
573
|
+
const hasSearchHits = useMemo(
|
|
574
|
+
() => Boolean(searchQuery?.trim()) && searchResults.some((res) => res.pageIndex === pageIndex),
|
|
575
|
+
[searchQuery, searchResults, pageIndex]
|
|
576
|
+
);
|
|
521
577
|
useEffect3(() => {
|
|
522
578
|
let active = true;
|
|
523
579
|
const loadSize = async () => {
|
|
@@ -550,8 +606,14 @@ var PageRenderer = ({ engine, pageIndex, availableWidth }) => {
|
|
|
550
606
|
};
|
|
551
607
|
}, [pageSize, zoom, fitScale]);
|
|
552
608
|
useEffect3(() => {
|
|
553
|
-
if (
|
|
554
|
-
|
|
609
|
+
if (!displaySize || !onMeasuredSize) return;
|
|
610
|
+
onMeasuredSize(pageIndex, {
|
|
611
|
+
width: Math.round(displaySize.width),
|
|
612
|
+
height: Math.round(displaySize.height)
|
|
613
|
+
});
|
|
614
|
+
}, [displaySize, onMeasuredSize, pageIndex]);
|
|
615
|
+
useEffect3(() => {
|
|
616
|
+
if (scrollToPageSignal === pageIndex) {
|
|
555
617
|
setDocumentState({ scrollToPageSignal: null });
|
|
556
618
|
}
|
|
557
619
|
}, [scrollToPageSignal, pageIndex, setDocumentState]);
|
|
@@ -563,10 +625,15 @@ var PageRenderer = ({ engine, pageIndex, availableWidth }) => {
|
|
|
563
625
|
setLoading(true);
|
|
564
626
|
try {
|
|
565
627
|
const RENDER_SCALE = 2;
|
|
566
|
-
const
|
|
567
|
-
|
|
628
|
+
const canvasRenderScale = isElementRender ? 1 : RENDER_SCALE * fitScale;
|
|
629
|
+
const textRenderScale = isElementRender ? 1 : fitScale;
|
|
630
|
+
if (!isElementRender && canvasRef.current && displaySize) {
|
|
631
|
+
canvasRef.current.style.width = `${displaySize.width}px`;
|
|
632
|
+
canvasRef.current.style.height = `${displaySize.height}px`;
|
|
633
|
+
}
|
|
634
|
+
await engine.renderPage(pageIndex, renderTarget, canvasRenderScale);
|
|
568
635
|
if (!isElementRender && !pageSize && canvasRef.current) {
|
|
569
|
-
const denom =
|
|
636
|
+
const denom = canvasRenderScale * Math.max(zoom, 0.01);
|
|
570
637
|
if (denom > 0) {
|
|
571
638
|
setPageSize({
|
|
572
639
|
width: canvasRef.current.width / denom,
|
|
@@ -577,7 +644,7 @@ var PageRenderer = ({ engine, pageIndex, availableWidth }) => {
|
|
|
577
644
|
if (!active || !textLayerRef.current) return;
|
|
578
645
|
if (!isElementRender) {
|
|
579
646
|
textLayerRef.current.innerHTML = "";
|
|
580
|
-
await engine.renderTextLayer(pageIndex, textLayerRef.current,
|
|
647
|
+
await engine.renderTextLayer(pageIndex, textLayerRef.current, textRenderScale);
|
|
581
648
|
}
|
|
582
649
|
if (!active || !textLayerRef.current) return;
|
|
583
650
|
if (!isElementRender && displaySize) {
|
|
@@ -588,6 +655,7 @@ var PageRenderer = ({ engine, pageIndex, availableWidth }) => {
|
|
|
588
655
|
textLayerRef.current.style.width = `${displaySize.width}px`;
|
|
589
656
|
textLayerRef.current.style.height = `${displaySize.height}px`;
|
|
590
657
|
}
|
|
658
|
+
setTextLayerVersion((v) => v + 1);
|
|
591
659
|
} catch (err) {
|
|
592
660
|
if (!active) return;
|
|
593
661
|
console.error("[Papyrus] Falha na renderiza\xE7\xE3o:", err);
|
|
@@ -600,8 +668,68 @@ var PageRenderer = ({ engine, pageIndex, availableWidth }) => {
|
|
|
600
668
|
active = false;
|
|
601
669
|
};
|
|
602
670
|
}, [engine, pageIndex, zoom, rotation, isElementRender, fitScale, displaySize, pageSize]);
|
|
671
|
+
useEffect3(() => {
|
|
672
|
+
if (isElementRender) return;
|
|
673
|
+
const layer = textLayerRef.current;
|
|
674
|
+
if (!layer) return;
|
|
675
|
+
const query = searchQuery?.trim().toLowerCase();
|
|
676
|
+
const existingMarks = Array.from(layer.querySelectorAll("mark.papyrus-search-hit"));
|
|
677
|
+
existingMarks.forEach((mark) => {
|
|
678
|
+
const parent = mark.parentNode;
|
|
679
|
+
if (!parent) return;
|
|
680
|
+
while (mark.firstChild) parent.insertBefore(mark.firstChild, mark);
|
|
681
|
+
parent.removeChild(mark);
|
|
682
|
+
parent.normalize();
|
|
683
|
+
});
|
|
684
|
+
if (!query || !hasSearchHits) return;
|
|
685
|
+
const nodes = [];
|
|
686
|
+
const walker = document.createTreeWalker(layer, NodeFilter.SHOW_TEXT, {
|
|
687
|
+
acceptNode(node) {
|
|
688
|
+
const text = node.nodeValue ?? "";
|
|
689
|
+
if (!text.trim()) return NodeFilter.FILTER_REJECT;
|
|
690
|
+
return NodeFilter.FILTER_ACCEPT;
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
while (walker.nextNode()) {
|
|
694
|
+
nodes.push(walker.currentNode);
|
|
695
|
+
}
|
|
696
|
+
nodes.forEach((textNode) => {
|
|
697
|
+
const text = textNode.nodeValue ?? "";
|
|
698
|
+
const lower = text.toLowerCase();
|
|
699
|
+
if (!lower.includes(query)) return;
|
|
700
|
+
const fragment = document.createDocumentFragment();
|
|
701
|
+
let cursor = 0;
|
|
702
|
+
let index = lower.indexOf(query, cursor);
|
|
703
|
+
while (index !== -1) {
|
|
704
|
+
if (index > cursor) {
|
|
705
|
+
fragment.appendChild(document.createTextNode(text.slice(cursor, index)));
|
|
706
|
+
}
|
|
707
|
+
const mark = document.createElement("mark");
|
|
708
|
+
mark.className = "papyrus-search-hit";
|
|
709
|
+
mark.textContent = text.slice(index, index + query.length);
|
|
710
|
+
fragment.appendChild(mark);
|
|
711
|
+
cursor = index + query.length;
|
|
712
|
+
index = lower.indexOf(query, cursor);
|
|
713
|
+
}
|
|
714
|
+
if (cursor < text.length) {
|
|
715
|
+
fragment.appendChild(document.createTextNode(text.slice(cursor)));
|
|
716
|
+
}
|
|
717
|
+
const parent = textNode.parentNode;
|
|
718
|
+
if (parent) parent.replaceChild(fragment, textNode);
|
|
719
|
+
});
|
|
720
|
+
}, [searchQuery, hasSearchHits, pageIndex, isElementRender, activeSearchIndex, textLayerVersion]);
|
|
603
721
|
const handleMouseDown = (e) => {
|
|
604
|
-
|
|
722
|
+
setSelectionMenu(null);
|
|
723
|
+
if (activeTool === "ink") {
|
|
724
|
+
const rect2 = containerRef.current?.getBoundingClientRect();
|
|
725
|
+
if (!rect2) return;
|
|
726
|
+
const x2 = (e.clientX - rect2.left) / rect2.width;
|
|
727
|
+
const y2 = (e.clientY - rect2.top) / rect2.height;
|
|
728
|
+
setIsInkDrawing(true);
|
|
729
|
+
setInkPoints([{ x: x2, y: y2 }]);
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
if (canSelectText) return;
|
|
605
733
|
const rect = containerRef.current?.getBoundingClientRect();
|
|
606
734
|
if (!rect) return;
|
|
607
735
|
setIsDragging(true);
|
|
@@ -611,6 +739,14 @@ var PageRenderer = ({ engine, pageIndex, availableWidth }) => {
|
|
|
611
739
|
setCurrentRect({ x, y, w: 0, h: 0 });
|
|
612
740
|
};
|
|
613
741
|
const handleMouseMove = (e) => {
|
|
742
|
+
if (isInkDrawing) {
|
|
743
|
+
const rect2 = containerRef.current?.getBoundingClientRect();
|
|
744
|
+
if (!rect2) return;
|
|
745
|
+
const x = (e.clientX - rect2.left) / rect2.width;
|
|
746
|
+
const y = (e.clientY - rect2.top) / rect2.height;
|
|
747
|
+
setInkPoints((prev) => [...prev, { x, y }]);
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
614
750
|
if (!isDragging) return;
|
|
615
751
|
const rect = containerRef.current?.getBoundingClientRect();
|
|
616
752
|
if (!rect) return;
|
|
@@ -624,6 +760,115 @@ var PageRenderer = ({ engine, pageIndex, availableWidth }) => {
|
|
|
624
760
|
});
|
|
625
761
|
};
|
|
626
762
|
const handleMouseUp = (e) => {
|
|
763
|
+
if (isInkDrawing) {
|
|
764
|
+
setIsInkDrawing(false);
|
|
765
|
+
if (inkPoints.length > 1) {
|
|
766
|
+
const xs = inkPoints.map((p) => p.x);
|
|
767
|
+
const ys = inkPoints.map((p) => p.y);
|
|
768
|
+
const minX = Math.min(...xs);
|
|
769
|
+
const maxX = Math.max(...xs);
|
|
770
|
+
const minY = Math.min(...ys);
|
|
771
|
+
const maxY = Math.max(...ys);
|
|
772
|
+
const width = Math.max(maxX - minX, 5e-4);
|
|
773
|
+
const height = Math.max(maxY - minY, 5e-4);
|
|
774
|
+
const path = inkPoints.map((p) => ({
|
|
775
|
+
x: Math.max(0, Math.min(1, p.x)),
|
|
776
|
+
y: Math.max(0, Math.min(1, p.y))
|
|
777
|
+
}));
|
|
778
|
+
addAnnotation({
|
|
779
|
+
id: Math.random().toString(36).substr(2, 9),
|
|
780
|
+
pageIndex,
|
|
781
|
+
type: "ink",
|
|
782
|
+
rect: { x: minX, y: minY, width, height },
|
|
783
|
+
path,
|
|
784
|
+
color: annotationColor,
|
|
785
|
+
createdAt: Date.now()
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
setInkPoints([]);
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
const selection = window.getSelection();
|
|
792
|
+
const selectionText = selection?.toString().trim() ?? "";
|
|
793
|
+
if (selectionText && textLayerRef.current && containerRef.current && selection && selection.rangeCount > 0) {
|
|
794
|
+
const range = selection.getRangeAt(0);
|
|
795
|
+
if (textLayerRef.current.contains(range.commonAncestorContainer)) {
|
|
796
|
+
const containerRect = containerRef.current.getBoundingClientRect();
|
|
797
|
+
const clientRects = Array.from(range.getClientRects());
|
|
798
|
+
const rects = clientRects.filter((r) => r.width > 1 && r.height > 1).map((r) => {
|
|
799
|
+
const x = (r.left - containerRect.left) / containerRect.width;
|
|
800
|
+
const y = (r.top - containerRect.top) / containerRect.height;
|
|
801
|
+
const width = r.width / containerRect.width;
|
|
802
|
+
const height = r.height / containerRect.height;
|
|
803
|
+
return {
|
|
804
|
+
x: Math.max(0, Math.min(1, x)),
|
|
805
|
+
y: Math.max(0, Math.min(1, y)),
|
|
806
|
+
width: Math.max(0, Math.min(1, width)),
|
|
807
|
+
height: Math.max(0, Math.min(1, height))
|
|
808
|
+
};
|
|
809
|
+
});
|
|
810
|
+
const uniqueRects = rects.filter((rect, index, list) => {
|
|
811
|
+
const key = `${Math.round(rect.x * 1e4)}-${Math.round(rect.y * 1e4)}-${Math.round(rect.width * 1e4)}-${Math.round(rect.height * 1e4)}`;
|
|
812
|
+
return list.findIndex((r) => `${Math.round(r.x * 1e4)}-${Math.round(r.y * 1e4)}-${Math.round(r.width * 1e4)}-${Math.round(r.height * 1e4)}` === key) === index;
|
|
813
|
+
});
|
|
814
|
+
const mergedRects = uniqueRects.reduce((acc, rect) => {
|
|
815
|
+
const target = acc.find((r) => {
|
|
816
|
+
const closeY = Math.abs(r.y - rect.y) < 2e-3 && Math.abs(r.height - rect.height) < 2e-3;
|
|
817
|
+
const overlaps = rect.x <= r.x + r.width + 2e-3 && rect.x + rect.width >= r.x - 2e-3;
|
|
818
|
+
return closeY && overlaps;
|
|
819
|
+
});
|
|
820
|
+
if (!target) {
|
|
821
|
+
acc.push({ ...rect });
|
|
822
|
+
return acc;
|
|
823
|
+
}
|
|
824
|
+
const left = Math.min(target.x, rect.x);
|
|
825
|
+
const right = Math.max(target.x + target.width, rect.x + rect.width);
|
|
826
|
+
target.x = left;
|
|
827
|
+
target.width = right - left;
|
|
828
|
+
return acc;
|
|
829
|
+
}, []);
|
|
830
|
+
if (mergedRects.length) {
|
|
831
|
+
const xs = mergedRects.map((r) => r.x);
|
|
832
|
+
const ys = mergedRects.map((r) => r.y);
|
|
833
|
+
const xe = mergedRects.map((r) => r.x + r.width);
|
|
834
|
+
const ye = mergedRects.map((r) => r.y + r.height);
|
|
835
|
+
const rect = {
|
|
836
|
+
x: Math.min(...xs),
|
|
837
|
+
y: Math.min(...ys),
|
|
838
|
+
width: Math.max(...xe) - Math.min(...xs),
|
|
839
|
+
height: Math.max(...ye) - Math.min(...ys)
|
|
840
|
+
};
|
|
841
|
+
if (textMarkupTools.has(activeTool)) {
|
|
842
|
+
addAnnotation({
|
|
843
|
+
id: Math.random().toString(36).substr(2, 9),
|
|
844
|
+
pageIndex,
|
|
845
|
+
type: activeTool,
|
|
846
|
+
rect,
|
|
847
|
+
rects: mergedRects,
|
|
848
|
+
color: annotationColor,
|
|
849
|
+
content: selectionText,
|
|
850
|
+
createdAt: Date.now()
|
|
851
|
+
});
|
|
852
|
+
selection.removeAllRanges();
|
|
853
|
+
setSelectionMenu(null);
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
if (activeTool === "select") {
|
|
857
|
+
const anchorX = (rect.x + rect.width) * containerRect.width;
|
|
858
|
+
const anchorY = rect.y * containerRect.height;
|
|
859
|
+
setSelectionMenu({
|
|
860
|
+
rects: mergedRects,
|
|
861
|
+
rect,
|
|
862
|
+
text: selectionText,
|
|
863
|
+
anchor: {
|
|
864
|
+
x: Math.max(12, Math.min(containerRect.width - 12, anchorX)),
|
|
865
|
+
y: Math.max(12, anchorY - 32)
|
|
866
|
+
}
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
627
872
|
if (isDragging) {
|
|
628
873
|
setIsDragging(false);
|
|
629
874
|
if (currentRect.w > 5 && currentRect.h > 5) {
|
|
@@ -639,7 +884,7 @@ var PageRenderer = ({ engine, pageIndex, availableWidth }) => {
|
|
|
639
884
|
width: currentRect.w / rect.width,
|
|
640
885
|
height: currentRect.h / rect.height
|
|
641
886
|
},
|
|
642
|
-
color: activeTool === "highlight" ?
|
|
887
|
+
color: activeTool === "highlight" ? annotationColor : accentColor,
|
|
643
888
|
content: activeTool === "text" || activeTool === "comment" ? "" : void 0,
|
|
644
889
|
createdAt: Date.now()
|
|
645
890
|
});
|
|
@@ -649,7 +894,6 @@ var PageRenderer = ({ engine, pageIndex, availableWidth }) => {
|
|
|
649
894
|
return;
|
|
650
895
|
}
|
|
651
896
|
if (activeTool === "select") {
|
|
652
|
-
const selection = window.getSelection();
|
|
653
897
|
const selectedText = selection?.toString().trim();
|
|
654
898
|
if (selectedText) {
|
|
655
899
|
papyrusEvents.emit(PapyrusEventType.TEXT_SELECTED, {
|
|
@@ -675,7 +919,7 @@ var PageRenderer = ({ engine, pageIndex, availableWidth }) => {
|
|
|
675
919
|
"div",
|
|
676
920
|
{
|
|
677
921
|
ref: containerRef,
|
|
678
|
-
className: `relative inline-block shadow-2xl bg-white mb-10 transition-all ${
|
|
922
|
+
className: `relative inline-block shadow-2xl bg-white mb-10 transition-all ${canSelectText ? "" : "no-select cursor-crosshair"}`,
|
|
679
923
|
style: { scrollMarginTop: "20px", minHeight: "100px" },
|
|
680
924
|
onMouseDown: handleMouseDown,
|
|
681
925
|
onMouseMove: handleMouseMove,
|
|
@@ -704,7 +948,7 @@ var PageRenderer = ({ engine, pageIndex, availableWidth }) => {
|
|
|
704
948
|
ref: textLayerRef,
|
|
705
949
|
className: "textLayer",
|
|
706
950
|
style: {
|
|
707
|
-
pointerEvents: isElementRender ? "none" :
|
|
951
|
+
pointerEvents: isElementRender ? "none" : canSelectText ? "auto" : "none",
|
|
708
952
|
display: isElementRender ? "none" : "block"
|
|
709
953
|
}
|
|
710
954
|
}
|
|
@@ -714,8 +958,9 @@ var PageRenderer = ({ engine, pageIndex, availableWidth }) => {
|
|
|
714
958
|
{
|
|
715
959
|
className: "absolute border-2 z-[40] pointer-events-none",
|
|
716
960
|
style: {
|
|
717
|
-
borderColor: accentColor,
|
|
718
|
-
backgroundColor: `${accentColor}33`,
|
|
961
|
+
borderColor: activeTool === "highlight" ? annotationColor : accentColor,
|
|
962
|
+
backgroundColor: activeTool === "highlight" ? `${annotationColor}66` : `${accentColor}33`,
|
|
963
|
+
mixBlendMode: activeTool === "highlight" ? "multiply" : void 0,
|
|
719
964
|
left: currentRect.x,
|
|
720
965
|
top: currentRect.y,
|
|
721
966
|
width: currentRect.w,
|
|
@@ -723,6 +968,60 @@ var PageRenderer = ({ engine, pageIndex, availableWidth }) => {
|
|
|
723
968
|
}
|
|
724
969
|
}
|
|
725
970
|
),
|
|
971
|
+
isInkDrawing && inkPoints.length > 1 && /* @__PURE__ */ jsx4(
|
|
972
|
+
"svg",
|
|
973
|
+
{
|
|
974
|
+
className: "absolute inset-0 pointer-events-none z-[45]",
|
|
975
|
+
viewBox: "0 0 1 1",
|
|
976
|
+
preserveAspectRatio: "none",
|
|
977
|
+
style: { width: "100%", height: "100%" },
|
|
978
|
+
children: /* @__PURE__ */ jsx4(
|
|
979
|
+
"path",
|
|
980
|
+
{
|
|
981
|
+
d: inkPoints.map((p, idx) => `${idx === 0 ? "M" : "L"} ${p.x} ${p.y}`).join(" "),
|
|
982
|
+
fill: "none",
|
|
983
|
+
stroke: annotationColor,
|
|
984
|
+
strokeWidth: 8e-3,
|
|
985
|
+
strokeLinecap: "round",
|
|
986
|
+
strokeLinejoin: "round"
|
|
987
|
+
}
|
|
988
|
+
)
|
|
989
|
+
}
|
|
990
|
+
),
|
|
991
|
+
selectionMenu && /* @__PURE__ */ jsx4(
|
|
992
|
+
"div",
|
|
993
|
+
{
|
|
994
|
+
className: "absolute z-[60] flex items-center gap-1 rounded-full border px-2 py-1 shadow-xl bg-white/95 backdrop-blur-md text-gray-700",
|
|
995
|
+
style: { left: selectionMenu.anchor.x, top: selectionMenu.anchor.y },
|
|
996
|
+
children: [
|
|
997
|
+
{ id: "highlight", label: "Marcar" },
|
|
998
|
+
{ id: "underline", label: "Sublinhar" },
|
|
999
|
+
{ id: "squiggly", label: "Onda" },
|
|
1000
|
+
{ id: "strikeout", label: "Risco" }
|
|
1001
|
+
].map((action) => /* @__PURE__ */ jsx4(
|
|
1002
|
+
"button",
|
|
1003
|
+
{
|
|
1004
|
+
className: "text-[10px] font-bold px-2 py-1 rounded-full hover:bg-gray-100",
|
|
1005
|
+
onClick: () => {
|
|
1006
|
+
addAnnotation({
|
|
1007
|
+
id: Math.random().toString(36).substr(2, 9),
|
|
1008
|
+
pageIndex,
|
|
1009
|
+
type: action.id,
|
|
1010
|
+
rect: selectionMenu.rect,
|
|
1011
|
+
rects: selectionMenu.rects,
|
|
1012
|
+
content: selectionMenu.text,
|
|
1013
|
+
color: annotationColor,
|
|
1014
|
+
createdAt: Date.now()
|
|
1015
|
+
});
|
|
1016
|
+
window.getSelection()?.removeAllRanges();
|
|
1017
|
+
setSelectionMenu(null);
|
|
1018
|
+
},
|
|
1019
|
+
children: action.label
|
|
1020
|
+
},
|
|
1021
|
+
action.id
|
|
1022
|
+
))
|
|
1023
|
+
}
|
|
1024
|
+
),
|
|
726
1025
|
/* @__PURE__ */ jsx4("div", { className: "absolute inset-0 pointer-events-none z-20", children: annotations.filter((a) => a.pageIndex === pageIndex).map((ann) => /* @__PURE__ */ jsx4(
|
|
727
1026
|
AnnotationItem,
|
|
728
1027
|
{
|
|
@@ -740,6 +1039,101 @@ var PageRenderer = ({ engine, pageIndex, availableWidth }) => {
|
|
|
740
1039
|
};
|
|
741
1040
|
var AnnotationItem = ({ ann, isSelected, accentColor, onDelete, onSelect }) => {
|
|
742
1041
|
const isText = ann.type === "text" || ann.type === "comment";
|
|
1042
|
+
const isHighlight = ann.type === "highlight";
|
|
1043
|
+
const isMarkup = ann.type === "highlight" || ann.type === "underline" || ann.type === "squiggly" || ann.type === "strikeout";
|
|
1044
|
+
const rects = ann.rects && ann.rects.length > 0 ? ann.rects : [ann.rect];
|
|
1045
|
+
const isInk = ann.type === "ink" && ann.path && ann.path.length > 1;
|
|
1046
|
+
const renderMarkupRects = () => {
|
|
1047
|
+
if (!isMarkup) return null;
|
|
1048
|
+
return rects.map((r, idx) => {
|
|
1049
|
+
const left = ann.rect.width ? (r.x - ann.rect.x) / ann.rect.width * 100 : 0;
|
|
1050
|
+
const top = ann.rect.height ? (r.y - ann.rect.y) / ann.rect.height * 100 : 0;
|
|
1051
|
+
const width = ann.rect.width ? r.width / ann.rect.width * 100 : 100;
|
|
1052
|
+
const height = ann.rect.height ? r.height / ann.rect.height * 100 : 100;
|
|
1053
|
+
if (ann.type === "highlight") {
|
|
1054
|
+
return /* @__PURE__ */ jsx4(
|
|
1055
|
+
"div",
|
|
1056
|
+
{
|
|
1057
|
+
className: "absolute rounded-sm",
|
|
1058
|
+
style: {
|
|
1059
|
+
left: `${left}%`,
|
|
1060
|
+
top: `${top}%`,
|
|
1061
|
+
width: `${width}%`,
|
|
1062
|
+
height: `${height}%`,
|
|
1063
|
+
backgroundColor: `${ann.color}88`,
|
|
1064
|
+
mixBlendMode: "multiply"
|
|
1065
|
+
}
|
|
1066
|
+
},
|
|
1067
|
+
idx
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
const lineStyle = {
|
|
1071
|
+
left: `${left}%`,
|
|
1072
|
+
width: `${width}%`
|
|
1073
|
+
};
|
|
1074
|
+
if (ann.type === "underline") {
|
|
1075
|
+
return /* @__PURE__ */ jsx4(
|
|
1076
|
+
"div",
|
|
1077
|
+
{
|
|
1078
|
+
className: "absolute",
|
|
1079
|
+
style: {
|
|
1080
|
+
...lineStyle,
|
|
1081
|
+
top: `calc(${top}% + ${height}% - 2px)`,
|
|
1082
|
+
height: "2px",
|
|
1083
|
+
backgroundColor: ann.color
|
|
1084
|
+
}
|
|
1085
|
+
},
|
|
1086
|
+
idx
|
|
1087
|
+
);
|
|
1088
|
+
}
|
|
1089
|
+
if (ann.type === "strikeout") {
|
|
1090
|
+
return /* @__PURE__ */ jsx4(
|
|
1091
|
+
"div",
|
|
1092
|
+
{
|
|
1093
|
+
className: "absolute",
|
|
1094
|
+
style: {
|
|
1095
|
+
...lineStyle,
|
|
1096
|
+
top: `calc(${top}% + ${height * 0.5}% - 1px)`,
|
|
1097
|
+
height: "2px",
|
|
1098
|
+
backgroundColor: ann.color
|
|
1099
|
+
}
|
|
1100
|
+
},
|
|
1101
|
+
idx
|
|
1102
|
+
);
|
|
1103
|
+
}
|
|
1104
|
+
if (ann.type === "squiggly") {
|
|
1105
|
+
return /* @__PURE__ */ jsx4(
|
|
1106
|
+
"div",
|
|
1107
|
+
{
|
|
1108
|
+
className: "absolute",
|
|
1109
|
+
style: {
|
|
1110
|
+
...lineStyle,
|
|
1111
|
+
top: `calc(${top}% + ${height}% - 4px)`,
|
|
1112
|
+
height: "4px",
|
|
1113
|
+
backgroundImage: `linear-gradient(135deg, transparent 75%, ${ann.color} 0), linear-gradient(225deg, transparent 75%, ${ann.color} 0)`,
|
|
1114
|
+
backgroundSize: "6px 6px",
|
|
1115
|
+
backgroundPosition: "0 0, 3px 3px"
|
|
1116
|
+
}
|
|
1117
|
+
},
|
|
1118
|
+
idx
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
1121
|
+
return null;
|
|
1122
|
+
});
|
|
1123
|
+
};
|
|
1124
|
+
const renderInk = () => {
|
|
1125
|
+
if (!isInk || !ann.path) return null;
|
|
1126
|
+
const d = ann.path.map((p, idx) => `${idx === 0 ? "M" : "L"} ${p.x} ${p.y}`).join(" ");
|
|
1127
|
+
return /* @__PURE__ */ jsx4(
|
|
1128
|
+
"svg",
|
|
1129
|
+
{
|
|
1130
|
+
className: "absolute inset-0",
|
|
1131
|
+
viewBox: `${ann.rect.x} ${ann.rect.y} ${ann.rect.width} ${ann.rect.height}`,
|
|
1132
|
+
preserveAspectRatio: "none",
|
|
1133
|
+
children: /* @__PURE__ */ jsx4("path", { d, fill: "none", stroke: ann.color, strokeWidth: 8e-3, strokeLinecap: "round", strokeLinejoin: "round" })
|
|
1134
|
+
}
|
|
1135
|
+
);
|
|
1136
|
+
};
|
|
743
1137
|
return /* @__PURE__ */ jsxs4(
|
|
744
1138
|
"div",
|
|
745
1139
|
{
|
|
@@ -749,8 +1143,9 @@ var AnnotationItem = ({ ann, isSelected, accentColor, onDelete, onSelect }) => {
|
|
|
749
1143
|
top: `${ann.rect.y * 100}%`,
|
|
750
1144
|
width: `${ann.rect.width * 100}%`,
|
|
751
1145
|
height: `${ann.rect.height * 100}%`,
|
|
752
|
-
backgroundColor:
|
|
753
|
-
|
|
1146
|
+
backgroundColor: !isMarkup && isHighlight ? `${ann.color}88` : "transparent",
|
|
1147
|
+
mixBlendMode: !isMarkup && isHighlight ? "multiply" : void 0,
|
|
1148
|
+
borderBottom: ann.type === "strikeout" && !isMarkup ? `2px solid ${ann.color}` : "none",
|
|
754
1149
|
outline: isSelected ? `2px solid ${accentColor}` : void 0
|
|
755
1150
|
},
|
|
756
1151
|
onClick: (e) => {
|
|
@@ -758,6 +1153,8 @@ var AnnotationItem = ({ ann, isSelected, accentColor, onDelete, onSelect }) => {
|
|
|
758
1153
|
onSelect();
|
|
759
1154
|
},
|
|
760
1155
|
children: [
|
|
1156
|
+
renderMarkupRects(),
|
|
1157
|
+
renderInk(),
|
|
761
1158
|
isText && isSelected && /* @__PURE__ */ jsx4("div", { className: "absolute top-full mt-2 w-64 bg-white shadow-2xl rounded-xl p-4 border border-gray-100 z-50", children: /* @__PURE__ */ jsx4(
|
|
762
1159
|
"textarea",
|
|
763
1160
|
{
|
|
@@ -787,14 +1184,36 @@ var PageRenderer_default = PageRenderer;
|
|
|
787
1184
|
|
|
788
1185
|
// components/Viewer.tsx
|
|
789
1186
|
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1187
|
+
var BASE_OVERSCAN = 6;
|
|
790
1188
|
var Viewer = ({ engine }) => {
|
|
791
|
-
const {
|
|
1189
|
+
const { pageCount, currentPage, zoom, activeTool, uiTheme, scrollToPageSignal, setDocumentState, accentColor, annotationColor, setAnnotationColor } = useViewerStore5();
|
|
792
1190
|
const isDark = uiTheme === "dark";
|
|
793
1191
|
const viewerRef = useRef4(null);
|
|
1192
|
+
const colorPickerRef = useRef4(null);
|
|
1193
|
+
const pageRefs = useRef4([]);
|
|
1194
|
+
const intersectionRatiosRef = useRef4({});
|
|
1195
|
+
const frameRef = useRef4(null);
|
|
1196
|
+
const jumpRef = useRef4(false);
|
|
1197
|
+
const jumpTimerRef = useRef4(null);
|
|
794
1198
|
const [availableWidth, setAvailableWidth] = useState5(null);
|
|
1199
|
+
const [basePageSize, setBasePageSize] = useState5(null);
|
|
1200
|
+
const [pageSizes, setPageSizes] = useState5({});
|
|
1201
|
+
const [colorPickerOpen, setColorPickerOpen] = useState5(false);
|
|
795
1202
|
const isCompact = availableWidth !== null && availableWidth < 820;
|
|
796
1203
|
const paddingY = isCompact ? "py-10" : "py-16";
|
|
797
|
-
const toolDockPosition = isCompact ? "bottom-
|
|
1204
|
+
const toolDockPosition = isCompact ? "bottom-4" : "bottom-8";
|
|
1205
|
+
const colorPalette = ["#fbbf24", "#f97316", "#ef4444", "#22c55e", "#06b6d4", "#3b82f6", "#8b5cf6", "#111827"];
|
|
1206
|
+
useEffect4(() => {
|
|
1207
|
+
if (!colorPickerOpen) return;
|
|
1208
|
+
const handleClick = (event) => {
|
|
1209
|
+
if (!colorPickerRef.current) return;
|
|
1210
|
+
if (!colorPickerRef.current.contains(event.target)) {
|
|
1211
|
+
setColorPickerOpen(false);
|
|
1212
|
+
}
|
|
1213
|
+
};
|
|
1214
|
+
document.addEventListener("mousedown", handleClick);
|
|
1215
|
+
return () => document.removeEventListener("mousedown", handleClick);
|
|
1216
|
+
}, [colorPickerOpen]);
|
|
798
1217
|
useEffect4(() => {
|
|
799
1218
|
const element = viewerRef.current;
|
|
800
1219
|
if (!element) return;
|
|
@@ -811,29 +1230,139 @@ var Viewer = ({ engine }) => {
|
|
|
811
1230
|
observer.observe(element);
|
|
812
1231
|
return () => observer.disconnect();
|
|
813
1232
|
}, []);
|
|
1233
|
+
useEffect4(() => {
|
|
1234
|
+
let active = true;
|
|
1235
|
+
if (!pageCount) return;
|
|
1236
|
+
const loadBaseSize = async () => {
|
|
1237
|
+
try {
|
|
1238
|
+
const size = await engine.getPageDimensions(0);
|
|
1239
|
+
if (!active || !size.width || !size.height) return;
|
|
1240
|
+
setBasePageSize(size);
|
|
1241
|
+
} catch {
|
|
1242
|
+
}
|
|
1243
|
+
};
|
|
1244
|
+
loadBaseSize();
|
|
1245
|
+
return () => {
|
|
1246
|
+
active = false;
|
|
1247
|
+
};
|
|
1248
|
+
}, [engine, pageCount]);
|
|
1249
|
+
useEffect4(() => {
|
|
1250
|
+
if (scrollToPageSignal == null) return;
|
|
1251
|
+
const root = viewerRef.current;
|
|
1252
|
+
const target = pageRefs.current[scrollToPageSignal];
|
|
1253
|
+
if (root) {
|
|
1254
|
+
setDocumentState({ currentPage: scrollToPageSignal + 1 });
|
|
1255
|
+
jumpRef.current = true;
|
|
1256
|
+
if (jumpTimerRef.current) clearTimeout(jumpTimerRef.current);
|
|
1257
|
+
const previousBehavior = root.style.scrollBehavior;
|
|
1258
|
+
root.style.scrollBehavior = "auto";
|
|
1259
|
+
let targetTop = null;
|
|
1260
|
+
if (target) {
|
|
1261
|
+
targetTop = target.offsetTop;
|
|
1262
|
+
} else if (basePageSize && availableWidth) {
|
|
1263
|
+
const fitScale = Math.min(1, Math.max(0, availableWidth - 48) / basePageSize.width);
|
|
1264
|
+
const estimatedPageHeight = basePageSize.height * fitScale * zoom + 64;
|
|
1265
|
+
targetTop = Math.max(0, estimatedPageHeight * scrollToPageSignal);
|
|
1266
|
+
} else if (pageCount > 1) {
|
|
1267
|
+
const maxScroll = Math.max(0, root.scrollHeight - root.clientHeight);
|
|
1268
|
+
const ratio = scrollToPageSignal / Math.max(1, pageCount - 1);
|
|
1269
|
+
targetTop = Math.max(0, maxScroll * ratio);
|
|
1270
|
+
}
|
|
1271
|
+
if (targetTop != null) {
|
|
1272
|
+
root.scrollTop = Math.max(0, targetTop - 12);
|
|
1273
|
+
}
|
|
1274
|
+
requestAnimationFrame(() => {
|
|
1275
|
+
root.style.scrollBehavior = previousBehavior;
|
|
1276
|
+
});
|
|
1277
|
+
jumpTimerRef.current = setTimeout(() => {
|
|
1278
|
+
jumpRef.current = false;
|
|
1279
|
+
}, 250);
|
|
1280
|
+
}
|
|
1281
|
+
setDocumentState({ scrollToPageSignal: null });
|
|
1282
|
+
}, [scrollToPageSignal, setDocumentState, basePageSize, availableWidth, zoom, pageCount]);
|
|
1283
|
+
useEffect4(() => {
|
|
1284
|
+
setPageSizes({});
|
|
1285
|
+
}, [zoom]);
|
|
814
1286
|
useEffect4(() => {
|
|
815
1287
|
const root = viewerRef.current;
|
|
816
1288
|
if (!root) return;
|
|
1289
|
+
const flushCurrentPage = () => {
|
|
1290
|
+
if (jumpRef.current) return;
|
|
1291
|
+
const ratios = intersectionRatiosRef.current;
|
|
1292
|
+
const entries = Object.entries(ratios).filter(([, ratio]) => ratio > 0);
|
|
1293
|
+
if (!entries.length) return;
|
|
1294
|
+
const currentIndex = currentPage - 1;
|
|
1295
|
+
const currentRatio = ratios[currentIndex] ?? 0;
|
|
1296
|
+
const [bestIndexText, bestRatio] = entries.reduce(
|
|
1297
|
+
(best, candidate) => Number(candidate[1]) > Number(best[1]) ? candidate : best
|
|
1298
|
+
);
|
|
1299
|
+
const bestIndex = Number(bestIndexText);
|
|
1300
|
+
if (!Number.isFinite(bestIndex)) return;
|
|
1301
|
+
const bestPage = bestIndex + 1;
|
|
1302
|
+
const shouldSwitch = bestPage !== currentPage && (currentRatio <= 0 || bestRatio >= currentRatio + 0.1 || bestRatio >= 0.75);
|
|
1303
|
+
if (shouldSwitch) setDocumentState({ currentPage: bestPage });
|
|
1304
|
+
};
|
|
817
1305
|
const observer = new IntersectionObserver((entries) => {
|
|
818
1306
|
entries.forEach((entry) => {
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
1307
|
+
const pageIndex = parseInt(entry.target.getAttribute("data-page-index") || "0");
|
|
1308
|
+
if (!Number.isFinite(pageIndex)) return;
|
|
1309
|
+
intersectionRatiosRef.current[pageIndex] = entry.isIntersecting ? entry.intersectionRatio : 0;
|
|
1310
|
+
});
|
|
1311
|
+
if (frameRef.current != null) cancelAnimationFrame(frameRef.current);
|
|
1312
|
+
frameRef.current = requestAnimationFrame(() => {
|
|
1313
|
+
frameRef.current = null;
|
|
1314
|
+
flushCurrentPage();
|
|
823
1315
|
});
|
|
824
|
-
}, { root, threshold: 0.5 });
|
|
1316
|
+
}, { root, threshold: [0.25, 0.5, 0.75] });
|
|
825
1317
|
const pageElements = root.querySelectorAll(".page-container");
|
|
826
1318
|
pageElements.forEach((el) => observer.observe(el));
|
|
827
1319
|
return () => {
|
|
1320
|
+
if (frameRef.current != null) {
|
|
1321
|
+
cancelAnimationFrame(frameRef.current);
|
|
1322
|
+
frameRef.current = null;
|
|
1323
|
+
}
|
|
828
1324
|
pageElements.forEach((el) => observer.unobserve(el));
|
|
829
1325
|
observer.disconnect();
|
|
830
1326
|
};
|
|
831
1327
|
}, [pageCount, setDocumentState, currentPage]);
|
|
1328
|
+
const virtualOverscan = zoom > 1.35 ? 4 : BASE_OVERSCAN;
|
|
1329
|
+
const virtualAnchor = currentPage - 1;
|
|
1330
|
+
const virtualStart = Math.max(0, virtualAnchor - virtualOverscan);
|
|
1331
|
+
const virtualEnd = Math.min(pageCount - 1, virtualAnchor + virtualOverscan);
|
|
1332
|
+
const fallbackSize = useMemo2(() => {
|
|
1333
|
+
if (basePageSize && availableWidth) {
|
|
1334
|
+
const fitScale = Math.min(1, Math.max(0, availableWidth - 48) / basePageSize.width);
|
|
1335
|
+
return {
|
|
1336
|
+
width: Math.round(basePageSize.width * fitScale * zoom),
|
|
1337
|
+
height: Math.round(basePageSize.height * fitScale * zoom)
|
|
1338
|
+
};
|
|
1339
|
+
}
|
|
1340
|
+
const base = availableWidth ? Math.max(680, availableWidth * 1.3) : 1100;
|
|
1341
|
+
return {
|
|
1342
|
+
width: Math.round((availableWidth ?? 860) - 48),
|
|
1343
|
+
height: Math.round(base * zoom)
|
|
1344
|
+
};
|
|
1345
|
+
}, [basePageSize, availableWidth, zoom]);
|
|
1346
|
+
const averagePageHeight = useMemo2(() => {
|
|
1347
|
+
const heights = Object.values(pageSizes).map((size) => size.height);
|
|
1348
|
+
if (!heights.length) return availableWidth ? Math.max(680, availableWidth * 1.3) : 1100;
|
|
1349
|
+
return Math.round(heights.reduce((sum, h) => sum + h, 0) / heights.length);
|
|
1350
|
+
}, [pageSizes, availableWidth]);
|
|
832
1351
|
const pages = Array.from({ length: pageCount }).map((_, i) => i);
|
|
1352
|
+
const handlePageMeasured = (pageIndex, size) => {
|
|
1353
|
+
setPageSizes((prev) => {
|
|
1354
|
+
const current = prev[pageIndex];
|
|
1355
|
+
if (current && current.width === size.width && current.height === size.height) return prev;
|
|
1356
|
+
return { ...prev, [pageIndex]: size };
|
|
1357
|
+
});
|
|
1358
|
+
};
|
|
833
1359
|
const tools = [
|
|
834
1360
|
{ id: "select", name: "Select", icon: "M15 15l-2 5L9 9l11 4-5 2zm0 0l5 5" },
|
|
835
|
-
{ id: "highlight", name: "
|
|
836
|
-
{ id: "
|
|
1361
|
+
{ id: "highlight", name: "Highlight", icon: "M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" },
|
|
1362
|
+
{ id: "underline", name: "Underline", icon: "M6 3v6a6 6 0 0012 0V3M4 21h16" },
|
|
1363
|
+
{ id: "squiggly", name: "Squiggly", icon: "M3 17c2-4 4-4 6 0s4 4 6 0 4-4 6 0" },
|
|
1364
|
+
{ id: "strikeout", name: "Strike", icon: "M4 12h16M8 6h8M8 18h8" },
|
|
1365
|
+
{ id: "ink", name: "Freehand", icon: "M4 19c4-6 7-9 10-9 3 0 5 2 6 5" },
|
|
837
1366
|
{ id: "comment", name: "Note", icon: "M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" }
|
|
838
1367
|
];
|
|
839
1368
|
return /* @__PURE__ */ jsxs5(
|
|
@@ -841,19 +1370,96 @@ var Viewer = ({ engine }) => {
|
|
|
841
1370
|
{
|
|
842
1371
|
ref: viewerRef,
|
|
843
1372
|
"data-papyrus-theme": uiTheme,
|
|
844
|
-
className: `papyrus-viewer papyrus-theme flex-1 overflow-
|
|
1373
|
+
className: `papyrus-viewer papyrus-theme min-w-0 w-full flex-1 overflow-y-scroll overflow-x-hidden flex flex-col items-center ${paddingY} relative custom-scrollbar scroll-smooth ${isDark ? "bg-[#121212]" : "bg-[#e9ecef]"}`,
|
|
845
1374
|
children: [
|
|
846
|
-
/* @__PURE__ */ jsx5("div", { className: "flex flex-col items-center gap-6 w-full", children: pages.map((idx) => /* @__PURE__ */ jsx5(
|
|
847
|
-
|
|
848
|
-
"button",
|
|
1375
|
+
/* @__PURE__ */ jsx5("div", { className: "flex flex-col items-center gap-6 w-full min-w-0", children: pages.map((idx) => /* @__PURE__ */ jsx5(
|
|
1376
|
+
"div",
|
|
849
1377
|
{
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
1378
|
+
ref: (element) => {
|
|
1379
|
+
pageRefs.current[idx] = element;
|
|
1380
|
+
},
|
|
1381
|
+
"data-page-index": idx,
|
|
1382
|
+
className: "page-container",
|
|
1383
|
+
children: idx >= virtualStart && idx <= virtualEnd ? /* @__PURE__ */ jsx5(
|
|
1384
|
+
PageRenderer_default,
|
|
1385
|
+
{
|
|
1386
|
+
engine,
|
|
1387
|
+
pageIndex: idx,
|
|
1388
|
+
availableWidth: availableWidth ?? void 0,
|
|
1389
|
+
onMeasuredSize: handlePageMeasured
|
|
1390
|
+
}
|
|
1391
|
+
) : /* @__PURE__ */ jsx5(
|
|
1392
|
+
"div",
|
|
1393
|
+
{
|
|
1394
|
+
className: `inline-block mb-10 shadow-2xl border ${isDark ? "bg-[#0f0f0f] border-[#2b2b2b]" : "bg-white border-gray-200"}`,
|
|
1395
|
+
style: {
|
|
1396
|
+
width: pageSizes[idx]?.width ?? fallbackSize.width,
|
|
1397
|
+
height: pageSizes[idx]?.height ?? Math.max(fallbackSize.height, averagePageHeight)
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
)
|
|
854
1401
|
},
|
|
855
|
-
|
|
856
|
-
)) })
|
|
1402
|
+
idx
|
|
1403
|
+
)) }),
|
|
1404
|
+
/* @__PURE__ */ jsx5("div", { className: `papyrus-tool-dock sticky ${toolDockPosition} w-full flex justify-center pointer-events-none z-[70]`, children: /* @__PURE__ */ jsxs5("div", { className: `pointer-events-auto shadow-2xl rounded-2xl p-2 flex items-center border z-[80] ${isDark ? "bg-[#2a2a2a]/90 border-[#3a3a3a] backdrop-blur-xl" : "bg-white/95 border-gray-100 backdrop-blur-md"}`, children: [
|
|
1405
|
+
tools.map((tool) => /* @__PURE__ */ jsx5(
|
|
1406
|
+
"button",
|
|
1407
|
+
{
|
|
1408
|
+
title: tool.name,
|
|
1409
|
+
"aria-label": tool.name,
|
|
1410
|
+
onClick: () => setDocumentState({ activeTool: tool.id }),
|
|
1411
|
+
className: `w-10 h-10 rounded-xl flex items-center justify-center transition-all ${activeTool === tool.id ? "text-white shadow-lg" : "text-gray-400"}`,
|
|
1412
|
+
style: activeTool === tool.id ? { backgroundColor: accentColor } : void 0,
|
|
1413
|
+
children: /* @__PURE__ */ jsx5("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx5("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: tool.icon }) })
|
|
1414
|
+
},
|
|
1415
|
+
tool.id
|
|
1416
|
+
)),
|
|
1417
|
+
/* @__PURE__ */ jsx5("div", { className: "w-px h-7 mx-2 bg-white/10" }),
|
|
1418
|
+
/* @__PURE__ */ jsxs5("div", { ref: colorPickerRef, className: "relative", children: [
|
|
1419
|
+
/* @__PURE__ */ jsx5(
|
|
1420
|
+
"button",
|
|
1421
|
+
{
|
|
1422
|
+
title: "Cor do marcador",
|
|
1423
|
+
"aria-label": "Cor do marcador",
|
|
1424
|
+
onClick: () => setColorPickerOpen((prev) => !prev),
|
|
1425
|
+
className: "w-9 h-9 rounded-full flex items-center justify-center border transition-all cursor-pointer relative",
|
|
1426
|
+
style: { borderColor: annotationColor },
|
|
1427
|
+
children: /* @__PURE__ */ jsx5("span", { className: "w-5 h-5 rounded-full", style: { backgroundColor: annotationColor } })
|
|
1428
|
+
}
|
|
1429
|
+
),
|
|
1430
|
+
colorPickerOpen && /* @__PURE__ */ jsxs5("div", { className: `absolute bottom-full left-1/2 -translate-x-1/2 mb-3 w-48 rounded-xl border p-3 shadow-2xl overflow-hidden ${isDark ? "bg-[#1f1f1f] border-[#333]" : "bg-white border-gray-200"}`, children: [
|
|
1431
|
+
/* @__PURE__ */ jsx5("div", { className: "grid grid-cols-4 gap-2 mb-3", children: colorPalette.map((color) => /* @__PURE__ */ jsx5(
|
|
1432
|
+
"button",
|
|
1433
|
+
{
|
|
1434
|
+
onClick: () => {
|
|
1435
|
+
setAnnotationColor(color);
|
|
1436
|
+
setColorPickerOpen(false);
|
|
1437
|
+
},
|
|
1438
|
+
className: "w-7 h-7 rounded-full border transition-all",
|
|
1439
|
+
style: { backgroundColor: color, borderColor: color === annotationColor ? "#fff" : "transparent" }
|
|
1440
|
+
},
|
|
1441
|
+
color
|
|
1442
|
+
)) }),
|
|
1443
|
+
/* @__PURE__ */ jsxs5("div", { className: "flex items-center gap-2 w-full", children: [
|
|
1444
|
+
/* @__PURE__ */ jsx5("span", { className: "text-[10px] uppercase tracking-widest text-gray-400 shrink-0", children: "Hex" }),
|
|
1445
|
+
/* @__PURE__ */ jsx5(
|
|
1446
|
+
"input",
|
|
1447
|
+
{
|
|
1448
|
+
type: "text",
|
|
1449
|
+
value: annotationColor.toUpperCase(),
|
|
1450
|
+
onChange: (e) => {
|
|
1451
|
+
const next = e.target.value.trim();
|
|
1452
|
+
if (next.startsWith("#") && (next.length === 4 || next.length === 7)) {
|
|
1453
|
+
setAnnotationColor(next);
|
|
1454
|
+
}
|
|
1455
|
+
},
|
|
1456
|
+
className: `flex-1 min-w-0 w-full text-xs rounded-md px-2 py-1 border ${isDark ? "bg-[#2a2a2a] border-[#444] text-white" : "bg-gray-100 border-gray-200 text-gray-700"}`
|
|
1457
|
+
}
|
|
1458
|
+
)
|
|
1459
|
+
] })
|
|
1460
|
+
] })
|
|
1461
|
+
] })
|
|
1462
|
+
] }) })
|
|
857
1463
|
]
|
|
858
1464
|
}
|
|
859
1465
|
);
|