@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.js
CHANGED
|
@@ -57,16 +57,31 @@ var Topbar = ({
|
|
|
57
57
|
triggerScrollToPage
|
|
58
58
|
} = (0, import_core.useViewerStore)();
|
|
59
59
|
const fileInputRef = (0, import_react.useRef)(null);
|
|
60
|
+
const zoomTimerRef = (0, import_react.useRef)(null);
|
|
61
|
+
const pendingZoomRef = (0, import_react.useRef)(null);
|
|
60
62
|
const [pageInput, setPageInput] = (0, import_react.useState)(currentPage.toString());
|
|
63
|
+
const pageDigits = Math.max(2, String(pageCount || 1).length);
|
|
61
64
|
const [showPageThemes, setShowPageThemes] = (0, import_react.useState)(false);
|
|
62
65
|
const isDark = uiTheme === "dark";
|
|
63
66
|
(0, import_react.useEffect)(() => {
|
|
64
67
|
setPageInput(currentPage.toString());
|
|
65
68
|
}, [currentPage]);
|
|
69
|
+
(0, import_react.useEffect)(() => () => {
|
|
70
|
+
if (zoomTimerRef.current) clearTimeout(zoomTimerRef.current);
|
|
71
|
+
}, []);
|
|
66
72
|
const handleZoom = (delta) => {
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
73
|
+
const baseZoom = pendingZoomRef.current ?? zoom;
|
|
74
|
+
const nextZoom = Math.max(0.2, Math.min(5, baseZoom + delta));
|
|
75
|
+
pendingZoomRef.current = nextZoom;
|
|
76
|
+
if (zoomTimerRef.current) return;
|
|
77
|
+
zoomTimerRef.current = setTimeout(() => {
|
|
78
|
+
zoomTimerRef.current = null;
|
|
79
|
+
const targetZoom = pendingZoomRef.current;
|
|
80
|
+
pendingZoomRef.current = null;
|
|
81
|
+
if (targetZoom == null) return;
|
|
82
|
+
engine.setZoom(targetZoom);
|
|
83
|
+
setDocumentState({ zoom: targetZoom });
|
|
84
|
+
}, 80);
|
|
70
85
|
};
|
|
71
86
|
const handlePageChange = (page) => {
|
|
72
87
|
if (pageCount <= 0) return;
|
|
@@ -102,7 +117,8 @@ var Topbar = ({
|
|
|
102
117
|
"input",
|
|
103
118
|
{
|
|
104
119
|
type: "text",
|
|
105
|
-
className: "papyrus-input
|
|
120
|
+
className: "papyrus-input text-center bg-transparent focus:outline-none font-bold text-sm shrink-0",
|
|
121
|
+
style: { width: `${pageDigits + 1.75}ch` },
|
|
106
122
|
value: pageInput,
|
|
107
123
|
onChange: (e) => setPageInput(e.target.value),
|
|
108
124
|
onKeyDown: (e) => e.key === "Enter" && handlePageChange(parseInt(pageInput)),
|
|
@@ -194,22 +210,44 @@ var withAlpha = (hex, alpha) => {
|
|
|
194
210
|
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
195
211
|
};
|
|
196
212
|
var Thumbnail = ({ engine, pageIndex, active, isDark, accentColor, onClick }) => {
|
|
213
|
+
const wrapperRef = (0, import_react2.useRef)(null);
|
|
197
214
|
const canvasRef = (0, import_react2.useRef)(null);
|
|
198
215
|
const htmlRef = (0, import_react2.useRef)(null);
|
|
199
216
|
const accentSoft = withAlpha(accentColor, 0.12);
|
|
200
217
|
const renderTargetType = engine.getRenderTargetType?.() ?? "canvas";
|
|
218
|
+
const [isVisible, setIsVisible] = (0, import_react2.useState)(false);
|
|
219
|
+
(0, import_react2.useEffect)(() => {
|
|
220
|
+
const target = wrapperRef.current;
|
|
221
|
+
if (!target) return;
|
|
222
|
+
if (typeof IntersectionObserver === "undefined") {
|
|
223
|
+
setIsVisible(true);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const root = target.closest(".custom-scrollbar");
|
|
227
|
+
const observer = new IntersectionObserver((entries) => {
|
|
228
|
+
entries.forEach((entry) => {
|
|
229
|
+
if (entry.isIntersecting) {
|
|
230
|
+
setIsVisible(true);
|
|
231
|
+
observer.disconnect();
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
}, { root: root ?? null, rootMargin: "200px" });
|
|
235
|
+
observer.observe(target);
|
|
236
|
+
return () => observer.disconnect();
|
|
237
|
+
}, []);
|
|
201
238
|
(0, import_react2.useEffect)(() => {
|
|
202
|
-
if (renderTargetType === "element") return;
|
|
239
|
+
if (renderTargetType === "element" || !isVisible) return;
|
|
203
240
|
const target = canvasRef.current;
|
|
204
241
|
if (target) {
|
|
205
242
|
engine.renderPage(pageIndex, target, 0.15).catch((err) => {
|
|
206
243
|
console.error("[Papyrus] Thumbnail render failed:", err);
|
|
207
244
|
});
|
|
208
245
|
}
|
|
209
|
-
}, [engine, pageIndex, renderTargetType]);
|
|
246
|
+
}, [engine, pageIndex, renderTargetType, isVisible]);
|
|
210
247
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
211
248
|
"div",
|
|
212
249
|
{
|
|
250
|
+
ref: wrapperRef,
|
|
213
251
|
onClick,
|
|
214
252
|
className: `p-3 cursor-pointer transition-all rounded-lg border-2 ${active ? "shadow-sm" : "border-transparent"}`,
|
|
215
253
|
style: active ? { borderColor: accentColor, backgroundColor: accentSoft } : void 0,
|
|
@@ -382,6 +420,7 @@ var SidebarRight = ({ engine }) => {
|
|
|
382
420
|
const searchService = new import_core3.SearchService(engine);
|
|
383
421
|
const isDark = uiTheme === "dark";
|
|
384
422
|
const accentSoft = withAlpha2(accentColor, 0.12);
|
|
423
|
+
const resultsCount = searchResults.length;
|
|
385
424
|
const handleSearch = async (e) => {
|
|
386
425
|
e.preventDefault();
|
|
387
426
|
if (!query.trim()) {
|
|
@@ -421,7 +460,7 @@ var SidebarRight = ({ engine }) => {
|
|
|
421
460
|
}
|
|
422
461
|
)
|
|
423
462
|
] }),
|
|
424
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { onClick: () => toggleSidebarRight(), className: "text-gray-400 hover:text-red-500 transition-colors", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) }) })
|
|
463
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { onClick: () => toggleSidebarRight(), className: "papyrus-unstyled-button text-gray-400 hover:text-red-500 transition-colors", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) }) })
|
|
425
464
|
] }),
|
|
426
465
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "flex-1 overflow-y-auto p-4 custom-scrollbar bg-opacity-50", children: sidebarRightTab === "search" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "space-y-4", children: [
|
|
427
466
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("form", { onSubmit: handleSearch, className: "relative mb-6", children: [
|
|
@@ -435,7 +474,8 @@ var SidebarRight = ({ engine }) => {
|
|
|
435
474
|
onChange: (e) => setQuery(e.target.value)
|
|
436
475
|
}
|
|
437
476
|
),
|
|
438
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("
|
|
477
|
+
resultsCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "absolute right-9 top-2.5 text-[10px] font-bold text-gray-400", children: resultsCount }),
|
|
478
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "submit", className: "papyrus-unstyled-button absolute right-3 top-2.5 text-gray-400 transition-colors", style: { color: accentColor }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2.5, d: "M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" }) }) })
|
|
439
479
|
] }),
|
|
440
480
|
isSearching && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex flex-col items-center justify-center py-12 space-y-3", children: [
|
|
441
481
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "w-6 h-6 border-2 border-t-transparent rounded-full animate-spin", style: { borderColor: accentColor } }),
|
|
@@ -445,7 +485,9 @@ var SidebarRight = ({ engine }) => {
|
|
|
445
485
|
"div",
|
|
446
486
|
{
|
|
447
487
|
onClick: () => {
|
|
448
|
-
|
|
488
|
+
const page = res.pageIndex + 1;
|
|
489
|
+
engine.goToPage(page);
|
|
490
|
+
setDocumentState({ activeSearchIndex: idx, currentPage: page });
|
|
449
491
|
triggerScrollToPage(res.pageIndex);
|
|
450
492
|
},
|
|
451
493
|
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"}`,
|
|
@@ -521,7 +563,7 @@ var import_react4 = require("react");
|
|
|
521
563
|
var import_core4 = require("@papyrus-sdk/core");
|
|
522
564
|
var import_types = require("@papyrus-sdk/types");
|
|
523
565
|
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
524
|
-
var PageRenderer = ({ engine, pageIndex, availableWidth }) => {
|
|
566
|
+
var PageRenderer = ({ engine, pageIndex, availableWidth, onMeasuredSize }) => {
|
|
525
567
|
const containerRef = (0, import_react4.useRef)(null);
|
|
526
568
|
const canvasRef = (0, import_react4.useRef)(null);
|
|
527
569
|
const htmlLayerRef = (0, import_react4.useRef)(null);
|
|
@@ -531,6 +573,10 @@ var PageRenderer = ({ engine, pageIndex, availableWidth }) => {
|
|
|
531
573
|
const [isDragging, setIsDragging] = (0, import_react4.useState)(false);
|
|
532
574
|
const [startPos, setStartPos] = (0, import_react4.useState)({ x: 0, y: 0 });
|
|
533
575
|
const [currentRect, setCurrentRect] = (0, import_react4.useState)({ x: 0, y: 0, w: 0, h: 0 });
|
|
576
|
+
const [textLayerVersion, setTextLayerVersion] = (0, import_react4.useState)(0);
|
|
577
|
+
const [selectionMenu, setSelectionMenu] = (0, import_react4.useState)(null);
|
|
578
|
+
const [isInkDrawing, setIsInkDrawing] = (0, import_react4.useState)(false);
|
|
579
|
+
const [inkPoints, setInkPoints] = (0, import_react4.useState)([]);
|
|
534
580
|
const {
|
|
535
581
|
zoom,
|
|
536
582
|
rotation,
|
|
@@ -543,10 +589,20 @@ var PageRenderer = ({ engine, pageIndex, availableWidth }) => {
|
|
|
543
589
|
removeAnnotation,
|
|
544
590
|
selectedAnnotationId,
|
|
545
591
|
setSelectedAnnotation,
|
|
546
|
-
accentColor
|
|
592
|
+
accentColor,
|
|
593
|
+
annotationColor,
|
|
594
|
+
searchQuery,
|
|
595
|
+
searchResults,
|
|
596
|
+
activeSearchIndex
|
|
547
597
|
} = (0, import_core4.useViewerStore)();
|
|
548
598
|
const renderTargetType = engine.getRenderTargetType?.() ?? "canvas";
|
|
549
599
|
const isElementRender = renderTargetType === "element";
|
|
600
|
+
const textMarkupTools = /* @__PURE__ */ new Set(["highlight", "underline", "squiggly", "strikeout"]);
|
|
601
|
+
const canSelectText = activeTool === "select" || textMarkupTools.has(activeTool);
|
|
602
|
+
const hasSearchHits = (0, import_react4.useMemo)(
|
|
603
|
+
() => Boolean(searchQuery?.trim()) && searchResults.some((res) => res.pageIndex === pageIndex),
|
|
604
|
+
[searchQuery, searchResults, pageIndex]
|
|
605
|
+
);
|
|
550
606
|
(0, import_react4.useEffect)(() => {
|
|
551
607
|
let active = true;
|
|
552
608
|
const loadSize = async () => {
|
|
@@ -579,8 +635,14 @@ var PageRenderer = ({ engine, pageIndex, availableWidth }) => {
|
|
|
579
635
|
};
|
|
580
636
|
}, [pageSize, zoom, fitScale]);
|
|
581
637
|
(0, import_react4.useEffect)(() => {
|
|
582
|
-
if (
|
|
583
|
-
|
|
638
|
+
if (!displaySize || !onMeasuredSize) return;
|
|
639
|
+
onMeasuredSize(pageIndex, {
|
|
640
|
+
width: Math.round(displaySize.width),
|
|
641
|
+
height: Math.round(displaySize.height)
|
|
642
|
+
});
|
|
643
|
+
}, [displaySize, onMeasuredSize, pageIndex]);
|
|
644
|
+
(0, import_react4.useEffect)(() => {
|
|
645
|
+
if (scrollToPageSignal === pageIndex) {
|
|
584
646
|
setDocumentState({ scrollToPageSignal: null });
|
|
585
647
|
}
|
|
586
648
|
}, [scrollToPageSignal, pageIndex, setDocumentState]);
|
|
@@ -592,10 +654,15 @@ var PageRenderer = ({ engine, pageIndex, availableWidth }) => {
|
|
|
592
654
|
setLoading(true);
|
|
593
655
|
try {
|
|
594
656
|
const RENDER_SCALE = 2;
|
|
595
|
-
const
|
|
596
|
-
|
|
657
|
+
const canvasRenderScale = isElementRender ? 1 : RENDER_SCALE * fitScale;
|
|
658
|
+
const textRenderScale = isElementRender ? 1 : fitScale;
|
|
659
|
+
if (!isElementRender && canvasRef.current && displaySize) {
|
|
660
|
+
canvasRef.current.style.width = `${displaySize.width}px`;
|
|
661
|
+
canvasRef.current.style.height = `${displaySize.height}px`;
|
|
662
|
+
}
|
|
663
|
+
await engine.renderPage(pageIndex, renderTarget, canvasRenderScale);
|
|
597
664
|
if (!isElementRender && !pageSize && canvasRef.current) {
|
|
598
|
-
const denom =
|
|
665
|
+
const denom = canvasRenderScale * Math.max(zoom, 0.01);
|
|
599
666
|
if (denom > 0) {
|
|
600
667
|
setPageSize({
|
|
601
668
|
width: canvasRef.current.width / denom,
|
|
@@ -606,7 +673,7 @@ var PageRenderer = ({ engine, pageIndex, availableWidth }) => {
|
|
|
606
673
|
if (!active || !textLayerRef.current) return;
|
|
607
674
|
if (!isElementRender) {
|
|
608
675
|
textLayerRef.current.innerHTML = "";
|
|
609
|
-
await engine.renderTextLayer(pageIndex, textLayerRef.current,
|
|
676
|
+
await engine.renderTextLayer(pageIndex, textLayerRef.current, textRenderScale);
|
|
610
677
|
}
|
|
611
678
|
if (!active || !textLayerRef.current) return;
|
|
612
679
|
if (!isElementRender && displaySize) {
|
|
@@ -617,6 +684,7 @@ var PageRenderer = ({ engine, pageIndex, availableWidth }) => {
|
|
|
617
684
|
textLayerRef.current.style.width = `${displaySize.width}px`;
|
|
618
685
|
textLayerRef.current.style.height = `${displaySize.height}px`;
|
|
619
686
|
}
|
|
687
|
+
setTextLayerVersion((v) => v + 1);
|
|
620
688
|
} catch (err) {
|
|
621
689
|
if (!active) return;
|
|
622
690
|
console.error("[Papyrus] Falha na renderiza\xE7\xE3o:", err);
|
|
@@ -629,8 +697,68 @@ var PageRenderer = ({ engine, pageIndex, availableWidth }) => {
|
|
|
629
697
|
active = false;
|
|
630
698
|
};
|
|
631
699
|
}, [engine, pageIndex, zoom, rotation, isElementRender, fitScale, displaySize, pageSize]);
|
|
700
|
+
(0, import_react4.useEffect)(() => {
|
|
701
|
+
if (isElementRender) return;
|
|
702
|
+
const layer = textLayerRef.current;
|
|
703
|
+
if (!layer) return;
|
|
704
|
+
const query = searchQuery?.trim().toLowerCase();
|
|
705
|
+
const existingMarks = Array.from(layer.querySelectorAll("mark.papyrus-search-hit"));
|
|
706
|
+
existingMarks.forEach((mark) => {
|
|
707
|
+
const parent = mark.parentNode;
|
|
708
|
+
if (!parent) return;
|
|
709
|
+
while (mark.firstChild) parent.insertBefore(mark.firstChild, mark);
|
|
710
|
+
parent.removeChild(mark);
|
|
711
|
+
parent.normalize();
|
|
712
|
+
});
|
|
713
|
+
if (!query || !hasSearchHits) return;
|
|
714
|
+
const nodes = [];
|
|
715
|
+
const walker = document.createTreeWalker(layer, NodeFilter.SHOW_TEXT, {
|
|
716
|
+
acceptNode(node) {
|
|
717
|
+
const text = node.nodeValue ?? "";
|
|
718
|
+
if (!text.trim()) return NodeFilter.FILTER_REJECT;
|
|
719
|
+
return NodeFilter.FILTER_ACCEPT;
|
|
720
|
+
}
|
|
721
|
+
});
|
|
722
|
+
while (walker.nextNode()) {
|
|
723
|
+
nodes.push(walker.currentNode);
|
|
724
|
+
}
|
|
725
|
+
nodes.forEach((textNode) => {
|
|
726
|
+
const text = textNode.nodeValue ?? "";
|
|
727
|
+
const lower = text.toLowerCase();
|
|
728
|
+
if (!lower.includes(query)) return;
|
|
729
|
+
const fragment = document.createDocumentFragment();
|
|
730
|
+
let cursor = 0;
|
|
731
|
+
let index = lower.indexOf(query, cursor);
|
|
732
|
+
while (index !== -1) {
|
|
733
|
+
if (index > cursor) {
|
|
734
|
+
fragment.appendChild(document.createTextNode(text.slice(cursor, index)));
|
|
735
|
+
}
|
|
736
|
+
const mark = document.createElement("mark");
|
|
737
|
+
mark.className = "papyrus-search-hit";
|
|
738
|
+
mark.textContent = text.slice(index, index + query.length);
|
|
739
|
+
fragment.appendChild(mark);
|
|
740
|
+
cursor = index + query.length;
|
|
741
|
+
index = lower.indexOf(query, cursor);
|
|
742
|
+
}
|
|
743
|
+
if (cursor < text.length) {
|
|
744
|
+
fragment.appendChild(document.createTextNode(text.slice(cursor)));
|
|
745
|
+
}
|
|
746
|
+
const parent = textNode.parentNode;
|
|
747
|
+
if (parent) parent.replaceChild(fragment, textNode);
|
|
748
|
+
});
|
|
749
|
+
}, [searchQuery, hasSearchHits, pageIndex, isElementRender, activeSearchIndex, textLayerVersion]);
|
|
632
750
|
const handleMouseDown = (e) => {
|
|
633
|
-
|
|
751
|
+
setSelectionMenu(null);
|
|
752
|
+
if (activeTool === "ink") {
|
|
753
|
+
const rect2 = containerRef.current?.getBoundingClientRect();
|
|
754
|
+
if (!rect2) return;
|
|
755
|
+
const x2 = (e.clientX - rect2.left) / rect2.width;
|
|
756
|
+
const y2 = (e.clientY - rect2.top) / rect2.height;
|
|
757
|
+
setIsInkDrawing(true);
|
|
758
|
+
setInkPoints([{ x: x2, y: y2 }]);
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
if (canSelectText) return;
|
|
634
762
|
const rect = containerRef.current?.getBoundingClientRect();
|
|
635
763
|
if (!rect) return;
|
|
636
764
|
setIsDragging(true);
|
|
@@ -640,6 +768,14 @@ var PageRenderer = ({ engine, pageIndex, availableWidth }) => {
|
|
|
640
768
|
setCurrentRect({ x, y, w: 0, h: 0 });
|
|
641
769
|
};
|
|
642
770
|
const handleMouseMove = (e) => {
|
|
771
|
+
if (isInkDrawing) {
|
|
772
|
+
const rect2 = containerRef.current?.getBoundingClientRect();
|
|
773
|
+
if (!rect2) return;
|
|
774
|
+
const x = (e.clientX - rect2.left) / rect2.width;
|
|
775
|
+
const y = (e.clientY - rect2.top) / rect2.height;
|
|
776
|
+
setInkPoints((prev) => [...prev, { x, y }]);
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
643
779
|
if (!isDragging) return;
|
|
644
780
|
const rect = containerRef.current?.getBoundingClientRect();
|
|
645
781
|
if (!rect) return;
|
|
@@ -653,6 +789,115 @@ var PageRenderer = ({ engine, pageIndex, availableWidth }) => {
|
|
|
653
789
|
});
|
|
654
790
|
};
|
|
655
791
|
const handleMouseUp = (e) => {
|
|
792
|
+
if (isInkDrawing) {
|
|
793
|
+
setIsInkDrawing(false);
|
|
794
|
+
if (inkPoints.length > 1) {
|
|
795
|
+
const xs = inkPoints.map((p) => p.x);
|
|
796
|
+
const ys = inkPoints.map((p) => p.y);
|
|
797
|
+
const minX = Math.min(...xs);
|
|
798
|
+
const maxX = Math.max(...xs);
|
|
799
|
+
const minY = Math.min(...ys);
|
|
800
|
+
const maxY = Math.max(...ys);
|
|
801
|
+
const width = Math.max(maxX - minX, 5e-4);
|
|
802
|
+
const height = Math.max(maxY - minY, 5e-4);
|
|
803
|
+
const path = inkPoints.map((p) => ({
|
|
804
|
+
x: Math.max(0, Math.min(1, p.x)),
|
|
805
|
+
y: Math.max(0, Math.min(1, p.y))
|
|
806
|
+
}));
|
|
807
|
+
addAnnotation({
|
|
808
|
+
id: Math.random().toString(36).substr(2, 9),
|
|
809
|
+
pageIndex,
|
|
810
|
+
type: "ink",
|
|
811
|
+
rect: { x: minX, y: minY, width, height },
|
|
812
|
+
path,
|
|
813
|
+
color: annotationColor,
|
|
814
|
+
createdAt: Date.now()
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
setInkPoints([]);
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
const selection = window.getSelection();
|
|
821
|
+
const selectionText = selection?.toString().trim() ?? "";
|
|
822
|
+
if (selectionText && textLayerRef.current && containerRef.current && selection && selection.rangeCount > 0) {
|
|
823
|
+
const range = selection.getRangeAt(0);
|
|
824
|
+
if (textLayerRef.current.contains(range.commonAncestorContainer)) {
|
|
825
|
+
const containerRect = containerRef.current.getBoundingClientRect();
|
|
826
|
+
const clientRects = Array.from(range.getClientRects());
|
|
827
|
+
const rects = clientRects.filter((r) => r.width > 1 && r.height > 1).map((r) => {
|
|
828
|
+
const x = (r.left - containerRect.left) / containerRect.width;
|
|
829
|
+
const y = (r.top - containerRect.top) / containerRect.height;
|
|
830
|
+
const width = r.width / containerRect.width;
|
|
831
|
+
const height = r.height / containerRect.height;
|
|
832
|
+
return {
|
|
833
|
+
x: Math.max(0, Math.min(1, x)),
|
|
834
|
+
y: Math.max(0, Math.min(1, y)),
|
|
835
|
+
width: Math.max(0, Math.min(1, width)),
|
|
836
|
+
height: Math.max(0, Math.min(1, height))
|
|
837
|
+
};
|
|
838
|
+
});
|
|
839
|
+
const uniqueRects = rects.filter((rect, index, list) => {
|
|
840
|
+
const key = `${Math.round(rect.x * 1e4)}-${Math.round(rect.y * 1e4)}-${Math.round(rect.width * 1e4)}-${Math.round(rect.height * 1e4)}`;
|
|
841
|
+
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;
|
|
842
|
+
});
|
|
843
|
+
const mergedRects = uniqueRects.reduce((acc, rect) => {
|
|
844
|
+
const target = acc.find((r) => {
|
|
845
|
+
const closeY = Math.abs(r.y - rect.y) < 2e-3 && Math.abs(r.height - rect.height) < 2e-3;
|
|
846
|
+
const overlaps = rect.x <= r.x + r.width + 2e-3 && rect.x + rect.width >= r.x - 2e-3;
|
|
847
|
+
return closeY && overlaps;
|
|
848
|
+
});
|
|
849
|
+
if (!target) {
|
|
850
|
+
acc.push({ ...rect });
|
|
851
|
+
return acc;
|
|
852
|
+
}
|
|
853
|
+
const left = Math.min(target.x, rect.x);
|
|
854
|
+
const right = Math.max(target.x + target.width, rect.x + rect.width);
|
|
855
|
+
target.x = left;
|
|
856
|
+
target.width = right - left;
|
|
857
|
+
return acc;
|
|
858
|
+
}, []);
|
|
859
|
+
if (mergedRects.length) {
|
|
860
|
+
const xs = mergedRects.map((r) => r.x);
|
|
861
|
+
const ys = mergedRects.map((r) => r.y);
|
|
862
|
+
const xe = mergedRects.map((r) => r.x + r.width);
|
|
863
|
+
const ye = mergedRects.map((r) => r.y + r.height);
|
|
864
|
+
const rect = {
|
|
865
|
+
x: Math.min(...xs),
|
|
866
|
+
y: Math.min(...ys),
|
|
867
|
+
width: Math.max(...xe) - Math.min(...xs),
|
|
868
|
+
height: Math.max(...ye) - Math.min(...ys)
|
|
869
|
+
};
|
|
870
|
+
if (textMarkupTools.has(activeTool)) {
|
|
871
|
+
addAnnotation({
|
|
872
|
+
id: Math.random().toString(36).substr(2, 9),
|
|
873
|
+
pageIndex,
|
|
874
|
+
type: activeTool,
|
|
875
|
+
rect,
|
|
876
|
+
rects: mergedRects,
|
|
877
|
+
color: annotationColor,
|
|
878
|
+
content: selectionText,
|
|
879
|
+
createdAt: Date.now()
|
|
880
|
+
});
|
|
881
|
+
selection.removeAllRanges();
|
|
882
|
+
setSelectionMenu(null);
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
if (activeTool === "select") {
|
|
886
|
+
const anchorX = (rect.x + rect.width) * containerRect.width;
|
|
887
|
+
const anchorY = rect.y * containerRect.height;
|
|
888
|
+
setSelectionMenu({
|
|
889
|
+
rects: mergedRects,
|
|
890
|
+
rect,
|
|
891
|
+
text: selectionText,
|
|
892
|
+
anchor: {
|
|
893
|
+
x: Math.max(12, Math.min(containerRect.width - 12, anchorX)),
|
|
894
|
+
y: Math.max(12, anchorY - 32)
|
|
895
|
+
}
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
656
901
|
if (isDragging) {
|
|
657
902
|
setIsDragging(false);
|
|
658
903
|
if (currentRect.w > 5 && currentRect.h > 5) {
|
|
@@ -668,7 +913,7 @@ var PageRenderer = ({ engine, pageIndex, availableWidth }) => {
|
|
|
668
913
|
width: currentRect.w / rect.width,
|
|
669
914
|
height: currentRect.h / rect.height
|
|
670
915
|
},
|
|
671
|
-
color: activeTool === "highlight" ?
|
|
916
|
+
color: activeTool === "highlight" ? annotationColor : accentColor,
|
|
672
917
|
content: activeTool === "text" || activeTool === "comment" ? "" : void 0,
|
|
673
918
|
createdAt: Date.now()
|
|
674
919
|
});
|
|
@@ -678,7 +923,6 @@ var PageRenderer = ({ engine, pageIndex, availableWidth }) => {
|
|
|
678
923
|
return;
|
|
679
924
|
}
|
|
680
925
|
if (activeTool === "select") {
|
|
681
|
-
const selection = window.getSelection();
|
|
682
926
|
const selectedText = selection?.toString().trim();
|
|
683
927
|
if (selectedText) {
|
|
684
928
|
import_core4.papyrusEvents.emit(import_types.PapyrusEventType.TEXT_SELECTED, {
|
|
@@ -704,7 +948,7 @@ var PageRenderer = ({ engine, pageIndex, availableWidth }) => {
|
|
|
704
948
|
"div",
|
|
705
949
|
{
|
|
706
950
|
ref: containerRef,
|
|
707
|
-
className: `relative inline-block shadow-2xl bg-white mb-10 transition-all ${
|
|
951
|
+
className: `relative inline-block shadow-2xl bg-white mb-10 transition-all ${canSelectText ? "" : "no-select cursor-crosshair"}`,
|
|
708
952
|
style: { scrollMarginTop: "20px", minHeight: "100px" },
|
|
709
953
|
onMouseDown: handleMouseDown,
|
|
710
954
|
onMouseMove: handleMouseMove,
|
|
@@ -733,7 +977,7 @@ var PageRenderer = ({ engine, pageIndex, availableWidth }) => {
|
|
|
733
977
|
ref: textLayerRef,
|
|
734
978
|
className: "textLayer",
|
|
735
979
|
style: {
|
|
736
|
-
pointerEvents: isElementRender ? "none" :
|
|
980
|
+
pointerEvents: isElementRender ? "none" : canSelectText ? "auto" : "none",
|
|
737
981
|
display: isElementRender ? "none" : "block"
|
|
738
982
|
}
|
|
739
983
|
}
|
|
@@ -743,8 +987,9 @@ var PageRenderer = ({ engine, pageIndex, availableWidth }) => {
|
|
|
743
987
|
{
|
|
744
988
|
className: "absolute border-2 z-[40] pointer-events-none",
|
|
745
989
|
style: {
|
|
746
|
-
borderColor: accentColor,
|
|
747
|
-
backgroundColor: `${accentColor}33`,
|
|
990
|
+
borderColor: activeTool === "highlight" ? annotationColor : accentColor,
|
|
991
|
+
backgroundColor: activeTool === "highlight" ? `${annotationColor}66` : `${accentColor}33`,
|
|
992
|
+
mixBlendMode: activeTool === "highlight" ? "multiply" : void 0,
|
|
748
993
|
left: currentRect.x,
|
|
749
994
|
top: currentRect.y,
|
|
750
995
|
width: currentRect.w,
|
|
@@ -752,6 +997,60 @@ var PageRenderer = ({ engine, pageIndex, availableWidth }) => {
|
|
|
752
997
|
}
|
|
753
998
|
}
|
|
754
999
|
),
|
|
1000
|
+
isInkDrawing && inkPoints.length > 1 && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
1001
|
+
"svg",
|
|
1002
|
+
{
|
|
1003
|
+
className: "absolute inset-0 pointer-events-none z-[45]",
|
|
1004
|
+
viewBox: "0 0 1 1",
|
|
1005
|
+
preserveAspectRatio: "none",
|
|
1006
|
+
style: { width: "100%", height: "100%" },
|
|
1007
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
1008
|
+
"path",
|
|
1009
|
+
{
|
|
1010
|
+
d: inkPoints.map((p, idx) => `${idx === 0 ? "M" : "L"} ${p.x} ${p.y}`).join(" "),
|
|
1011
|
+
fill: "none",
|
|
1012
|
+
stroke: annotationColor,
|
|
1013
|
+
strokeWidth: 8e-3,
|
|
1014
|
+
strokeLinecap: "round",
|
|
1015
|
+
strokeLinejoin: "round"
|
|
1016
|
+
}
|
|
1017
|
+
)
|
|
1018
|
+
}
|
|
1019
|
+
),
|
|
1020
|
+
selectionMenu && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
1021
|
+
"div",
|
|
1022
|
+
{
|
|
1023
|
+
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",
|
|
1024
|
+
style: { left: selectionMenu.anchor.x, top: selectionMenu.anchor.y },
|
|
1025
|
+
children: [
|
|
1026
|
+
{ id: "highlight", label: "Marcar" },
|
|
1027
|
+
{ id: "underline", label: "Sublinhar" },
|
|
1028
|
+
{ id: "squiggly", label: "Onda" },
|
|
1029
|
+
{ id: "strikeout", label: "Risco" }
|
|
1030
|
+
].map((action) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
1031
|
+
"button",
|
|
1032
|
+
{
|
|
1033
|
+
className: "text-[10px] font-bold px-2 py-1 rounded-full hover:bg-gray-100",
|
|
1034
|
+
onClick: () => {
|
|
1035
|
+
addAnnotation({
|
|
1036
|
+
id: Math.random().toString(36).substr(2, 9),
|
|
1037
|
+
pageIndex,
|
|
1038
|
+
type: action.id,
|
|
1039
|
+
rect: selectionMenu.rect,
|
|
1040
|
+
rects: selectionMenu.rects,
|
|
1041
|
+
content: selectionMenu.text,
|
|
1042
|
+
color: annotationColor,
|
|
1043
|
+
createdAt: Date.now()
|
|
1044
|
+
});
|
|
1045
|
+
window.getSelection()?.removeAllRanges();
|
|
1046
|
+
setSelectionMenu(null);
|
|
1047
|
+
},
|
|
1048
|
+
children: action.label
|
|
1049
|
+
},
|
|
1050
|
+
action.id
|
|
1051
|
+
))
|
|
1052
|
+
}
|
|
1053
|
+
),
|
|
755
1054
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "absolute inset-0 pointer-events-none z-20", children: annotations.filter((a) => a.pageIndex === pageIndex).map((ann) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
756
1055
|
AnnotationItem,
|
|
757
1056
|
{
|
|
@@ -769,6 +1068,101 @@ var PageRenderer = ({ engine, pageIndex, availableWidth }) => {
|
|
|
769
1068
|
};
|
|
770
1069
|
var AnnotationItem = ({ ann, isSelected, accentColor, onDelete, onSelect }) => {
|
|
771
1070
|
const isText = ann.type === "text" || ann.type === "comment";
|
|
1071
|
+
const isHighlight = ann.type === "highlight";
|
|
1072
|
+
const isMarkup = ann.type === "highlight" || ann.type === "underline" || ann.type === "squiggly" || ann.type === "strikeout";
|
|
1073
|
+
const rects = ann.rects && ann.rects.length > 0 ? ann.rects : [ann.rect];
|
|
1074
|
+
const isInk = ann.type === "ink" && ann.path && ann.path.length > 1;
|
|
1075
|
+
const renderMarkupRects = () => {
|
|
1076
|
+
if (!isMarkup) return null;
|
|
1077
|
+
return rects.map((r, idx) => {
|
|
1078
|
+
const left = ann.rect.width ? (r.x - ann.rect.x) / ann.rect.width * 100 : 0;
|
|
1079
|
+
const top = ann.rect.height ? (r.y - ann.rect.y) / ann.rect.height * 100 : 0;
|
|
1080
|
+
const width = ann.rect.width ? r.width / ann.rect.width * 100 : 100;
|
|
1081
|
+
const height = ann.rect.height ? r.height / ann.rect.height * 100 : 100;
|
|
1082
|
+
if (ann.type === "highlight") {
|
|
1083
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
1084
|
+
"div",
|
|
1085
|
+
{
|
|
1086
|
+
className: "absolute rounded-sm",
|
|
1087
|
+
style: {
|
|
1088
|
+
left: `${left}%`,
|
|
1089
|
+
top: `${top}%`,
|
|
1090
|
+
width: `${width}%`,
|
|
1091
|
+
height: `${height}%`,
|
|
1092
|
+
backgroundColor: `${ann.color}88`,
|
|
1093
|
+
mixBlendMode: "multiply"
|
|
1094
|
+
}
|
|
1095
|
+
},
|
|
1096
|
+
idx
|
|
1097
|
+
);
|
|
1098
|
+
}
|
|
1099
|
+
const lineStyle = {
|
|
1100
|
+
left: `${left}%`,
|
|
1101
|
+
width: `${width}%`
|
|
1102
|
+
};
|
|
1103
|
+
if (ann.type === "underline") {
|
|
1104
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
1105
|
+
"div",
|
|
1106
|
+
{
|
|
1107
|
+
className: "absolute",
|
|
1108
|
+
style: {
|
|
1109
|
+
...lineStyle,
|
|
1110
|
+
top: `calc(${top}% + ${height}% - 2px)`,
|
|
1111
|
+
height: "2px",
|
|
1112
|
+
backgroundColor: ann.color
|
|
1113
|
+
}
|
|
1114
|
+
},
|
|
1115
|
+
idx
|
|
1116
|
+
);
|
|
1117
|
+
}
|
|
1118
|
+
if (ann.type === "strikeout") {
|
|
1119
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
1120
|
+
"div",
|
|
1121
|
+
{
|
|
1122
|
+
className: "absolute",
|
|
1123
|
+
style: {
|
|
1124
|
+
...lineStyle,
|
|
1125
|
+
top: `calc(${top}% + ${height * 0.5}% - 1px)`,
|
|
1126
|
+
height: "2px",
|
|
1127
|
+
backgroundColor: ann.color
|
|
1128
|
+
}
|
|
1129
|
+
},
|
|
1130
|
+
idx
|
|
1131
|
+
);
|
|
1132
|
+
}
|
|
1133
|
+
if (ann.type === "squiggly") {
|
|
1134
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
1135
|
+
"div",
|
|
1136
|
+
{
|
|
1137
|
+
className: "absolute",
|
|
1138
|
+
style: {
|
|
1139
|
+
...lineStyle,
|
|
1140
|
+
top: `calc(${top}% + ${height}% - 4px)`,
|
|
1141
|
+
height: "4px",
|
|
1142
|
+
backgroundImage: `linear-gradient(135deg, transparent 75%, ${ann.color} 0), linear-gradient(225deg, transparent 75%, ${ann.color} 0)`,
|
|
1143
|
+
backgroundSize: "6px 6px",
|
|
1144
|
+
backgroundPosition: "0 0, 3px 3px"
|
|
1145
|
+
}
|
|
1146
|
+
},
|
|
1147
|
+
idx
|
|
1148
|
+
);
|
|
1149
|
+
}
|
|
1150
|
+
return null;
|
|
1151
|
+
});
|
|
1152
|
+
};
|
|
1153
|
+
const renderInk = () => {
|
|
1154
|
+
if (!isInk || !ann.path) return null;
|
|
1155
|
+
const d = ann.path.map((p, idx) => `${idx === 0 ? "M" : "L"} ${p.x} ${p.y}`).join(" ");
|
|
1156
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
1157
|
+
"svg",
|
|
1158
|
+
{
|
|
1159
|
+
className: "absolute inset-0",
|
|
1160
|
+
viewBox: `${ann.rect.x} ${ann.rect.y} ${ann.rect.width} ${ann.rect.height}`,
|
|
1161
|
+
preserveAspectRatio: "none",
|
|
1162
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d, fill: "none", stroke: ann.color, strokeWidth: 8e-3, strokeLinecap: "round", strokeLinejoin: "round" })
|
|
1163
|
+
}
|
|
1164
|
+
);
|
|
1165
|
+
};
|
|
772
1166
|
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
|
|
773
1167
|
"div",
|
|
774
1168
|
{
|
|
@@ -778,8 +1172,9 @@ var AnnotationItem = ({ ann, isSelected, accentColor, onDelete, onSelect }) => {
|
|
|
778
1172
|
top: `${ann.rect.y * 100}%`,
|
|
779
1173
|
width: `${ann.rect.width * 100}%`,
|
|
780
1174
|
height: `${ann.rect.height * 100}%`,
|
|
781
|
-
backgroundColor:
|
|
782
|
-
|
|
1175
|
+
backgroundColor: !isMarkup && isHighlight ? `${ann.color}88` : "transparent",
|
|
1176
|
+
mixBlendMode: !isMarkup && isHighlight ? "multiply" : void 0,
|
|
1177
|
+
borderBottom: ann.type === "strikeout" && !isMarkup ? `2px solid ${ann.color}` : "none",
|
|
783
1178
|
outline: isSelected ? `2px solid ${accentColor}` : void 0
|
|
784
1179
|
},
|
|
785
1180
|
onClick: (e) => {
|
|
@@ -787,6 +1182,8 @@ var AnnotationItem = ({ ann, isSelected, accentColor, onDelete, onSelect }) => {
|
|
|
787
1182
|
onSelect();
|
|
788
1183
|
},
|
|
789
1184
|
children: [
|
|
1185
|
+
renderMarkupRects(),
|
|
1186
|
+
renderInk(),
|
|
790
1187
|
isText && isSelected && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("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__ */ (0, import_jsx_runtime4.jsx)(
|
|
791
1188
|
"textarea",
|
|
792
1189
|
{
|
|
@@ -816,14 +1213,36 @@ var PageRenderer_default = PageRenderer;
|
|
|
816
1213
|
|
|
817
1214
|
// components/Viewer.tsx
|
|
818
1215
|
var import_jsx_runtime5 = require("react/jsx-runtime");
|
|
1216
|
+
var BASE_OVERSCAN = 6;
|
|
819
1217
|
var Viewer = ({ engine }) => {
|
|
820
|
-
const {
|
|
1218
|
+
const { pageCount, currentPage, zoom, activeTool, uiTheme, scrollToPageSignal, setDocumentState, accentColor, annotationColor, setAnnotationColor } = (0, import_core5.useViewerStore)();
|
|
821
1219
|
const isDark = uiTheme === "dark";
|
|
822
1220
|
const viewerRef = (0, import_react5.useRef)(null);
|
|
1221
|
+
const colorPickerRef = (0, import_react5.useRef)(null);
|
|
1222
|
+
const pageRefs = (0, import_react5.useRef)([]);
|
|
1223
|
+
const intersectionRatiosRef = (0, import_react5.useRef)({});
|
|
1224
|
+
const frameRef = (0, import_react5.useRef)(null);
|
|
1225
|
+
const jumpRef = (0, import_react5.useRef)(false);
|
|
1226
|
+
const jumpTimerRef = (0, import_react5.useRef)(null);
|
|
823
1227
|
const [availableWidth, setAvailableWidth] = (0, import_react5.useState)(null);
|
|
1228
|
+
const [basePageSize, setBasePageSize] = (0, import_react5.useState)(null);
|
|
1229
|
+
const [pageSizes, setPageSizes] = (0, import_react5.useState)({});
|
|
1230
|
+
const [colorPickerOpen, setColorPickerOpen] = (0, import_react5.useState)(false);
|
|
824
1231
|
const isCompact = availableWidth !== null && availableWidth < 820;
|
|
825
1232
|
const paddingY = isCompact ? "py-10" : "py-16";
|
|
826
|
-
const toolDockPosition = isCompact ? "bottom-
|
|
1233
|
+
const toolDockPosition = isCompact ? "bottom-4" : "bottom-8";
|
|
1234
|
+
const colorPalette = ["#fbbf24", "#f97316", "#ef4444", "#22c55e", "#06b6d4", "#3b82f6", "#8b5cf6", "#111827"];
|
|
1235
|
+
(0, import_react5.useEffect)(() => {
|
|
1236
|
+
if (!colorPickerOpen) return;
|
|
1237
|
+
const handleClick = (event) => {
|
|
1238
|
+
if (!colorPickerRef.current) return;
|
|
1239
|
+
if (!colorPickerRef.current.contains(event.target)) {
|
|
1240
|
+
setColorPickerOpen(false);
|
|
1241
|
+
}
|
|
1242
|
+
};
|
|
1243
|
+
document.addEventListener("mousedown", handleClick);
|
|
1244
|
+
return () => document.removeEventListener("mousedown", handleClick);
|
|
1245
|
+
}, [colorPickerOpen]);
|
|
827
1246
|
(0, import_react5.useEffect)(() => {
|
|
828
1247
|
const element = viewerRef.current;
|
|
829
1248
|
if (!element) return;
|
|
@@ -840,29 +1259,139 @@ var Viewer = ({ engine }) => {
|
|
|
840
1259
|
observer.observe(element);
|
|
841
1260
|
return () => observer.disconnect();
|
|
842
1261
|
}, []);
|
|
1262
|
+
(0, import_react5.useEffect)(() => {
|
|
1263
|
+
let active = true;
|
|
1264
|
+
if (!pageCount) return;
|
|
1265
|
+
const loadBaseSize = async () => {
|
|
1266
|
+
try {
|
|
1267
|
+
const size = await engine.getPageDimensions(0);
|
|
1268
|
+
if (!active || !size.width || !size.height) return;
|
|
1269
|
+
setBasePageSize(size);
|
|
1270
|
+
} catch {
|
|
1271
|
+
}
|
|
1272
|
+
};
|
|
1273
|
+
loadBaseSize();
|
|
1274
|
+
return () => {
|
|
1275
|
+
active = false;
|
|
1276
|
+
};
|
|
1277
|
+
}, [engine, pageCount]);
|
|
1278
|
+
(0, import_react5.useEffect)(() => {
|
|
1279
|
+
if (scrollToPageSignal == null) return;
|
|
1280
|
+
const root = viewerRef.current;
|
|
1281
|
+
const target = pageRefs.current[scrollToPageSignal];
|
|
1282
|
+
if (root) {
|
|
1283
|
+
setDocumentState({ currentPage: scrollToPageSignal + 1 });
|
|
1284
|
+
jumpRef.current = true;
|
|
1285
|
+
if (jumpTimerRef.current) clearTimeout(jumpTimerRef.current);
|
|
1286
|
+
const previousBehavior = root.style.scrollBehavior;
|
|
1287
|
+
root.style.scrollBehavior = "auto";
|
|
1288
|
+
let targetTop = null;
|
|
1289
|
+
if (target) {
|
|
1290
|
+
targetTop = target.offsetTop;
|
|
1291
|
+
} else if (basePageSize && availableWidth) {
|
|
1292
|
+
const fitScale = Math.min(1, Math.max(0, availableWidth - 48) / basePageSize.width);
|
|
1293
|
+
const estimatedPageHeight = basePageSize.height * fitScale * zoom + 64;
|
|
1294
|
+
targetTop = Math.max(0, estimatedPageHeight * scrollToPageSignal);
|
|
1295
|
+
} else if (pageCount > 1) {
|
|
1296
|
+
const maxScroll = Math.max(0, root.scrollHeight - root.clientHeight);
|
|
1297
|
+
const ratio = scrollToPageSignal / Math.max(1, pageCount - 1);
|
|
1298
|
+
targetTop = Math.max(0, maxScroll * ratio);
|
|
1299
|
+
}
|
|
1300
|
+
if (targetTop != null) {
|
|
1301
|
+
root.scrollTop = Math.max(0, targetTop - 12);
|
|
1302
|
+
}
|
|
1303
|
+
requestAnimationFrame(() => {
|
|
1304
|
+
root.style.scrollBehavior = previousBehavior;
|
|
1305
|
+
});
|
|
1306
|
+
jumpTimerRef.current = setTimeout(() => {
|
|
1307
|
+
jumpRef.current = false;
|
|
1308
|
+
}, 250);
|
|
1309
|
+
}
|
|
1310
|
+
setDocumentState({ scrollToPageSignal: null });
|
|
1311
|
+
}, [scrollToPageSignal, setDocumentState, basePageSize, availableWidth, zoom, pageCount]);
|
|
1312
|
+
(0, import_react5.useEffect)(() => {
|
|
1313
|
+
setPageSizes({});
|
|
1314
|
+
}, [zoom]);
|
|
843
1315
|
(0, import_react5.useEffect)(() => {
|
|
844
1316
|
const root = viewerRef.current;
|
|
845
1317
|
if (!root) return;
|
|
1318
|
+
const flushCurrentPage = () => {
|
|
1319
|
+
if (jumpRef.current) return;
|
|
1320
|
+
const ratios = intersectionRatiosRef.current;
|
|
1321
|
+
const entries = Object.entries(ratios).filter(([, ratio]) => ratio > 0);
|
|
1322
|
+
if (!entries.length) return;
|
|
1323
|
+
const currentIndex = currentPage - 1;
|
|
1324
|
+
const currentRatio = ratios[currentIndex] ?? 0;
|
|
1325
|
+
const [bestIndexText, bestRatio] = entries.reduce(
|
|
1326
|
+
(best, candidate) => Number(candidate[1]) > Number(best[1]) ? candidate : best
|
|
1327
|
+
);
|
|
1328
|
+
const bestIndex = Number(bestIndexText);
|
|
1329
|
+
if (!Number.isFinite(bestIndex)) return;
|
|
1330
|
+
const bestPage = bestIndex + 1;
|
|
1331
|
+
const shouldSwitch = bestPage !== currentPage && (currentRatio <= 0 || bestRatio >= currentRatio + 0.1 || bestRatio >= 0.75);
|
|
1332
|
+
if (shouldSwitch) setDocumentState({ currentPage: bestPage });
|
|
1333
|
+
};
|
|
846
1334
|
const observer = new IntersectionObserver((entries) => {
|
|
847
1335
|
entries.forEach((entry) => {
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
1336
|
+
const pageIndex = parseInt(entry.target.getAttribute("data-page-index") || "0");
|
|
1337
|
+
if (!Number.isFinite(pageIndex)) return;
|
|
1338
|
+
intersectionRatiosRef.current[pageIndex] = entry.isIntersecting ? entry.intersectionRatio : 0;
|
|
1339
|
+
});
|
|
1340
|
+
if (frameRef.current != null) cancelAnimationFrame(frameRef.current);
|
|
1341
|
+
frameRef.current = requestAnimationFrame(() => {
|
|
1342
|
+
frameRef.current = null;
|
|
1343
|
+
flushCurrentPage();
|
|
852
1344
|
});
|
|
853
|
-
}, { root, threshold: 0.5 });
|
|
1345
|
+
}, { root, threshold: [0.25, 0.5, 0.75] });
|
|
854
1346
|
const pageElements = root.querySelectorAll(".page-container");
|
|
855
1347
|
pageElements.forEach((el) => observer.observe(el));
|
|
856
1348
|
return () => {
|
|
1349
|
+
if (frameRef.current != null) {
|
|
1350
|
+
cancelAnimationFrame(frameRef.current);
|
|
1351
|
+
frameRef.current = null;
|
|
1352
|
+
}
|
|
857
1353
|
pageElements.forEach((el) => observer.unobserve(el));
|
|
858
1354
|
observer.disconnect();
|
|
859
1355
|
};
|
|
860
1356
|
}, [pageCount, setDocumentState, currentPage]);
|
|
1357
|
+
const virtualOverscan = zoom > 1.35 ? 4 : BASE_OVERSCAN;
|
|
1358
|
+
const virtualAnchor = currentPage - 1;
|
|
1359
|
+
const virtualStart = Math.max(0, virtualAnchor - virtualOverscan);
|
|
1360
|
+
const virtualEnd = Math.min(pageCount - 1, virtualAnchor + virtualOverscan);
|
|
1361
|
+
const fallbackSize = (0, import_react5.useMemo)(() => {
|
|
1362
|
+
if (basePageSize && availableWidth) {
|
|
1363
|
+
const fitScale = Math.min(1, Math.max(0, availableWidth - 48) / basePageSize.width);
|
|
1364
|
+
return {
|
|
1365
|
+
width: Math.round(basePageSize.width * fitScale * zoom),
|
|
1366
|
+
height: Math.round(basePageSize.height * fitScale * zoom)
|
|
1367
|
+
};
|
|
1368
|
+
}
|
|
1369
|
+
const base = availableWidth ? Math.max(680, availableWidth * 1.3) : 1100;
|
|
1370
|
+
return {
|
|
1371
|
+
width: Math.round((availableWidth ?? 860) - 48),
|
|
1372
|
+
height: Math.round(base * zoom)
|
|
1373
|
+
};
|
|
1374
|
+
}, [basePageSize, availableWidth, zoom]);
|
|
1375
|
+
const averagePageHeight = (0, import_react5.useMemo)(() => {
|
|
1376
|
+
const heights = Object.values(pageSizes).map((size) => size.height);
|
|
1377
|
+
if (!heights.length) return availableWidth ? Math.max(680, availableWidth * 1.3) : 1100;
|
|
1378
|
+
return Math.round(heights.reduce((sum, h) => sum + h, 0) / heights.length);
|
|
1379
|
+
}, [pageSizes, availableWidth]);
|
|
861
1380
|
const pages = Array.from({ length: pageCount }).map((_, i) => i);
|
|
1381
|
+
const handlePageMeasured = (pageIndex, size) => {
|
|
1382
|
+
setPageSizes((prev) => {
|
|
1383
|
+
const current = prev[pageIndex];
|
|
1384
|
+
if (current && current.width === size.width && current.height === size.height) return prev;
|
|
1385
|
+
return { ...prev, [pageIndex]: size };
|
|
1386
|
+
});
|
|
1387
|
+
};
|
|
862
1388
|
const tools = [
|
|
863
1389
|
{ id: "select", name: "Select", icon: "M15 15l-2 5L9 9l11 4-5 2zm0 0l5 5" },
|
|
864
|
-
{ id: "highlight", name: "
|
|
865
|
-
{ id: "
|
|
1390
|
+
{ 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" },
|
|
1391
|
+
{ id: "underline", name: "Underline", icon: "M6 3v6a6 6 0 0012 0V3M4 21h16" },
|
|
1392
|
+
{ id: "squiggly", name: "Squiggly", icon: "M3 17c2-4 4-4 6 0s4 4 6 0 4-4 6 0" },
|
|
1393
|
+
{ id: "strikeout", name: "Strike", icon: "M4 12h16M8 6h8M8 18h8" },
|
|
1394
|
+
{ id: "ink", name: "Freehand", icon: "M4 19c4-6 7-9 10-9 3 0 5 2 6 5" },
|
|
866
1395
|
{ 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" }
|
|
867
1396
|
];
|
|
868
1397
|
return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
|
|
@@ -870,19 +1399,96 @@ var Viewer = ({ engine }) => {
|
|
|
870
1399
|
{
|
|
871
1400
|
ref: viewerRef,
|
|
872
1401
|
"data-papyrus-theme": uiTheme,
|
|
873
|
-
className: `papyrus-viewer papyrus-theme flex-1 overflow-
|
|
1402
|
+
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]"}`,
|
|
874
1403
|
children: [
|
|
875
|
-
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "flex flex-col items-center gap-6 w-full", children: pages.map((idx) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
876
|
-
|
|
877
|
-
"button",
|
|
1404
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "flex flex-col items-center gap-6 w-full min-w-0", children: pages.map((idx) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1405
|
+
"div",
|
|
878
1406
|
{
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
1407
|
+
ref: (element) => {
|
|
1408
|
+
pageRefs.current[idx] = element;
|
|
1409
|
+
},
|
|
1410
|
+
"data-page-index": idx,
|
|
1411
|
+
className: "page-container",
|
|
1412
|
+
children: idx >= virtualStart && idx <= virtualEnd ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1413
|
+
PageRenderer_default,
|
|
1414
|
+
{
|
|
1415
|
+
engine,
|
|
1416
|
+
pageIndex: idx,
|
|
1417
|
+
availableWidth: availableWidth ?? void 0,
|
|
1418
|
+
onMeasuredSize: handlePageMeasured
|
|
1419
|
+
}
|
|
1420
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1421
|
+
"div",
|
|
1422
|
+
{
|
|
1423
|
+
className: `inline-block mb-10 shadow-2xl border ${isDark ? "bg-[#0f0f0f] border-[#2b2b2b]" : "bg-white border-gray-200"}`,
|
|
1424
|
+
style: {
|
|
1425
|
+
width: pageSizes[idx]?.width ?? fallbackSize.width,
|
|
1426
|
+
height: pageSizes[idx]?.height ?? Math.max(fallbackSize.height, averagePageHeight)
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
)
|
|
883
1430
|
},
|
|
884
|
-
|
|
885
|
-
)) })
|
|
1431
|
+
idx
|
|
1432
|
+
)) }),
|
|
1433
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: `papyrus-tool-dock sticky ${toolDockPosition} w-full flex justify-center pointer-events-none z-[70]`, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("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: [
|
|
1434
|
+
tools.map((tool) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1435
|
+
"button",
|
|
1436
|
+
{
|
|
1437
|
+
title: tool.name,
|
|
1438
|
+
"aria-label": tool.name,
|
|
1439
|
+
onClick: () => setDocumentState({ activeTool: tool.id }),
|
|
1440
|
+
className: `w-10 h-10 rounded-xl flex items-center justify-center transition-all ${activeTool === tool.id ? "text-white shadow-lg" : "text-gray-400"}`,
|
|
1441
|
+
style: activeTool === tool.id ? { backgroundColor: accentColor } : void 0,
|
|
1442
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: tool.icon }) })
|
|
1443
|
+
},
|
|
1444
|
+
tool.id
|
|
1445
|
+
)),
|
|
1446
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "w-px h-7 mx-2 bg-white/10" }),
|
|
1447
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { ref: colorPickerRef, className: "relative", children: [
|
|
1448
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1449
|
+
"button",
|
|
1450
|
+
{
|
|
1451
|
+
title: "Cor do marcador",
|
|
1452
|
+
"aria-label": "Cor do marcador",
|
|
1453
|
+
onClick: () => setColorPickerOpen((prev) => !prev),
|
|
1454
|
+
className: "w-9 h-9 rounded-full flex items-center justify-center border transition-all cursor-pointer relative",
|
|
1455
|
+
style: { borderColor: annotationColor },
|
|
1456
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "w-5 h-5 rounded-full", style: { backgroundColor: annotationColor } })
|
|
1457
|
+
}
|
|
1458
|
+
),
|
|
1459
|
+
colorPickerOpen && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("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: [
|
|
1460
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "grid grid-cols-4 gap-2 mb-3", children: colorPalette.map((color) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1461
|
+
"button",
|
|
1462
|
+
{
|
|
1463
|
+
onClick: () => {
|
|
1464
|
+
setAnnotationColor(color);
|
|
1465
|
+
setColorPickerOpen(false);
|
|
1466
|
+
},
|
|
1467
|
+
className: "w-7 h-7 rounded-full border transition-all",
|
|
1468
|
+
style: { backgroundColor: color, borderColor: color === annotationColor ? "#fff" : "transparent" }
|
|
1469
|
+
},
|
|
1470
|
+
color
|
|
1471
|
+
)) }),
|
|
1472
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex items-center gap-2 w-full", children: [
|
|
1473
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "text-[10px] uppercase tracking-widest text-gray-400 shrink-0", children: "Hex" }),
|
|
1474
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1475
|
+
"input",
|
|
1476
|
+
{
|
|
1477
|
+
type: "text",
|
|
1478
|
+
value: annotationColor.toUpperCase(),
|
|
1479
|
+
onChange: (e) => {
|
|
1480
|
+
const next = e.target.value.trim();
|
|
1481
|
+
if (next.startsWith("#") && (next.length === 4 || next.length === 7)) {
|
|
1482
|
+
setAnnotationColor(next);
|
|
1483
|
+
}
|
|
1484
|
+
},
|
|
1485
|
+
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"}`
|
|
1486
|
+
}
|
|
1487
|
+
)
|
|
1488
|
+
] })
|
|
1489
|
+
] })
|
|
1490
|
+
] })
|
|
1491
|
+
] }) })
|
|
886
1492
|
]
|
|
887
1493
|
}
|
|
888
1494
|
);
|