@papyrus-sdk/ui-react 0.2.22 → 0.2.23

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
@@ -43,6 +43,8 @@ var Topbar = ({
43
43
  const [isMobileViewport, setIsMobileViewport] = useState(false);
44
44
  const pageDigits = Math.max(2, String(pageCount || 1).length);
45
45
  const isDark = uiTheme === "dark";
46
+ const renderTargetType = engine.getRenderTargetType?.() ?? "canvas";
47
+ const isSingleViewportMode = renderTargetType === "element" || renderTargetType === "webview";
46
48
  const canUseDOM = typeof document !== "undefined";
47
49
  const hasMobileMenu = showZoomControls || showPageThemeSelector || showUIToggle || showUpload;
48
50
  useEffect(() => {
@@ -124,6 +126,10 @@ var Topbar = ({
124
126
  if (pageCount <= 0) return;
125
127
  const nextPage = Math.max(1, Math.min(pageCount, isNaN(page) ? 1 : page));
126
128
  engine.goToPage(nextPage);
129
+ if (isSingleViewportMode) {
130
+ setDocumentState({ currentPage: nextPage, scrollToPageSignal: null });
131
+ return;
132
+ }
127
133
  triggerScrollToPage(nextPage - 1);
128
134
  };
129
135
  const handleFileUpload = async (event) => {
@@ -759,12 +765,20 @@ var withAlpha = (hex, alpha) => {
759
765
  const b = parseInt(value.slice(4, 6), 16);
760
766
  return `rgba(${r}, ${g}, ${b}, ${alpha})`;
761
767
  };
768
+ var isEpubDebugEnabled = () => {
769
+ try {
770
+ return Boolean(globalThis?.__PAPYRUS_EPUB_DEBUG__);
771
+ } catch {
772
+ return false;
773
+ }
774
+ };
762
775
  var Thumbnail = ({ engine, pageIndex, active, isDark, accentColor, onClick }) => {
763
776
  const wrapperRef = useRef2(null);
764
777
  const canvasRef = useRef2(null);
765
778
  const htmlRef = useRef2(null);
766
779
  const accentSoft = withAlpha(accentColor, 0.12);
767
780
  const renderTargetType = engine.getRenderTargetType?.() ?? "canvas";
781
+ const isElementRender = renderTargetType === "element";
768
782
  const [isVisible, setIsVisible] = useState2(false);
769
783
  useEffect2(() => {
770
784
  const target = wrapperRef.current;
@@ -789,14 +803,14 @@ var Thumbnail = ({ engine, pageIndex, active, isDark, accentColor, onClick }) =>
789
803
  return () => observer.disconnect();
790
804
  }, []);
791
805
  useEffect2(() => {
792
- if (renderTargetType === "element" || !isVisible) return;
793
- const target = canvasRef.current;
806
+ if (!isVisible || isElementRender) return;
807
+ const target = renderTargetType === "element" ? htmlRef.current : canvasRef.current;
794
808
  if (target) {
795
809
  engine.renderPage(pageIndex, target, 0.15).catch((err) => {
796
810
  console.error("[Papyrus] Thumbnail render failed:", err);
797
811
  });
798
812
  }
799
- }, [engine, pageIndex, renderTargetType, isVisible]);
813
+ }, [engine, pageIndex, renderTargetType, isVisible, isElementRender]);
800
814
  return /* @__PURE__ */ jsx2(
801
815
  "div",
802
816
  {
@@ -810,13 +824,20 @@ var Thumbnail = ({ engine, pageIndex, active, isDark, accentColor, onClick }) =>
810
824
  {
811
825
  className: `shadow-lg rounded overflow-hidden mb-2 border ${isDark ? "border-[#333]" : "border-gray-200"}`,
812
826
  children: [
827
+ /* @__PURE__ */ jsx2(
828
+ "div",
829
+ {
830
+ className: `w-[90px] h-[120px] items-center justify-center text-[10px] font-black tracking-wider ${isElementRender ? "flex" : "hidden"} ${isDark ? "bg-[#1f1f1f] text-gray-300" : "bg-gray-100 text-gray-500"}`,
831
+ children: "CAP"
832
+ }
833
+ ),
813
834
  /* @__PURE__ */ jsx2(
814
835
  "canvas",
815
836
  {
816
837
  ref: canvasRef,
817
838
  className: "max-w-full h-auto bg-white",
818
839
  style: {
819
- display: renderTargetType === "element" ? "none" : "block"
840
+ display: isElementRender ? "none" : "block"
820
841
  }
821
842
  }
822
843
  ),
@@ -828,10 +849,9 @@ var Thumbnail = ({ engine, pageIndex, active, isDark, accentColor, onClick }) =>
828
849
  style: {
829
850
  width: 90,
830
851
  height: 120,
831
- display: renderTargetType === "element" ? "block" : "none",
852
+ display: "none",
832
853
  overflow: "hidden"
833
- },
834
- children: renderTargetType === "element" && /* @__PURE__ */ jsx2("div", { className: "w-full h-full flex items-center justify-center text-[10px] font-semibold text-gray-500", children: "HTML" })
854
+ }
835
855
  }
836
856
  )
837
857
  ]
@@ -850,9 +870,11 @@ var Thumbnail = ({ engine, pageIndex, active, isDark, accentColor, onClick }) =>
850
870
  );
851
871
  };
852
872
  var OutlineNode = ({ item, engine, isDark, accentColor, depth = 0 }) => {
853
- const { triggerScrollToPage, outlineSearchQuery } = useViewerStore2();
873
+ const { triggerScrollToPage, outlineSearchQuery, setDocumentState } = useViewerStore2();
854
874
  const [expanded, setExpanded] = useState2(true);
855
875
  const accentSoft = withAlpha(accentColor, 0.2);
876
+ const renderTargetType = engine.getRenderTargetType?.() ?? "canvas";
877
+ const isSingleViewportMode = renderTargetType === "element" || renderTargetType === "webview";
856
878
  const matchesSearch = outlineSearchQuery === "" || item.title.toLowerCase().includes(outlineSearchQuery.toLowerCase());
857
879
  const hasMatchingChildren = item.children?.some(
858
880
  (child) => child.title.toLowerCase().includes(outlineSearchQuery.toLowerCase())
@@ -860,10 +882,53 @@ var OutlineNode = ({ item, engine, isDark, accentColor, depth = 0 }) => {
860
882
  if (!matchesSearch && !hasMatchingChildren && outlineSearchQuery !== "")
861
883
  return null;
862
884
  const handleClick = () => {
863
- if (item.pageIndex >= 0) {
864
- engine.goToPage(item.pageIndex + 1);
865
- triggerScrollToPage(item.pageIndex);
866
- }
885
+ void (async () => {
886
+ if (item.pageIndex < 0 && !item.dest) return;
887
+ let targetPageIndex = item.pageIndex;
888
+ let navigatedByDestination = false;
889
+ const destinationEngine = engine;
890
+ if (isSingleViewportMode && item.dest && typeof destinationEngine.goToDestination === "function") {
891
+ try {
892
+ if (isEpubDebugEnabled()) {
893
+ console.log("[EPUBUI] toc-click", {
894
+ title: item.title,
895
+ dest: item.dest,
896
+ pageIndex: item.pageIndex
897
+ });
898
+ }
899
+ const resolved = await destinationEngine.goToDestination(item.dest);
900
+ if (isEpubDebugEnabled()) {
901
+ console.log("[EPUBUI] toc-resolved", {
902
+ title: item.title,
903
+ resolved
904
+ });
905
+ }
906
+ if (resolved != null) targetPageIndex = resolved;
907
+ navigatedByDestination = true;
908
+ } catch {
909
+ }
910
+ }
911
+ if (item.dest && (!navigatedByDestination || targetPageIndex < 0)) {
912
+ try {
913
+ const resolved = await engine.getPageIndex(item.dest);
914
+ if (resolved != null) targetPageIndex = resolved;
915
+ } catch {
916
+ }
917
+ }
918
+ if (navigatedByDestination && isSingleViewportMode) {
919
+ const page2 = targetPageIndex >= 0 ? targetPageIndex + 1 : engine.getCurrentPage();
920
+ setDocumentState({ currentPage: page2, scrollToPageSignal: null });
921
+ return;
922
+ }
923
+ if (targetPageIndex < 0) return;
924
+ const page = targetPageIndex + 1;
925
+ engine.goToPage(page);
926
+ if (isSingleViewportMode) {
927
+ setDocumentState({ currentPage: page, scrollToPageSignal: null });
928
+ } else {
929
+ triggerScrollToPage(targetPageIndex);
930
+ }
931
+ })();
867
932
  };
868
933
  return /* @__PURE__ */ jsxs2("div", { className: "flex flex-col", children: [
869
934
  /* @__PURE__ */ jsxs2(
@@ -945,6 +1010,24 @@ var SidebarLeft = ({ engine, style }) => {
945
1010
  accentColor
946
1011
  } = useViewerStore2();
947
1012
  const isDark = uiTheme === "dark";
1013
+ const renderTargetType = engine.getRenderTargetType?.() ?? "canvas";
1014
+ const prefersSummaryByDefault = renderTargetType === "element" || renderTargetType === "webview";
1015
+ const autoSummaryKeyRef = useRef2(null);
1016
+ useEffect2(() => {
1017
+ if (!prefersSummaryByDefault) return;
1018
+ if (sidebarLeftTab !== "thumbnails") return;
1019
+ if (pageCount <= 0) return;
1020
+ const docKey = `${pageCount}:${outline.length}`;
1021
+ if (autoSummaryKeyRef.current === docKey) return;
1022
+ autoSummaryKeyRef.current = docKey;
1023
+ setSidebarLeftTab("summary");
1024
+ }, [
1025
+ prefersSummaryByDefault,
1026
+ sidebarLeftTab,
1027
+ pageCount,
1028
+ outline.length,
1029
+ setSidebarLeftTab
1030
+ ]);
948
1031
  if (!sidebarLeftOpen) return null;
949
1032
  return /* @__PURE__ */ jsxs2(
950
1033
  "div",
@@ -1058,8 +1141,16 @@ var SidebarLeft = ({ engine, style }) => {
1058
1141
  accentColor,
1059
1142
  active: currentPage === idx + 1,
1060
1143
  onClick: () => {
1061
- engine.goToPage(idx + 1);
1062
- triggerScrollToPage(idx);
1144
+ const page = idx + 1;
1145
+ engine.goToPage(page);
1146
+ if (prefersSummaryByDefault) {
1147
+ setDocumentState({
1148
+ currentPage: page,
1149
+ scrollToPageSignal: null
1150
+ });
1151
+ } else {
1152
+ triggerScrollToPage(idx);
1153
+ }
1063
1154
  }
1064
1155
  },
1065
1156
  idx
@@ -1111,10 +1202,14 @@ var SidebarRight = ({ engine, style }) => {
1111
1202
  } = useViewerStore3();
1112
1203
  const [query, setQuery] = useState3("");
1113
1204
  const [isSearching, setIsSearching] = useState3(false);
1114
- const [contentDrafts, setContentDrafts] = useState3({});
1205
+ const [contentDrafts, setContentDrafts] = useState3(
1206
+ {}
1207
+ );
1115
1208
  const [replyDrafts, setReplyDrafts] = useState3({});
1116
1209
  const searchService = new SearchService(engine);
1117
1210
  const isDark = uiTheme === "dark";
1211
+ const renderTargetType = engine.getRenderTargetType?.() ?? "canvas";
1212
+ const isSingleViewportMode = renderTargetType === "element" || renderTargetType === "webview";
1118
1213
  const accentSoft = withAlpha2(accentColor, 0.12);
1119
1214
  const resultsCount = searchResults.length;
1120
1215
  const handleSearch = async (e) => {
@@ -1131,9 +1226,13 @@ var SidebarRight = ({ engine, style }) => {
1131
1226
  const jumpToAnnotation = (annotation) => {
1132
1227
  const page = annotation.pageIndex + 1;
1133
1228
  engine.goToPage(page);
1134
- setDocumentState({ currentPage: page });
1229
+ if (isSingleViewportMode) {
1230
+ setDocumentState({ currentPage: page, scrollToPageSignal: null });
1231
+ } else {
1232
+ setDocumentState({ currentPage: page });
1233
+ triggerScrollToPage(annotation.pageIndex);
1234
+ }
1135
1235
  setSelectedAnnotation(annotation.id);
1136
- triggerScrollToPage(annotation.pageIndex);
1137
1236
  };
1138
1237
  const getContentDraft = (annotation) => {
1139
1238
  if (Object.prototype.hasOwnProperty.call(contentDrafts, annotation.id)) {
@@ -1280,11 +1379,19 @@ var SidebarRight = ({ engine, style }) => {
1280
1379
  onClick: () => {
1281
1380
  const page = res.pageIndex + 1;
1282
1381
  engine.goToPage(page);
1283
- setDocumentState({
1284
- activeSearchIndex: idx,
1285
- currentPage: page
1286
- });
1287
- triggerScrollToPage(res.pageIndex);
1382
+ if (isSingleViewportMode) {
1383
+ setDocumentState({
1384
+ activeSearchIndex: idx,
1385
+ currentPage: page,
1386
+ scrollToPageSignal: null
1387
+ });
1388
+ } else {
1389
+ setDocumentState({
1390
+ activeSearchIndex: idx,
1391
+ currentPage: page
1392
+ });
1393
+ triggerScrollToPage(res.pageIndex);
1394
+ }
1288
1395
  },
1289
1396
  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"}`,
1290
1397
  style: idx === activeSearchIndex ? {
@@ -1371,7 +1478,9 @@ var SidebarRight = ({ engine, style }) => {
1371
1478
  const replies = ann.replies ?? [];
1372
1479
  const contentDraft = getContentDraft(ann);
1373
1480
  const replyDraft = getReplyDraft(ann.id);
1374
- const hasExistingContent = Boolean((ann.content ?? "").trim());
1481
+ const hasExistingContent = Boolean(
1482
+ (ann.content ?? "").trim()
1483
+ );
1375
1484
  return /* @__PURE__ */ jsxs3(
1376
1485
  "div",
1377
1486
  {
@@ -1533,6 +1642,7 @@ var PageRenderer = ({
1533
1642
  engine,
1534
1643
  pageIndex,
1535
1644
  availableWidth,
1645
+ availableHeight,
1536
1646
  onMeasuredSize
1537
1647
  }) => {
1538
1648
  const containerRef = useRef3(null);
@@ -1574,6 +1684,11 @@ var PageRenderer = ({
1574
1684
  } = useViewerStore4();
1575
1685
  const renderTargetType = engine.getRenderTargetType?.() ?? "canvas";
1576
1686
  const isElementRender = renderTargetType === "element";
1687
+ const isLandscape = typeof availableWidth === "number" && typeof availableHeight === "number" && availableWidth > availableHeight;
1688
+ const isLandscapeShort = isLandscape && typeof availableHeight === "number" && availableHeight <= 500;
1689
+ const isMobileElementViewport = isElementRender && typeof availableWidth === "number" && (availableWidth <= 768 || isLandscapeShort);
1690
+ const renderZoomDependency = isElementRender ? 1 : zoom;
1691
+ const renderRotationDependency = isElementRender ? 0 : rotation;
1577
1692
  const textMarkupTools = /* @__PURE__ */ new Set([
1578
1693
  "highlight",
1579
1694
  "underline",
@@ -1603,6 +1718,10 @@ var PageRenderer = ({
1603
1718
  },
1604
1719
  []
1605
1720
  );
1721
+ useEffect3(() => {
1722
+ if (!isElementRender) return;
1723
+ setPageSize(null);
1724
+ }, [isElementRender, pageIndex]);
1606
1725
  useEffect3(() => {
1607
1726
  let active = true;
1608
1727
  const loadSize = async () => {
@@ -1621,12 +1740,13 @@ var PageRenderer = ({
1621
1740
  };
1622
1741
  }, [engine, pageIndex]);
1623
1742
  const fitScale = useMemo2(() => {
1743
+ if (isElementRender && isMobileElementViewport) return 1;
1624
1744
  if (!availableWidth || !pageSize?.width) return 1;
1625
1745
  const targetWidth = Math.max(0, availableWidth - 48);
1626
1746
  if (!targetWidth) return 1;
1627
1747
  const rawScale = Math.min(1, targetWidth / pageSize.width);
1628
1748
  return Math.round(rawScale * SCALE_PRECISION) / SCALE_PRECISION;
1629
- }, [availableWidth, pageSize]);
1749
+ }, [isElementRender, isMobileElementViewport, availableWidth, pageSize]);
1630
1750
  const displaySize = useMemo2(() => {
1631
1751
  if (!pageSize) return null;
1632
1752
  const scale = zoom * fitScale;
@@ -1662,6 +1782,15 @@ var PageRenderer = ({
1662
1782
  canvasRef.current.style.height = `${displaySize.height}px`;
1663
1783
  }
1664
1784
  await engine.renderPage(pageIndex, renderTarget, canvasRenderScale);
1785
+ const measuredSize = await engine.getPageDimensions(pageIndex);
1786
+ if (measuredSize.width > 0 && measuredSize.height > 0 && active) {
1787
+ setPageSize((prev) => {
1788
+ if (prev && prev.width === measuredSize.width && prev.height === measuredSize.height) {
1789
+ return prev;
1790
+ }
1791
+ return measuredSize;
1792
+ });
1793
+ }
1665
1794
  if (!isElementRender && !pageSize && canvasRef.current) {
1666
1795
  const denom = canvasRenderScale * Math.max(zoom, 0.01);
1667
1796
  if (denom > 0) {
@@ -1704,13 +1833,24 @@ var PageRenderer = ({
1704
1833
  }, [
1705
1834
  engine,
1706
1835
  pageIndex,
1707
- zoom,
1708
- rotation,
1709
1836
  isElementRender,
1837
+ availableWidth,
1710
1838
  fitScale,
1711
1839
  displaySize,
1712
- pageSize
1840
+ pageSize,
1841
+ renderZoomDependency,
1842
+ renderRotationDependency
1713
1843
  ]);
1844
+ useEffect3(() => {
1845
+ if (!isElementRender || pageSize) return;
1846
+ const target = htmlLayerRef.current;
1847
+ if (!target) return;
1848
+ const measuredWidth = target.clientWidth || target.scrollWidth || 0;
1849
+ const measuredHeight = target.clientHeight || target.scrollHeight || 0;
1850
+ if (measuredWidth > 0 && measuredHeight > 0) {
1851
+ setPageSize({ width: measuredWidth, height: measuredHeight });
1852
+ }
1853
+ }, [isElementRender, pageSize, textLayerVersion]);
1714
1854
  useEffect3(() => {
1715
1855
  if (isElementRender) return;
1716
1856
  const layer = textLayerRef.current;
@@ -2035,14 +2175,26 @@ var PageRenderer = ({
2035
2175
  return "none";
2036
2176
  }
2037
2177
  };
2178
+ const elementScale = zoom * fitScale;
2179
+ const elementBaseWidth = isElementRender ? isMobileElementViewport && availableWidth != null ? Math.max(260, Math.round(availableWidth)) : pageSize?.width ?? 640 : pageSize?.width ?? 640;
2180
+ const elementBaseHeight = pageSize?.height ?? (isElementRender ? 700 : 900);
2181
+ const elementContainerStyle = isElementRender ? {
2182
+ width: `${Math.max(1, Math.round(elementBaseWidth * elementScale))}px`,
2183
+ height: `${Math.max(
2184
+ 1,
2185
+ Math.round(elementBaseHeight * elementScale)
2186
+ )}px`
2187
+ } : void 0;
2038
2188
  return /* @__PURE__ */ jsxs4(
2039
2189
  "div",
2040
2190
  {
2041
2191
  ref: containerRef,
2042
- className: `relative inline-block shadow-2xl bg-white mb-10 ${canSelectText ? "" : "no-select cursor-crosshair"}`,
2192
+ className: `relative inline-block shadow-2xl bg-white ${isMobileElementViewport ? "mb-0" : "mb-10"} ${canSelectText ? "" : "no-select cursor-crosshair"}`,
2043
2193
  style: {
2044
2194
  scrollMarginTop: "20px",
2045
2195
  minHeight: "100px",
2196
+ overflow: "hidden",
2197
+ ...elementContainerStyle,
2046
2198
  touchAction: activeTool === "ink" || activeTool === "text" || activeTool === "comment" ? "none" : "auto"
2047
2199
  },
2048
2200
  onMouseDown: handleMouseDown,
@@ -2072,7 +2224,11 @@ var PageRenderer = ({
2072
2224
  className: "block",
2073
2225
  style: {
2074
2226
  filter: getPageFilter(),
2075
- display: isElementRender ? "block" : "none"
2227
+ display: isElementRender ? "block" : "none",
2228
+ width: `${elementBaseWidth}px`,
2229
+ height: `${elementBaseHeight}px`,
2230
+ transform: `scale(${elementScale})`,
2231
+ transformOrigin: "top left"
2076
2232
  }
2077
2233
  }
2078
2234
  ),
@@ -2522,6 +2678,15 @@ var PageRenderer_default = PageRenderer;
2522
2678
 
2523
2679
  // components/Viewer.tsx
2524
2680
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
2681
+ var withAlpha3 = (hex, alpha) => {
2682
+ const normalized = hex.replace("#", "").trim();
2683
+ const value = normalized.length === 3 ? normalized.split("").map((c) => c + c).join("") : normalized;
2684
+ if (value.length !== 6) return hex;
2685
+ const r = parseInt(value.slice(0, 2), 16);
2686
+ const g = parseInt(value.slice(2, 4), 16);
2687
+ const b = parseInt(value.slice(4, 6), 16);
2688
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
2689
+ };
2525
2690
  var BASE_OVERSCAN = 6;
2526
2691
  var MIN_ZOOM = 0.2;
2527
2692
  var MAX_ZOOM = 5;
@@ -2543,6 +2708,7 @@ var Viewer = ({ engine, style }) => {
2543
2708
  uiTheme,
2544
2709
  scrollToPageSignal,
2545
2710
  setDocumentState,
2711
+ triggerScrollToPage,
2546
2712
  accentColor,
2547
2713
  annotationColor,
2548
2714
  setAnnotationColor,
@@ -2550,7 +2716,10 @@ var Viewer = ({ engine, style }) => {
2550
2716
  } = viewerState;
2551
2717
  const mobileTopbarVisible = viewerState.mobileTopbarVisible ?? true;
2552
2718
  const isDark = uiTheme === "dark";
2719
+ const renderTargetType = engine.getRenderTargetType?.() ?? "canvas";
2720
+ const isSingleViewportMode = renderTargetType === "element" || renderTargetType === "webview";
2553
2721
  const viewerRef = useRef4(null);
2722
+ const singleNavInFlightRef = useRef4(false);
2554
2723
  const colorPickerRef = useRef4(null);
2555
2724
  const pageRefs = useRef4([]);
2556
2725
  const intersectionRatiosRef = useRef4({});
@@ -2573,6 +2742,7 @@ var Viewer = ({ engine, style }) => {
2573
2742
  });
2574
2743
  const [availableWidth, setAvailableWidth] = useState5(null);
2575
2744
  const [availableHeight, setAvailableHeight] = useState5(null);
2745
+ const [viewerBounds, setViewerBounds] = useState5(null);
2576
2746
  const [basePageSize, setBasePageSize] = useState5(null);
2577
2747
  const [pageSizes, setPageSizes] = useState5({});
2578
2748
  const [colorPickerOpen, setColorPickerOpen] = useState5(false);
@@ -2580,7 +2750,7 @@ var Viewer = ({ engine, style }) => {
2580
2750
  const isLandscapeShort = isLandscape && availableHeight !== null && availableHeight <= MOBILE_LANDSCAPE_MAX_HEIGHT_PX2;
2581
2751
  const isCompact = availableWidth !== null && (availableWidth < 820 || isLandscapeShort);
2582
2752
  const isMobileViewport = availableWidth !== null && (availableWidth < 640 || isLandscapeShort);
2583
- const paddingY = isCompact ? "py-10" : "py-16";
2753
+ const paddingY = isSingleViewportMode && isMobileViewport ? "py-0" : isCompact ? "py-10" : "py-16";
2584
2754
  const toolDockPosition = isCompact ? "bottom-4" : "bottom-8";
2585
2755
  const colorPalette = [
2586
2756
  "#fbbf24",
@@ -2592,6 +2762,45 @@ var Viewer = ({ engine, style }) => {
2592
2762
  "#8b5cf6",
2593
2763
  "#111827"
2594
2764
  ];
2765
+ const destinationNavEngine = engine;
2766
+ const canUseDestinationNavigation = isSingleViewportMode && typeof destinationNavEngine.goToAdjacentDestination === "function";
2767
+ const destinationNavigationState = isSingleViewportMode && typeof destinationNavEngine.getDestinationNavigationState === "function" ? destinationNavEngine.getDestinationNavigationState() : null;
2768
+ const canGoPrev = destinationNavigationState?.hasPrev ?? currentPage > 1;
2769
+ const canGoNext = destinationNavigationState?.hasNext ?? currentPage < pageCount;
2770
+ const viewerOverflowClass = isSingleViewportMode ? "overflow-hidden" : "overflow-y-scroll overflow-x-hidden";
2771
+ const navigateBy = (delta) => {
2772
+ if (pageCount <= 0) return;
2773
+ if (canUseDestinationNavigation) {
2774
+ if (singleNavInFlightRef.current) return;
2775
+ singleNavInFlightRef.current = true;
2776
+ void (async () => {
2777
+ try {
2778
+ const resolved = await destinationNavEngine.goToAdjacentDestination(
2779
+ delta
2780
+ );
2781
+ if (resolved == null) return;
2782
+ setDocumentState({
2783
+ currentPage: resolved + 1,
2784
+ scrollToPageSignal: null
2785
+ });
2786
+ } finally {
2787
+ singleNavInFlightRef.current = false;
2788
+ }
2789
+ })();
2790
+ return;
2791
+ }
2792
+ const enginePage = Number(engine.getCurrentPage?.());
2793
+ const normalizedEnginePage = Number.isFinite(enginePage) && enginePage >= 1 ? Math.floor(enginePage) : null;
2794
+ const basePage = normalizedEnginePage != null && Math.abs(normalizedEnginePage - currentPage) <= 1 ? normalizedEnginePage : currentPage;
2795
+ const clampedPage = Math.max(1, Math.min(pageCount, basePage + delta));
2796
+ if (clampedPage === basePage) return;
2797
+ engine.goToPage(clampedPage);
2798
+ if (isSingleViewportMode) {
2799
+ setDocumentState({ currentPage: clampedPage, scrollToPageSignal: null });
2800
+ return;
2801
+ }
2802
+ triggerScrollToPage(clampedPage - 1);
2803
+ };
2595
2804
  const setMobileTopbarVisibility = (visible) => {
2596
2805
  if (mobileTopbarVisibleRef.current === visible) return;
2597
2806
  mobileTopbarVisibleRef.current = visible;
@@ -2678,10 +2887,48 @@ var Viewer = ({ engine, style }) => {
2678
2887
  observer.disconnect();
2679
2888
  };
2680
2889
  }, []);
2890
+ useEffect4(() => {
2891
+ if (!isSingleViewportMode) return;
2892
+ const viewerElement = viewerRef.current;
2893
+ if (!viewerElement) return;
2894
+ let rafId = null;
2895
+ const updateBounds = () => {
2896
+ const rect = viewerElement.getBoundingClientRect();
2897
+ setViewerBounds({
2898
+ left: rect.left,
2899
+ width: rect.width,
2900
+ top: rect.top,
2901
+ height: rect.height
2902
+ });
2903
+ };
2904
+ const scheduleUpdate = () => {
2905
+ if (rafId != null) cancelAnimationFrame(rafId);
2906
+ rafId = requestAnimationFrame(() => {
2907
+ rafId = null;
2908
+ updateBounds();
2909
+ });
2910
+ };
2911
+ updateBounds();
2912
+ viewerElement.addEventListener("scroll", scheduleUpdate, { passive: true });
2913
+ window.addEventListener("resize", scheduleUpdate);
2914
+ window.addEventListener("scroll", scheduleUpdate, { passive: true });
2915
+ let observer = null;
2916
+ if (typeof ResizeObserver !== "undefined") {
2917
+ observer = new ResizeObserver(() => scheduleUpdate());
2918
+ observer.observe(viewerElement);
2919
+ }
2920
+ return () => {
2921
+ if (rafId != null) cancelAnimationFrame(rafId);
2922
+ viewerElement.removeEventListener("scroll", scheduleUpdate);
2923
+ window.removeEventListener("resize", scheduleUpdate);
2924
+ window.removeEventListener("scroll", scheduleUpdate);
2925
+ observer?.disconnect();
2926
+ };
2927
+ }, [isSingleViewportMode]);
2681
2928
  useEffect4(() => {
2682
2929
  const root = viewerRef.current;
2683
2930
  if (!root) return;
2684
- if (!isMobileViewport) {
2931
+ if (isSingleViewportMode || !isMobileViewport) {
2685
2932
  lastScrollTopRef.current = root.scrollTop;
2686
2933
  scrollDownAccumulatorRef.current = 0;
2687
2934
  scrollUpAccumulatorRef.current = 0;
@@ -2725,7 +2972,7 @@ var Viewer = ({ engine, style }) => {
2725
2972
  return () => {
2726
2973
  root.removeEventListener("scroll", handleScroll);
2727
2974
  };
2728
- }, [isMobileViewport, setDocumentState]);
2975
+ }, [isSingleViewportMode, isMobileViewport, setDocumentState]);
2729
2976
  useEffect4(() => {
2730
2977
  const previousPage = previousCurrentPageRef.current;
2731
2978
  previousCurrentPageRef.current = currentPage;
@@ -2754,6 +3001,19 @@ var Viewer = ({ engine, style }) => {
2754
3001
  }, [engine, pageCount]);
2755
3002
  useEffect4(() => {
2756
3003
  if (scrollToPageSignal == null) return;
3004
+ if (isSingleViewportMode) {
3005
+ const nextPageIndex = Math.max(
3006
+ 0,
3007
+ Math.min(Math.max(pageCount - 1, 0), scrollToPageSignal)
3008
+ );
3009
+ const root2 = viewerRef.current;
3010
+ if (root2) root2.scrollTop = 0;
3011
+ setDocumentState({
3012
+ currentPage: nextPageIndex + 1,
3013
+ scrollToPageSignal: null
3014
+ });
3015
+ return;
3016
+ }
2757
3017
  const root = viewerRef.current;
2758
3018
  const target = pageRefs.current[scrollToPageSignal];
2759
3019
  if (root) {
@@ -2790,16 +3050,57 @@ var Viewer = ({ engine, style }) => {
2790
3050
  setDocumentState({ scrollToPageSignal: null });
2791
3051
  }, [
2792
3052
  scrollToPageSignal,
3053
+ isSingleViewportMode,
2793
3054
  setDocumentState,
2794
3055
  basePageSize,
2795
3056
  availableWidth,
2796
3057
  zoom,
2797
3058
  pageCount
2798
3059
  ]);
3060
+ useEffect4(() => {
3061
+ if (!isSingleViewportMode) return;
3062
+ const root = viewerRef.current;
3063
+ if (!root) return;
3064
+ root.scrollTop = 0;
3065
+ }, [isSingleViewportMode, currentPage]);
2799
3066
  useEffect4(() => {
2800
3067
  setPageSizes({});
2801
3068
  }, [zoom]);
2802
3069
  useEffect4(() => {
3070
+ if (pageCount <= 1) return;
3071
+ const handleKeyNavigation = (event) => {
3072
+ if (event.defaultPrevented) return;
3073
+ if (event.altKey || event.ctrlKey || event.metaKey) return;
3074
+ const target = event.target;
3075
+ if (target) {
3076
+ const tag = target.tagName;
3077
+ const isEditable = tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || target.isContentEditable || target.getAttribute("contenteditable") === "true";
3078
+ if (isEditable) return;
3079
+ }
3080
+ if (event.key === "ArrowLeft") {
3081
+ event.preventDefault();
3082
+ if (!canGoPrev) return;
3083
+ navigateBy(-1);
3084
+ return;
3085
+ }
3086
+ if (event.key === "ArrowRight") {
3087
+ event.preventDefault();
3088
+ if (!canGoNext) return;
3089
+ navigateBy(1);
3090
+ }
3091
+ };
3092
+ window.addEventListener("keydown", handleKeyNavigation);
3093
+ return () => window.removeEventListener("keydown", handleKeyNavigation);
3094
+ }, [
3095
+ currentPage,
3096
+ pageCount,
3097
+ triggerScrollToPage,
3098
+ engine,
3099
+ canGoPrev,
3100
+ canGoNext
3101
+ ]);
3102
+ useEffect4(() => {
3103
+ if (isSingleViewportMode) return;
2803
3104
  const root = viewerRef.current;
2804
3105
  if (!root) return;
2805
3106
  const flushCurrentPage = () => {
@@ -2845,11 +3146,15 @@ var Viewer = ({ engine, style }) => {
2845
3146
  pageElements.forEach((el) => observer.unobserve(el));
2846
3147
  observer.disconnect();
2847
3148
  };
2848
- }, [pageCount, setDocumentState, currentPage]);
3149
+ }, [pageCount, setDocumentState, currentPage, isSingleViewportMode]);
3150
+ const safeCurrentPageIndex = Math.max(
3151
+ 0,
3152
+ Math.min(Math.max(pageCount - 1, 0), currentPage - 1)
3153
+ );
2849
3154
  const virtualOverscan = zoom > 1.35 ? 4 : BASE_OVERSCAN;
2850
- const virtualAnchor = currentPage - 1;
2851
- const virtualStart = Math.max(0, virtualAnchor - virtualOverscan);
2852
- const virtualEnd = Math.min(pageCount - 1, virtualAnchor + virtualOverscan);
3155
+ const virtualAnchor = safeCurrentPageIndex;
3156
+ const virtualStart = isSingleViewportMode ? safeCurrentPageIndex : Math.max(0, virtualAnchor - virtualOverscan);
3157
+ const virtualEnd = isSingleViewportMode ? safeCurrentPageIndex : Math.min(pageCount - 1, virtualAnchor + virtualOverscan);
2853
3158
  const fallbackSize = useMemo3(() => {
2854
3159
  if (basePageSize && availableWidth) {
2855
3160
  const fitScale = Math.min(
@@ -2873,7 +3178,17 @@ var Viewer = ({ engine, style }) => {
2873
3178
  return availableWidth ? Math.max(680, availableWidth * 1.3) : 1100;
2874
3179
  return Math.round(heights.reduce((sum, h) => sum + h, 0) / heights.length);
2875
3180
  }, [pageSizes, availableWidth]);
2876
- const pages = Array.from({ length: pageCount }).map((_, i) => i);
3181
+ const pages = isSingleViewportMode ? pageCount > 0 ? [safeCurrentPageIndex] : [] : Array.from({ length: pageCount }).map((_, i) => i);
3182
+ const viewerStyle = useMemo3(
3183
+ () => isSingleViewportMode ? {
3184
+ ...style ?? {},
3185
+ overflow: "hidden",
3186
+ overflowY: "hidden",
3187
+ overflowX: "hidden",
3188
+ overscrollBehavior: "none"
3189
+ } : style ?? {},
3190
+ [isSingleViewportMode, style]
3191
+ );
2877
3192
  const handlePageMeasured = (pageIndex, size) => {
2878
3193
  setPageSizes((prev) => {
2879
3194
  const current = prev[pageIndex];
@@ -2966,8 +3281,8 @@ var Viewer = ({ engine, style }) => {
2966
3281
  onTouchMove: handleTouchMove,
2967
3282
  onTouchEnd: handleTouchEnd,
2968
3283
  onTouchCancel: handleTouchEnd,
2969
- 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]"}`,
2970
- style,
3284
+ className: `papyrus-viewer papyrus-theme min-h-0 min-w-0 w-full flex-1 ${viewerOverflowClass} flex flex-col items-center ${paddingY} relative custom-scrollbar scroll-smooth ${isDark ? "bg-[#121212]" : "bg-[#e9ecef]"}`,
3285
+ style: viewerStyle,
2971
3286
  children: [
2972
3287
  /* @__PURE__ */ jsx5("div", { className: "flex flex-col items-center gap-6 w-full min-w-0", children: pages.map((idx) => /* @__PURE__ */ jsx5(
2973
3288
  "div",
@@ -2976,13 +3291,14 @@ var Viewer = ({ engine, style }) => {
2976
3291
  pageRefs.current[idx] = element;
2977
3292
  },
2978
3293
  "data-page-index": idx,
2979
- className: "page-container",
3294
+ className: `page-container ${isSingleViewportMode ? "relative" : ""}`,
2980
3295
  children: idx >= virtualStart && idx <= virtualEnd ? /* @__PURE__ */ jsx5(
2981
3296
  PageRenderer_default,
2982
3297
  {
2983
3298
  engine,
2984
3299
  pageIndex: idx,
2985
3300
  availableWidth: availableWidth ?? void 0,
3301
+ availableHeight: availableHeight ?? void 0,
2986
3302
  onMeasuredSize: handlePageMeasured
2987
3303
  }
2988
3304
  ) : /* @__PURE__ */ jsx5(
@@ -2996,8 +3312,94 @@ var Viewer = ({ engine, style }) => {
2996
3312
  }
2997
3313
  )
2998
3314
  },
2999
- idx
3315
+ isSingleViewportMode ? "single-viewport" : idx
3000
3316
  )) }),
3317
+ isSingleViewportMode && pageCount > 1 && viewerBounds && /* @__PURE__ */ jsxs5(
3318
+ "div",
3319
+ {
3320
+ className: "pointer-events-none fixed z-[75] flex items-center justify-between px-1.5 sm:px-2.5",
3321
+ style: {
3322
+ left: viewerBounds.left,
3323
+ width: viewerBounds.width,
3324
+ top: viewerBounds.top + viewerBounds.height / 2,
3325
+ transform: "translateY(-50%)"
3326
+ },
3327
+ children: [
3328
+ /* @__PURE__ */ jsx5(
3329
+ "button",
3330
+ {
3331
+ onClick: () => navigateBy(-1),
3332
+ disabled: !canGoPrev,
3333
+ className: `pointer-events-auto h-12 w-9 sm:h-14 sm:w-10 rounded-lg border backdrop-blur-md transition-all ${!canGoPrev ? "opacity-40 cursor-not-allowed" : "hover:scale-[1.03] active:scale-95"} ${isDark ? "bg-[#111827]/85 text-gray-100" : "bg-white/90 text-gray-700"}`,
3334
+ style: {
3335
+ borderColor: withAlpha3(accentColor, isDark ? 0.45 : 0.3),
3336
+ color: !canGoPrev ? void 0 : accentColor,
3337
+ boxShadow: `0 10px 24px ${withAlpha3(
3338
+ accentColor,
3339
+ isDark ? 0.18 : 0.12
3340
+ )}`
3341
+ },
3342
+ "aria-label": "Cap\xEDtulo anterior",
3343
+ title: "Cap\xEDtulo anterior",
3344
+ children: /* @__PURE__ */ jsx5(
3345
+ "svg",
3346
+ {
3347
+ className: "w-5 h-5 mx-auto",
3348
+ fill: "none",
3349
+ stroke: "currentColor",
3350
+ viewBox: "0 0 24 24",
3351
+ children: /* @__PURE__ */ jsx5(
3352
+ "path",
3353
+ {
3354
+ strokeLinecap: "round",
3355
+ strokeLinejoin: "round",
3356
+ strokeWidth: 2,
3357
+ d: "M15 19l-7-7 7-7"
3358
+ }
3359
+ )
3360
+ }
3361
+ )
3362
+ }
3363
+ ),
3364
+ /* @__PURE__ */ jsx5(
3365
+ "button",
3366
+ {
3367
+ onClick: () => navigateBy(1),
3368
+ disabled: !canGoNext,
3369
+ className: `pointer-events-auto h-12 w-9 sm:h-14 sm:w-10 rounded-lg border backdrop-blur-md transition-all ${!canGoNext ? "opacity-40 cursor-not-allowed" : "hover:scale-[1.03] active:scale-95"} ${isDark ? "bg-[#111827]/85 text-gray-100" : "bg-white/90 text-gray-700"}`,
3370
+ style: {
3371
+ borderColor: withAlpha3(accentColor, isDark ? 0.45 : 0.3),
3372
+ color: !canGoNext ? void 0 : accentColor,
3373
+ boxShadow: `0 10px 24px ${withAlpha3(
3374
+ accentColor,
3375
+ isDark ? 0.18 : 0.12
3376
+ )}`
3377
+ },
3378
+ "aria-label": "Pr\xF3ximo cap\xEDtulo",
3379
+ title: "Pr\xF3ximo cap\xEDtulo",
3380
+ children: /* @__PURE__ */ jsx5(
3381
+ "svg",
3382
+ {
3383
+ className: "w-5 h-5 mx-auto",
3384
+ fill: "none",
3385
+ stroke: "currentColor",
3386
+ viewBox: "0 0 24 24",
3387
+ children: /* @__PURE__ */ jsx5(
3388
+ "path",
3389
+ {
3390
+ strokeLinecap: "round",
3391
+ strokeLinejoin: "round",
3392
+ strokeWidth: 2,
3393
+ d: "M9 5l7 7-7 7"
3394
+ }
3395
+ )
3396
+ }
3397
+ )
3398
+ }
3399
+ )
3400
+ ]
3401
+ }
3402
+ ),
3001
3403
  toolDockOpen && /* @__PURE__ */ jsx5(
3002
3404
  "div",
3003
3405
  {