@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/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 newZoom = Math.max(0.2, Math.min(5, zoom + delta));
39
- engine.setZoom(newZoom);
40
- setDocumentState({ zoom: newZoom });
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 w-10 text-center bg-transparent focus:outline-none font-bold text-sm",
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("button", { type: "submit", className: "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" }) }) })
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
- setDocumentState({ activeSearchIndex: idx });
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 (scrollToPageSignal === pageIndex && containerRef.current) {
554
- containerRef.current.scrollIntoView({ behavior: "smooth", block: "start" });
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 renderScale = isElementRender ? 1 : RENDER_SCALE * fitScale;
567
- await engine.renderPage(pageIndex, renderTarget, renderScale);
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 = renderScale * Math.max(zoom, 0.01);
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, renderScale);
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
- if (activeTool === "select") return;
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" ? "#fbbf24" : activeTool === "strikeout" ? "#ef4444" : accentColor,
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 ${activeTool !== "select" ? "no-select cursor-crosshair" : ""}`,
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" : activeTool === "select" ? "auto" : "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: ann.type === "highlight" ? `${ann.color}77` : "transparent",
753
- borderBottom: ann.type === "strikeout" ? `2px solid ${ann.color}` : "none",
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 { viewMode, pageCount, currentPage, activeTool, uiTheme, setDocumentState, accentColor } = useViewerStore5();
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-6" : "bottom-10";
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
- if (entry.isIntersecting) {
820
- const pageIndex = parseInt(entry.target.getAttribute("data-page-index") || "0");
821
- if (pageIndex + 1 !== currentPage) setDocumentState({ currentPage: pageIndex + 1 });
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: "Marker", 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" },
836
- { id: "strikeout", name: "Strike", icon: "M13 10V3L4 14h7v7l9-11h-7z" },
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-auto flex flex-col items-center ${paddingY} relative custom-scrollbar scroll-smooth ${isDark ? "bg-[#121212]" : "bg-[#e9ecef]"}`,
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("div", { "data-page-index": idx, className: "page-container", children: /* @__PURE__ */ jsx5(PageRenderer_default, { engine, pageIndex: idx, availableWidth: availableWidth ?? void 0 }) }, idx)) }),
847
- /* @__PURE__ */ jsx5("div", { className: `fixed ${toolDockPosition} left-1/2 -translate-x-1/2 shadow-2xl rounded-2xl p-2 flex border z-50 ${isDark ? "bg-[#2a2a2a]/90 border-[#3a3a3a] backdrop-blur-xl" : "bg-white/95 border-gray-100 backdrop-blur-md"}`, children: tools.map((tool) => /* @__PURE__ */ jsx5(
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
- onClick: () => setDocumentState({ activeTool: tool.id }),
851
- className: `w-10 h-10 rounded-xl flex items-center justify-center transition-all ${activeTool === tool.id ? "text-white shadow-lg" : "text-gray-400"}`,
852
- style: activeTool === tool.id ? { backgroundColor: accentColor } : void 0,
853
- 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 }) })
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
- tool.id
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
  );