@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.js CHANGED
@@ -72,6 +72,8 @@ var Topbar = ({
72
72
  const [isMobileViewport, setIsMobileViewport] = (0, import_react.useState)(false);
73
73
  const pageDigits = Math.max(2, String(pageCount || 1).length);
74
74
  const isDark = uiTheme === "dark";
75
+ const renderTargetType = engine.getRenderTargetType?.() ?? "canvas";
76
+ const isSingleViewportMode = renderTargetType === "element" || renderTargetType === "webview";
75
77
  const canUseDOM = typeof document !== "undefined";
76
78
  const hasMobileMenu = showZoomControls || showPageThemeSelector || showUIToggle || showUpload;
77
79
  (0, import_react.useEffect)(() => {
@@ -153,6 +155,10 @@ var Topbar = ({
153
155
  if (pageCount <= 0) return;
154
156
  const nextPage = Math.max(1, Math.min(pageCount, isNaN(page) ? 1 : page));
155
157
  engine.goToPage(nextPage);
158
+ if (isSingleViewportMode) {
159
+ setDocumentState({ currentPage: nextPage, scrollToPageSignal: null });
160
+ return;
161
+ }
156
162
  triggerScrollToPage(nextPage - 1);
157
163
  };
158
164
  const handleFileUpload = async (event) => {
@@ -788,12 +794,20 @@ var withAlpha = (hex, alpha) => {
788
794
  const b = parseInt(value.slice(4, 6), 16);
789
795
  return `rgba(${r}, ${g}, ${b}, ${alpha})`;
790
796
  };
797
+ var isEpubDebugEnabled = () => {
798
+ try {
799
+ return Boolean(globalThis?.__PAPYRUS_EPUB_DEBUG__);
800
+ } catch {
801
+ return false;
802
+ }
803
+ };
791
804
  var Thumbnail = ({ engine, pageIndex, active, isDark, accentColor, onClick }) => {
792
805
  const wrapperRef = (0, import_react2.useRef)(null);
793
806
  const canvasRef = (0, import_react2.useRef)(null);
794
807
  const htmlRef = (0, import_react2.useRef)(null);
795
808
  const accentSoft = withAlpha(accentColor, 0.12);
796
809
  const renderTargetType = engine.getRenderTargetType?.() ?? "canvas";
810
+ const isElementRender = renderTargetType === "element";
797
811
  const [isVisible, setIsVisible] = (0, import_react2.useState)(false);
798
812
  (0, import_react2.useEffect)(() => {
799
813
  const target = wrapperRef.current;
@@ -818,14 +832,14 @@ var Thumbnail = ({ engine, pageIndex, active, isDark, accentColor, onClick }) =>
818
832
  return () => observer.disconnect();
819
833
  }, []);
820
834
  (0, import_react2.useEffect)(() => {
821
- if (renderTargetType === "element" || !isVisible) return;
822
- const target = canvasRef.current;
835
+ if (!isVisible || isElementRender) return;
836
+ const target = renderTargetType === "element" ? htmlRef.current : canvasRef.current;
823
837
  if (target) {
824
838
  engine.renderPage(pageIndex, target, 0.15).catch((err) => {
825
839
  console.error("[Papyrus] Thumbnail render failed:", err);
826
840
  });
827
841
  }
828
- }, [engine, pageIndex, renderTargetType, isVisible]);
842
+ }, [engine, pageIndex, renderTargetType, isVisible, isElementRender]);
829
843
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
830
844
  "div",
831
845
  {
@@ -839,13 +853,20 @@ var Thumbnail = ({ engine, pageIndex, active, isDark, accentColor, onClick }) =>
839
853
  {
840
854
  className: `shadow-lg rounded overflow-hidden mb-2 border ${isDark ? "border-[#333]" : "border-gray-200"}`,
841
855
  children: [
856
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
857
+ "div",
858
+ {
859
+ 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"}`,
860
+ children: "CAP"
861
+ }
862
+ ),
842
863
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
843
864
  "canvas",
844
865
  {
845
866
  ref: canvasRef,
846
867
  className: "max-w-full h-auto bg-white",
847
868
  style: {
848
- display: renderTargetType === "element" ? "none" : "block"
869
+ display: isElementRender ? "none" : "block"
849
870
  }
850
871
  }
851
872
  ),
@@ -857,10 +878,9 @@ var Thumbnail = ({ engine, pageIndex, active, isDark, accentColor, onClick }) =>
857
878
  style: {
858
879
  width: 90,
859
880
  height: 120,
860
- display: renderTargetType === "element" ? "block" : "none",
881
+ display: "none",
861
882
  overflow: "hidden"
862
- },
863
- children: renderTargetType === "element" && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "w-full h-full flex items-center justify-center text-[10px] font-semibold text-gray-500", children: "HTML" })
883
+ }
864
884
  }
865
885
  )
866
886
  ]
@@ -879,9 +899,11 @@ var Thumbnail = ({ engine, pageIndex, active, isDark, accentColor, onClick }) =>
879
899
  );
880
900
  };
