@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.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 newZoom = Math.max(0.2, Math.min(5, zoom + delta));
68
- engine.setZoom(newZoom);
69
- setDocumentState({ zoom: newZoom });
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 w-10 text-center bg-transparent focus:outline-none font-bold text-sm",
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)("button", { type: "submit", className: "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" }) }) })
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
- setDocumentState({ activeSearchIndex: idx });
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 (scrollToPageSignal === pageIndex && containerRef.current) {
583
- containerRef.current.scrollIntoView({ behavior: "smooth", block: "start" });
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 renderScale = isElementRender ? 1 : RENDER_SCALE * fitScale;
596
- await engine.renderPage(pageIndex, renderTarget, renderScale);
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 = renderScale * Math.max(zoom, 0.01);
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, renderScale);
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
- if (activeTool === "select") return;
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" ? "#fbbf24" : activeTool === "strikeout" ? "#ef4444" : accentColor,
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 ${activeTool !== "select" ? "no-select cursor-crosshair" : ""}`,
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" : activeTool === "select" ? "auto" : "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: ann.type === "highlight" ? `${ann.color}77` : "transparent",
782
- borderBottom: ann.type === "strikeout" ? `2px solid ${ann.color}` : "none",
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 { viewMode, pageCount, currentPage, activeTool, uiTheme, setDocumentState, accentColor } = (0, import_core5.useViewerStore)();
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-6" : "bottom-10";
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
- if (entry.isIntersecting) {
849
- const pageIndex = parseInt(entry.target.getAttribute("data-page-index") || "0");
850
- if (pageIndex + 1 !== currentPage) setDocumentState({ currentPage: pageIndex + 1 });
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: "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" },
865
- { id: "strikeout", name: "Strike", icon: "M13 10V3L4 14h7v7l9-11h-7z" },
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-auto flex flex-col items-center ${paddingY} relative custom-scrollbar scroll-smooth ${isDark ? "bg-[#121212]" : "bg-[#e9ecef]"}`,
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)("div", { "data-page-index": idx, className: "page-container", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(PageRenderer_default, { engine, pageIndex: idx, availableWidth: availableWidth ?? void 0 }) }, idx)) }),
876
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("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__ */ (0, import_jsx_runtime5.jsx)(
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
- onClick: () => setDocumentState({ activeTool: tool.id }),
880
- className: `w-10 h-10 rounded-xl flex items-center justify-center transition-all ${activeTool === tool.id ? "text-white shadow-lg" : "text-gray-400"}`,
881
- style: activeTool === tool.id ? { backgroundColor: accentColor } : void 0,
882
- 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 }) })
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
- tool.id
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
  );