881
901
  var OutlineNode = ({ item, engine, isDark, accentColor, depth = 0 }) => {
882
- const { triggerScrollToPage, outlineSearchQuery } = (0, import_core2.useViewerStore)();
902
+ const { triggerScrollToPage, outlineSearchQuery, setDocumentState } = (0, import_core2.useViewerStore)();
883
903
  const [expanded, setExpanded] = (0, import_react2.useState)(true);
884
904
  const accentSoft = withAlpha(accentColor, 0.2);
905
+ const renderTargetType = engine.getRenderTargetType?.() ?? "canvas";
906
+ const isSingleViewportMode = renderTargetType === "element" || renderTargetType === "webview";
885
907
  const matchesSearch = outlineSearchQuery === "" || item.title.toLowerCase().includes(outlineSearchQuery.toLowerCase());
886
908
  const hasMatchingChildren = item.children?.some(
887
909
  (child) => child.title.toLowerCase().includes(outlineSearchQuery.toLowerCase())
@@ -889,10 +911,53 @@ var OutlineNode = ({ item, engine, isDark, accentColor, depth = 0 }) => {
889
911
  if (!matchesSearch && !hasMatchingChildren && outlineSearchQuery !== "")
890
912
  return null;
891
913
  const handleClick = () => {
892
- if (item.pageIndex >= 0) {
893
- engine.goToPage(item.pageIndex + 1);
894
- triggerScrollToPage(item.pageIndex);
895
- }
914
+ void (async () => {
915
+ if (item.pageIndex < 0 && !item.dest) return;
916
+ let targetPageIndex = item.pageIndex;
917
+ let navigatedByDestination = false;
918
+ const destinationEngine = engine;
919
+ if (isSingleViewportMode && item.dest && typeof destinationEngine.goToDestination === "function") {
920
+ try {
921
+ if (isEpubDebugEnabled()) {
922
+ console.log("[EPUBUI] toc-click", {
923
+ title: item.title,
924
+ dest: item.dest,
925
+ pageIndex: item.pageIndex
926
+ });
927
+ }
928
+ const resolved = await destinationEngine.goToDestination(item.dest);
929
+ if (isEpubDebugEnabled()) {
930
+ console.log("[EPUBUI] toc-resolved", {
931
+ title: item.title,
932
+ resolved
933
+ });
934
+ }
935
+ if (resolved != null) targetPageIndex = resolved;
936
+ navigatedByDestination = true;
937
+ } catch {
938
+ }
939
+ }
940
+ if (item.dest && (!navigatedByDestination || targetPageIndex < 0)) {
941
+ try {
942
+ const resolved = await engine.getPageIndex(item.dest);
943
+ if (resolved != null) targetPageIndex = resolved;
944
+ } catch {
945
+ }
946
+ }
947
+ if (navigatedByDestination && isSingleViewportMode) {
948
+ const page2 = targetPageIndex >= 0 ? targetPageIndex + 1 : engine.getCurrentPage();
949
+ setDocumentState({ currentPage: page2, scrollToPageSignal: null });
950
+ return;
951
+ }
952
+ if (targetPageIndex < 0) return;
953
+ const page = targetPageIndex + 1;
954
+ engine.goToPage(page);
955
+ if (isSingleViewportMode) {
956
+ setDocumentState({ currentPage: page, scrollToPageSignal: null });
957
+ } else {
958
+ triggerScrollToPage(targetPageIndex);
959
+ }
960
+ })();
896
961
  };
897
962
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex flex-col", children: [
898
963
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
@@ -974,6 +1039,24 @@ var SidebarLeft = ({ engine, style }) => {
974
1039
  accentColor
975
1040
  } = (0, import_core2.useViewerStore)();
976
1041
  const isDark = uiTheme === "dark";
1042
+ const renderTargetType = engine.getRenderTargetType?.() ?? "canvas";
1043
+ const prefersSummaryByDefault = renderTargetType === "element" || renderTargetType === "webview";
1044
+ const autoSummaryKeyRef = (0, import_react2.useRef)(null);
1045
+ (0, import_react2.useEffect)(() => {
1046
+ if (!prefersSummaryByDefault) return;
1047
+ if (sidebarLeftTab !== "thumbnails") return;
1048
+ if (pageCount <= 0) return;
1049
+ const docKey = `${pageCount}:${outline.length}`;
1050
+ if (autoSummaryKeyRef.current === docKey) return;
1051
+ autoSummaryKeyRef.current = docKey;
1052
+ setSidebarLeftTab("summary");
1053
+ }, [
1054
+ prefersSummaryByDefault,
1055
+ sidebarLeftTab,
1056
+ pageCount,
1057
+ outline.length,
1058
+ setSidebarLeftTab
1059
+ ]);
977
1060
  if (!sidebarLeftOpen) return null;
978
1061
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
979
1062
  "div",
@@ -1087,8 +1170,16 @@ var SidebarLeft = ({ engine, style }) => {
1087
1170
  accentColor,
1088
1171
  active: currentPage === idx + 1,
1089
1172
  onClick: () => {
1090
- engine.goToPage(idx + 1);
1091
- triggerScrollToPage(idx);
1173
+ const page = idx + 1;
1174
+ engine.goToPage(page);
1175
+ if (prefersSummaryByDefault) {
1176
+ setDocumentState({
1177
+ currentPage: page,
1178
+ scrollToPageSignal: null
1179
+ });
1180
+ } else {
1181
+ triggerScrollToPage(idx);
1182
+ }
1092
1183
  }
1093
1184
  },
1094
1185
  idx
@@ -1140,10 +1231,14 @@ var SidebarRight = ({ engine, style }) => {
1140
1231
  } = (0, import_core3.useViewerStore)();
1141
1232
  const [query, setQuery] = (0, import_react3.useState)("");
1142
1233
  const [isSearching, setIsSearching] = (0, import_react3.useState)(false);
1143
- const [contentDrafts, setContentDrafts] = (0, import_react3.useState)({});
1234
+ const [contentDrafts, setContentDrafts] = (0, import_react3.useState)(
1235
+ {}
1236
+ );
1144
1237
  const [replyDrafts, setReplyDrafts] = (0, import_react3.useState)({});
1145
1238
  const searchService = new import_core3.SearchService(engine);
1146
1239
  const isDark = uiTheme === "dark";
1240
+ const renderTargetType = engine.getRenderTargetType?.() ?? "canvas";
1241
+ const isSingleViewportMode = renderTargetType === "element" || renderTargetType === "webview";
1147
1242
  const accentSoft = withAlpha2(accentColor, 0.12);
1148
1243
  const resultsCount = searchResults.length;
1149
1244
  const handleSearch = async (e) => {
@@ -1160,9 +1255,13 @@ var SidebarRight = ({ engine, style }) => {
1160
1255
  const jumpToAnnotation = (annotation) => {
1161
1256
  const page = annotation.pageIndex + 1;
1162
1257
  engine.goToPage(page);
1163
- setDocumentState({ currentPage: page });
1258
+ if (isSingleViewportMode) {
1259
+ setDocumentState({ currentPage: page, scrollToPageSignal: null });
1260
+ } else {
1261
+ setDocumentState({ currentPage: page });
1262
+ triggerScrollToPage(annotation.pageIndex);
1263
+ }
1164
1264
  setSelectedAnnotation(annotation.id);
1165
- triggerScrollToPage(annotation.pageIndex);
1166
1265
  };
1167
1266
  const getContentDraft = (annotation) => {
1168
1267
  if (Object.prototype.hasOwnProperty.call(contentDrafts, annotation.id)) {
@@ -1309,11 +1408,19 @@ var SidebarRight = ({ engine, style }) => {
1309
1408
  onClick: () => {
1310
1409
  const page = res.pageIndex + 1;
1311
1410
  engine.goToPage(page);
1312
- setDocumentState({
1313
- activeSearchIndex: idx,
1314
- currentPage: page
1315
- });
1316
- triggerScrollToPage(res.pageIndex);
1411
+ if (isSingleViewportMode) {
1412
+ setDocumentState({
1413
+ activeSearchIndex: idx,
1414
+ currentPage: page,
1415
+ scrollToPageSignal: null
1416
+ });
1417
+ } else {
1418
+ setDocumentState({
1419
+ activeSearchIndex: idx,
1420
+ currentPage: page
1421
+ });
1422
+ triggerScrollToPage(res.pageIndex);
1423
+ }
1317
1424
  },
1318
1425
  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"}`,
1319
1426
  style: idx === activeSearchIndex ? {
@@ -1400,7 +1507,9 @@ var SidebarRight = ({ engine, style }) => {
1400
1507
  const replies = ann.replies ?? [];
1401
1508
  const contentDraft = getContentDraft(ann);
1402
1509
  const replyDraft = getReplyDraft(ann.id);
1403
- const hasExistingContent = Boolean((ann.content ?? "").trim());
1510
+ const hasExistingContent = Boolean(
1511
+ (ann.content ?? "").trim()
1512
+ );
1404
1513
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1405
1514
  "div",
1406
1515
  {
@@ -1560,6 +1669,7 @@ var PageRenderer = ({
1560
1669
  engine,
1561
1670
  pageIndex,
1562
1671
  availableWidth,
1672
+ availableHeight,
1563
1673
  onMeasuredSize
1564
1674
  }) => {
1565
1675
  const containerRef = (0, import_react4.useRef)(null);
@@ -1601,6 +1711,11 @@ var PageRenderer = ({
1601
1711
  } = (0, import_core4.useViewerStore)();
1602
1712
  const renderTargetType = engine.getRenderTargetType?.() ?? "canvas";
1603
1713
  const isElementRender = renderTargetType === "element";
1714
+ const isLandscape = typeof availableWidth === "number" && typeof availableHeight === "number" && availableWidth > availableHeight;
1715
+ const isLandscapeShort = isLandscape && typeof availableHeight === "number" && availableHeight <= 500;
1716
+ const isMobileElementViewport = isElementRender && typeof availableWidth === "number" && (availableWidth <= 768 || isLandscapeShort);
1717
+ const renderZoomDependency = isElementRender ? 1 : zoom;
1718
+ const renderRotationDependency = isElementRender ? 0 : rotation;
1604
1719
  const textMarkupTools = /* @__PURE__ */ new Set([
1605
1720
  "highlight",
1606
1721
  "underline",
@@ -1630,6 +1745,10 @@ var PageRenderer = ({
1630
1745
  },
1631
1746
  []
1632
1747
  );
1748
+ (0, import_react4.useEffect)(() => {
1749
+ if (!isElementRender) return;
1750
+ setPageSize(null);
1751
+ }, [isElementRender, pageIndex]);
1633
1752
  (0, import_react4.useEffect)(() => {
1634
1753
  let active = true;
1635
1754
  const loadSize = async () => {
@@ -1648,12 +1767,13 @@ var PageRenderer = ({
1648
1767
  };
1649
1768
  }, [engine, pageIndex]);
1650
1769
  const fitScale = (0, import_react4.useMemo)(() => {
1770
+ if (isElementRender && isMobileElementViewport) return 1;
1651
1771
  if (!availableWidth || !pageSize?.width) return 1;
1652
1772
  const targetWidth = Math.max(0, availableWidth - 48);
1653
1773
  if (!targetWidth) return 1;
1654
1774
  const rawScale = Math.min(1, targetWidth / pageSize.width);
1655
1775
  return Math.round(rawScale * SCALE_PRECISION) / SCALE_PRECISION;
1656
- }, [availableWidth, pageSize]);
1776
+ }, [isElementRender, isMobileElementViewport, availableWidth, pageSize]);
1657
1777
  const displaySize = (0, import_react4.useMemo)(() => {
1658
1778
  if (!pageSize) return null;
1659
1779
  const scale = zoom * fitScale;
@@ -1689,6 +1809,15 @@ var PageRenderer = ({
1689
1809
  canvasRef.current.style.height = `${displaySize.height}px`;
1690
1810
  }
1691
1811
  await engine.renderPage(pageIndex, renderTarget, canvasRenderScale);
1812
+ const measuredSize = await engine.getPageDimensions(pageIndex);
1813
+ if (measuredSize.width > 0 && measuredSize.height > 0 && active) {
1814
+ setPageSize((prev) => {
1815
+ if (prev && prev.width === measuredSize.width && prev.height === measuredSize.height) {
1816
+ return prev;
1817
+ }
1818
+ return measuredSize;
1819
+ });
1820
+ }
1692
1821
  if (!isElementRender && !pageSize && canvasRef.current) {
1693
1822
  const denom = canvasRenderScale * Math.max(zoom, 0.01);
1694
1823
  if (denom > 0) {
@@ -1731,13 +1860,24 @@ var PageRenderer = ({
1731
1860
  }, [
1732
1861
  engine,
1733
1862
  pageIndex,
1734
- zoom,
1735
- rotation,
1736
1863
  isElementRender,
1864
+ availableWidth,
1737
1865
  fitScale,
1738
1866
  displaySize,
1739
- pageSize
1867
+ pageSize,
1868
+ renderZoomDependency,
1869
+ renderRotationDependency
1740
1870
  ]);
1871
+ (0, import_react4.useEffect)(() => {
1872
+ if (!isElementRender || pageSize) return;
1873
+ const target = htmlLayerRef.current;
1874
+ if (!target) return;
1875
+ const measuredWidth = target.clientWidth || target.scrollWidth || 0;
1876
+ const measuredHeight = target.clientHeight || target.scrollHeight || 0;
1877
+ if (measuredWidth > 0 && measuredHeight > 0) {
1878
+ setPageSize({ width: measuredWidth, height: measuredHeight });
1879
+ }
1880
+ }, [isElementRender, pageSize, textLayerVersion]);
1741
1881
  (0, import_react4.useEffect)(() => {
1742
1882
  if (isElementRender) return;
1743
1883
  const layer = textLayerRef.current;
@@ -2062,14 +2202,26 @@ var PageRenderer = ({
2062
2202
  return "none";
2063
2203
  }
2064
2204
  };
2205
+ const elementScale = zoom * fitScale;
2206
+ const elementBaseWidth = isElementRender ? isMobileElementViewport && availableWidth != null ? Math.max(260, Math.round(availableWidth)) : pageSize?.width ?? 640 : pageSize?.width ?? 640;
2207
+ const elementBaseHeight = pageSize?.height ?? (isElementRender ? 700 : 900);
2208
+ const elementContainerStyle = isElementRender ? {
2209
+ width: `${Math.max(1, Math.round(elementBaseWidth * elementScale))}px`,
2210
+ height: `${Math.max(
2211
+ 1,
2212
+ Math.round(elementBaseHeight * elementScale)
2213
+ )}px`
2214
+ } : void 0;
2065
2215
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
2066
2216
  "div",
2067
2217
  {
2068
2218
  ref: containerRef,
2069
- className: `relative inline-block shadow-2xl bg-white mb-10 ${canSelectText ? "" : "no-select cursor-crosshair"}`,
2219
+ className: `relative inline-block shadow-2xl bg-white ${isMobileElementViewport ? "mb-0" : "mb-10"} ${canSelectText ? "" : "no-select cursor-crosshair"}`,
2070
2220
  style: {
2071
2221
  scrollMarginTop: "20px",
2072
2222
  minHeight: "100px",
2223
+ overflow: "hidden",
2224
+ ...elementContainerStyle,
2073
2225
  touchAction: activeTool === "ink" || activeTool === "text" || activeTool === "comment" ? "none" : "auto"
2074
2226
  },
2075
2227
  onMouseDown: handleMouseDown,
@@ -2099,7 +2251,11 @@ var PageRenderer = ({
2099
2251
  className: "block",
2100
2252
  style: {
2101
2253
  filter: getPageFilter(),
2102
- display: isElementRender ? "block" : "none"
2254
+ display: isElementRender ? "block" : "none",
2255
+ width: `${elementBaseWidth}px`,
2256
+ height: `${elementBaseHeight}px`,
2257
+ transform: `scale(${elementScale})`,
2258
+ transformOrigin: "top left"
2103
2259
  }
2104
2260
  }
2105
2261
  ),
@@ -2549,6 +2705,15 @@ var PageRenderer_default = PageRenderer;
2549
2705
 
2550
2706
  // components/Viewer.tsx
2551
2707
  var import_jsx_runtime5 = require("react/jsx-runtime");
2708
+ var withAlpha3 = (hex, alpha) => {
2709
+ const normalized = hex.replace("#", "").trim();
2710
+ const value = normalized.length === 3 ? normalized.split("").map((c) => c + c).join("") : normalized;
2711
+ if (value.length !== 6) return hex;
2712
+ const r = parseInt(value.slice(0, 2), 16);
2713
+ const g = parseInt(value.slice(2, 4), 16);
2714
+ const b = parseInt(value.slice(4, 6), 16);
2715
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
2716
+ };
2552
2717
  var BASE_OVERSCAN = 6;
2553
2718
  var MIN_ZOOM = 0.2;
2554
2719
  var MAX_ZOOM = 5;
@@ -2570,6 +2735,7 @@ var Viewer = ({ engine, style }) => {
2570
2735
  uiTheme,
2571
2736
  scrollToPageSignal,
2572
2737
  setDocumentState,
2738
+ triggerScrollToPage,
2573
2739
  accentColor,
2574
2740
  annotationColor,
2575
2741
  setAnnotationColor,
@@ -2577,7 +2743,10 @@ var Viewer = ({ engine, style }) => {
2577
2743
  } = viewerState;
2578
2744
  const mobileTopbarVisible = viewerState.mobileTopbarVisible ?? true;
2579
2745
  const isDark = uiTheme === "dark";
2746
+ const renderTargetType = engine.getRenderTargetType?.() ?? "canvas";
2747
+ const isSingleViewportMode = renderTargetType === "element" || renderTargetType === "webview";
2580
2748
  const viewerRef = (0, import_react5.useRef)(null);
2749
+ const singleNavInFlightRef = (0, import_react5.useRef)(false);
2581
2750
  const colorPickerRef = (0, import_react5.useRef)(null);
2582
2751
  const pageRefs = (0, import_react5.useRef)([]);
2583
2752
  const intersectionRatiosRef = (0, import_react5.useRef)({});
@@ -2600,6 +2769,7 @@ var Viewer = ({ engine, style }) => {
2600
2769
  });
2601
2770
  const [availableWidth, setAvailableWidth] = (0, import_react5.useState)(null);
2602
2771
  const [availableHeight, setAvailableHeight] = (0, import_react5.useState)(null);
2772
+ const [viewerBounds, setViewerBounds] = (0, import_react5.useState)(null);
2603
2773
  const [basePageSize, setBasePageSize] = (0, import_react5.useState)(null);
2604
2774
  const [pageSizes, setPageSizes] = (0, import_react5.useState)({});
2605
2775
  const [colorPickerOpen, setColorPickerOpen] = (0, import_react5.useState)(false);
@@ -2607,7 +2777,7 @@ var Viewer = ({ engine, style }) => {
2607
2777
  const isLandscapeShort = isLandscape && availableHeight !== null && availableHeight <= MOBILE_LANDSCAPE_MAX_HEIGHT_PX2;
2608
2778
  const isCompact = availableWidth !== null && (availableWidth < 820 || isLandscapeShort);
2609
2779
  const isMobileViewport = availableWidth !== null && (availableWidth < 640 || isLandscapeShort);
2610
- const paddingY = isCompact ? "py-10" : "py-16";
2780
+ const paddingY = isSingleViewportMode && isMobileViewport ? "py-0" : isCompact ? "py-10" : "py-16";
2611
2781
  const toolDockPosition = isCompact ? "bottom-4" : "bottom-8";
2612
2782
  const colorPalette = [
2613
2783
  "#fbbf24",
@@ -2619,6 +2789,45 @@ var Viewer = ({ engine, style }) => {
2619
2789
  "#8b5cf6",
2620
2790
  "#111827"
2621
2791
  ];
2792
+ const destinationNavEngine = engine;
2793
+ const canUseDestinationNavigation = isSingleViewportMode && typeof destinationNavEngine.goToAdjacentDestination === "function";
2794
+ const destinationNavigationState = isSingleViewportMode && typeof destinationNavEngine.getDestinationNavigationState === "function" ? destinationNavEngine.getDestinationNavigationState() : null;
2795
+ const canGoPrev = destinationNavigationState?.hasPrev ?? currentPage > 1;
2796
+ const canGoNext = destinationNavigationState?.hasNext ?? currentPage < pageCount;
2797
+ const viewerOverflowClass = isSingleViewportMode ? "overflow-hidden" : "overflow-y-scroll overflow-x-hidden";
2798
+ const navigateBy = (delta) => {
2799
+ if (pageCount <= 0) return;
2800
+ if (canUseDestinationNavigation) {
2801
+ if (singleNavInFlightRef.current) return;
2802
+ singleNavInFlightRef.current = true;
2803
+ void (async () => {
2804
+ try {
2805
+ const resolved = await destinationNavEngine.goToAdjacentDestination(
2806
+ delta
2807
+ );
2808
+ if (resolved == null) return;
2809
+ setDocumentState({
2810
+ currentPage: resolved + 1,
2811
+ scrollToPageSignal: null
2812
+ });
2813
+ } finally {
2814
+ singleNavInFlightRef.current = false;
2815
+ }
2816
+ })();
2817
+ return;
2818
+ }
2819
+ const enginePage = Number(engine.getCurrentPage?.());
2820
+ const normalizedEnginePage = Number.isFinite(enginePage) && enginePage >= 1 ? Math.floor(enginePage) : null;
2821
+ const basePage = normalizedEnginePage != null && Math.abs(normalizedEnginePage - currentPage) <= 1 ? normalizedEnginePage : currentPage;
2822
+ const clampedPage = Math.max(1, Math.min(pageCount, basePage + delta));
2823
+ if (clampedPage === basePage) return;
2824
+ engine.goToPage(clampedPage);
2825
+ if (isSingleViewportMode) {
2826
+ setDocumentState({ currentPage: clampedPage, scrollToPageSignal: null });
2827
+ return;
2828
+ }
2829
+ triggerScrollToPage(clampedPage - 1);
2830
+ };
2622
2831
  const setMobileTopbarVisibility = (visible) => {
2623
2832
  if (mobileTopbarVisibleRef.current === visible) return;
2624
2833
  mobileTopbarVisibleRef.current = visible;
@@ -2705,10 +2914,48 @@ var Viewer = ({ engine, style }) => {
2705
2914
  observer.disconnect();
2706
2915
  };
2707
2916
  }, []);
2917
+ (0, import_react5.useEffect)(() => {
2918
+ if (!isSingleViewportMode) return;
2919
+ const viewerElement = viewerRef.current;
2920
+ if (!viewerElement) return;
2921
+ let rafId = null;
2922
+ const updateBounds = () => {
2923
+ const rect = viewerElement.getBoundingClientRect();
2924
+ setViewerBounds({
2925
+ left: rect.left,
2926
+ width: rect.width,
2927
+ top: rect.top,
2928
+ height: rect.height
2929
+ });
2930
+ };
2931
+ const scheduleUpdate = () => {
2932
+ if (rafId != null) cancelAnimationFrame(rafId);
2933
+ rafId = requestAnimationFrame(() => {
2934
+ rafId = null;
2935
+ updateBounds();
2936
+ });
2937
+ };
2938
+ updateBounds();
2939
+ viewerElement.addEventListener("scroll", scheduleUpdate, { passive: true });
2940
+ window.addEventListener("resize", scheduleUpdate);
2941
+ window.addEventListener("scroll", scheduleUpdate, { passive: true });
2942
+ let observer = null;
2943
+ if (typeof ResizeObserver !== "undefined") {
2944
+ observer = new ResizeObserver(() => scheduleUpdate());
2945
+ observer.observe(viewerElement);
2946
+ }
2947
+ return () => {
2948
+ if (rafId != null) cancelAnimationFrame(rafId);
2949
+ viewerElement.removeEventListener("scroll", scheduleUpdate);
2950
+ window.removeEventListener("resize", scheduleUpdate);
2951
+ window.removeEventListener("scroll", scheduleUpdate);
2952
+ observer?.disconnect();
2953
+ };
2954
+ }, [isSingleViewportMode]);
2708
2955
  (0, import_react5.useEffect)(() => {
2709
2956
  const root = viewerRef.current;
2710
2957
  if (!root) return;
2711
- if (!isMobileViewport) {
2958
+ if (isSingleViewportMode || !isMobileViewport) {
2712
2959
  lastScrollTopRef.current = root.scrollTop;
2713
2960
  scrollDownAccumulatorRef.current = 0;
2714
2961
  scrollUpAccumulatorRef.current = 0;
@@ -2752,7 +2999,7 @@ var Viewer = ({ engine, style }) => {
2752
2999
  return () => {
2753
3000
  root.removeEventListener("scroll", handleScroll);
2754
3001
  };
2755
- }, [isMobileViewport, setDocumentState]);
3002
+ }, [isSingleViewportMode, isMobileViewport, setDocumentState]);
2756
3003
  (0, import_react5.useEffect)(() => {
2757
3004
  const previousPage = previousCurrentPageRef.current;
2758
3005
  previousCurrentPageRef.current = currentPage;
@@ -2781,6 +3028,19 @@ var Viewer = ({ engine, style }) => {
2781
3028
  }, [engine, pageCount]);
2782
3029
  (0, import_react5.useEffect)(() => {
2783
3030
  if (scrollToPageSignal == null) return;
3031
+ if (isSingleViewportMode) {
3032
+ const nextPageIndex = Math.max(
3033
+ 0,
3034
+ Math.min(Math.max(pageCount - 1, 0), scrollToPageSignal)
3035
+ );
3036
+ const root2 = viewerRef.current;
3037
+ if (root2) root2.scrollTop = 0;
3038
+ setDocumentState({
3039
+ currentPage: nextPageIndex + 1,
3040
+ scrollToPageSignal: null
3041
+ });
3042
+ return;
3043
+ }
2784
3044
  const root = viewerRef.current;
2785
3045
  const target = pageRefs.current[scrollToPageSignal];
2786
3046
  if (root) {
@@ -2817,16 +3077,57 @@ var Viewer = ({ engine, style }) => {
2817
3077
  setDocumentState({ scrollToPageSignal: null });
2818
3078
  }, [
2819
3079
  scrollToPageSignal,
3080
+ isSingleViewportMode,
2820
3081
  setDocumentState,
2821
3082
  basePageSize,
2822
3083
  availableWidth,
2823
3084
  zoom,
2824
3085
  pageCount
2825
3086
  ]);
3087
+ (0, import_react5.useEffect)(() => {
3088
+ if (!isSingleViewportMode) return;
3089
+ const root = viewerRef.current;
3090
+ if (!root) return;
3091
+ root.scrollTop = 0;
3092
+ }, [isSingleViewportMode, currentPage]);
2826
3093
  (0, import_react5.useEffect)(() => {
2827
3094
  setPageSizes({});
2828
3095
  }, [zoom]);
2829
3096
  (0, import_react5.useEffect)(() => {
3097
+ if (pageCount <= 1) return;
3098
+ const handleKeyNavigation = (event) => {
3099
+ if (event.defaultPrevented) return;
3100
+ if (event.altKey || event.ctrlKey || event.metaKey) return;
3101
+ const target = event.target;
3102
+ if (target) {
3103
+ const tag = target.tagName;
3104
+ const isEditable = tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || target.isContentEditable || target.getAttribute("contenteditable") === "true";
3105
+ if (isEditable) return;
3106
+ }
3107
+ if (event.key === "ArrowLeft") {
3108
+ event.preventDefault();
3109
+ if (!canGoPrev) return;
3110
+ navigateBy(-1);
3111
+ return;
3112
+ }
3113
+ if (event.key === "ArrowRight") {
3114
+ event.preventDefault();
3115
+ if (!canGoNext) return;
3116
+ navigateBy(1);
3117
+ }
3118
+ };
3119
+ window.addEventListener("keydown", handleKeyNavigation);
3120
+ return () => window.removeEventListener("keydown", handleKeyNavigation);
3121
+ }, [
3122
+ currentPage,
3123
+ pageCount,
3124
+ triggerScrollToPage,
3125
+ engine,
3126
+ canGoPrev,
3127
+ canGoNext
3128
+ ]);
3129
+ (0, import_react5.useEffect)(() => {
3130
+ if (isSingleViewportMode) return;
2830
3131
  const root = viewerRef.current;
2831
3132
  if (!root) return;
2832
3133
  const flushCurrentPage = () => {
@@ -2872,11 +3173,15 @@ var Viewer = ({ engine, style }) => {
2872
3173
  pageElements.forEach((el) => observer.unobserve(el));
2873
3174
  observer.disconnect();
2874
3175
  };
2875
- }, [pageCount, setDocumentState, currentPage]);
3176
+ }, [pageCount, setDocumentState, currentPage, isSingleViewportMode]);
3177
+ const safeCurrentPageIndex = Math.max(
3178
+ 0,
3179
+ Math.min(Math.max(pageCount - 1, 0), currentPage - 1)
3180
+ );
2876
3181
  const virtualOverscan = zoom > 1.35 ? 4 : BASE_OVERSCAN;
2877
- const virtualAnchor = currentPage - 1;
2878
- const virtualStart = Math.max(0, virtualAnchor - virtualOverscan);
2879
- const virtualEnd = Math.min(pageCount - 1, virtualAnchor + virtualOverscan);
3182
+ const virtualAnchor = safeCurrentPageIndex;
3183
+ const virtualStart = isSingleViewportMode ? safeCurrentPageIndex : Math.max(0, virtualAnchor - virtualOverscan);
3184
+ const virtualEnd = isSingleViewportMode ? safeCurrentPageIndex : Math.min(pageCount - 1, virtualAnchor + virtualOverscan);
2880
3185
  const fallbackSize = (0, import_react5.useMemo)(() => {
2881
3186
  if (basePageSize && availableWidth) {
2882
3187
  const fitScale = Math.min(
@@ -2900,7 +3205,17 @@ var Viewer = ({ engine, style }) => {
2900
3205
  return availableWidth ? Math.max(680, availableWidth * 1.3) : 1100;
2901
3206
  return Math.round(heights.reduce((sum, h) => sum + h, 0) / heights.length);
2902
3207
  }, [pageSizes, availableWidth]);
2903
- const pages = Array.from({ length: pageCount }).map((_, i) => i);
3208
+ const pages = isSingleViewportMode ? pageCount > 0 ? [safeCurrentPageIndex] : [] : Array.from({ length: pageCount }).map((_, i) => i);
3209
+ const viewerStyle = (0, import_react5.useMemo)(
3210
+ () => isSingleViewportMode ? {
3211
+ ...style ?? {},
3212
+ overflow: "hidden",
3213
+ overflowY: "hidden",
3214
+ overflowX: "hidden",
3215
+ overscrollBehavior: "none"
3216
+ } : style ?? {},
3217
+ [isSingleViewportMode, style]
3218
+ );
2904
3219
  const handlePageMeasured = (pageIndex, size) => {
2905
3220
  setPageSizes((prev) => {
2906
3221
  const current = prev[pageIndex];
@@ -2993,8 +3308,8 @@ var Viewer = ({ engine, style }) => {
2993
3308
  onTouchMove: handleTouchMove,
2994
3309
  onTouchEnd: handleTouchEnd,
2995
3310
  onTouchCancel: handleTouchEnd,
2996
- 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]"}`,
2997
- style,
3311
+ 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]"}`,
3312
+ style: viewerStyle,
2998
3313
  children: [
2999
3314
  /* @__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)(
3000
3315
  "div",
@@ -3003,13 +3318,14 @@ var Viewer = ({ engine, style }) => {
3003
3318
  pageRefs.current[idx] = element;
3004
3319
  },
3005
3320
  "data-page-index": idx,
3006
- className: "page-container",
3321
+ className: `page-container ${isSingleViewportMode ? "relative" : ""}`,
3007
3322
  children: idx >= virtualStart && idx <= virtualEnd ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
3008
3323
  PageRenderer_default,
3009
3324
  {
3010
3325
  engine,
3011
3326
  pageIndex: idx,
3012
3327
  availableWidth: availableWidth ?? void 0,
3328
+ availableHeight: availableHeight ?? void 0,
3013
3329
  onMeasuredSize: handlePageMeasured
3014
3330
  }
3015
3331
  ) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
@@ -3023,8 +3339,94 @@ var Viewer = ({ engine, style }) => {
3023
3339
  }
3024
3340
  )
3025
3341
  },
3026
- idx
3342
+ isSingleViewportMode ? "single-viewport" : idx
3027
3343
  )) }),
3344
+ isSingleViewportMode && pageCount > 1 && viewerBounds && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
3345
+ "div",
3346
+ {
3347
+ className: "pointer-events-none fixed z-[75] flex items-center justify-between px-1.5 sm:px-2.5",
3348
+ style: {
3349
+ left: viewerBounds.left,
3350
+ width: viewerBounds.width,
3351
+ top: viewerBounds.top + viewerBounds.height / 2,
3352
+ transform: "translateY(-50%)"
3353
+ },
3354
+ children: [
3355
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
3356
+ "button",
3357
+ {
3358
+ onClick: () => navigateBy(-1),
3359
+ disabled: !canGoPrev,
3360
+ 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"}`,
3361
+ style: {
3362
+ borderColor: withAlpha3(accentColor, isDark ? 0.45 : 0.3),
3363
+ color: !canGoPrev ? void 0 : accentColor,
3364
+ boxShadow: `0 10px 24px ${withAlpha3(
3365
+ accentColor,
3366
+ isDark ? 0.18 : 0.12
3367
+ )}`
3368
+ },
3369
+ "aria-label": "Cap\xEDtulo anterior",
3370
+ title: "Cap\xEDtulo anterior",
3371
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
3372
+ "svg",
3373
+ {
3374
+ className: "w-5 h-5 mx-auto",
3375
+ fill: "none",
3376
+ stroke: "currentColor",
3377
+ viewBox: "0 0 24 24",
3378
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
3379
+ "path",
3380
+ {
3381
+ strokeLinecap: "round",
3382
+ strokeLinejoin: "round",
3383
+ strokeWidth: 2,
3384
+ d: "M15 19l-7-7 7-7"
3385
+ }
3386
+ )
3387
+ }
3388
+ )
3389
+ }
3390
+ ),
3391
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
3392
+ "button",
3393
+ {
3394
+ onClick: () => navigateBy(1),
3395
+ disabled: !canGoNext,
3396
+ 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"}`,
3397
+ style: {
3398
+ borderColor: withAlpha3(accentColor, isDark ? 0.45 : 0.3),
3399
+ color: !canGoNext ? void 0 : accentColor,
3400
+ boxShadow: `0 10px 24px ${withAlpha3(
3401
+ accentColor,
3402
+ isDark ? 0.18 : 0.12
3403
+ )}`
3404
+ },
3405
+ "aria-label": "Pr\xF3ximo cap\xEDtulo",
3406
+ title: "Pr\xF3ximo cap\xEDtulo",
3407
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
3408
+ "svg",
3409
+ {
3410
+ className: "w-5 h-5 mx-auto",
3411
+ fill: "none",
3412
+ stroke: "currentColor",
3413
+ viewBox: "0 0 24 24",
3414
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
3415
+ "path",
3416
+ {
3417
+ strokeLinecap: "round",
3418
+ strokeLinejoin: "round",
3419
+ strokeWidth: 2,
3420
+ d: "M9 5l7 7-7 7"
3421
+ }
3422
+ )
3423
+ }
3424
+ )
3425
+ }
3426
+ )
3427
+ ]
3428
+ }
3429
+ ),
3028
3430
  toolDockOpen && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
3029
3431
  "div",
3030
3432
  {