@papyrus-sdk/ui-react 0.2.21 → 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
@@ -32,6 +32,8 @@ var import_react = require("react");
32
32
  var import_react_dom = require("react-dom");
33
33
  var import_core = require("@papyrus-sdk/core");
34
34
  var import_jsx_runtime = require("react/jsx-runtime");
35
+ var MOBILE_LANDSCAPE_MAX_HEIGHT_PX = 500;
36
+ var MOBILE_VIEWPORT_QUERY = `(max-width: 639px), (orientation: landscape) and (max-height: ${MOBILE_LANDSCAPE_MAX_HEIGHT_PX}px)`;
35
37
  var Topbar = ({
36
38
  engine,
37
39
  showBrand = false,
@@ -70,6 +72,8 @@ var Topbar = ({
70
72
  const [isMobileViewport, setIsMobileViewport] = (0, import_react.useState)(false);
71
73
  const pageDigits = Math.max(2, String(pageCount || 1).length);
72
74
  const isDark = uiTheme === "dark";
75
+ const renderTargetType = engine.getRenderTargetType?.() ?? "canvas";
76
+ const isSingleViewportMode = renderTargetType === "element" || renderTargetType === "webview";
73
77
  const canUseDOM = typeof document !== "undefined";
74
78
  const hasMobileMenu = showZoomControls || showPageThemeSelector || showUIToggle || showUpload;
75
79
  (0, import_react.useEffect)(() => {
@@ -86,7 +90,7 @@ var Topbar = ({
86
90
  }, [hasMobileMenu]);
87
91
  (0, import_react.useEffect)(() => {
88
92
  if (!canUseDOM || typeof window.matchMedia !== "function") return;
89
- const mediaQuery = window.matchMedia("(max-width: 639px)");
93
+ const mediaQuery = window.matchMedia(MOBILE_VIEWPORT_QUERY);
90
94
  const updateViewport = () => setIsMobileViewport(mediaQuery.matches);
91
95
  updateViewport();
92
96
  if (typeof mediaQuery.addEventListener === "function") {
@@ -151,6 +155,10 @@ var Topbar = ({
151
155
  if (pageCount <= 0) return;
152
156
  const nextPage = Math.max(1, Math.min(pageCount, isNaN(page) ? 1 : page));
153
157
  engine.goToPage(nextPage);
158
+ if (isSingleViewportMode) {
159
+ setDocumentState({ currentPage: nextPage, scrollToPageSignal: null });
160
+ return;
161
+ }
154
162
  triggerScrollToPage(nextPage - 1);
155
163
  };
156
164
  const handleFileUpload = async (event) => {
@@ -786,12 +794,20 @@ var withAlpha = (hex, alpha) => {
786
794
  const b = parseInt(value.slice(4, 6), 16);
787
795
  return `rgba(${r}, ${g}, ${b}, ${alpha})`;
788
796
  };
797
+ var isEpubDebugEnabled = () => {
798
+ try {
799
+ return Boolean(globalThis?.__PAPYRUS_EPUB_DEBUG__);
800
+ } catch {
801
+ return false;
802
+ }
803
+ };
789
804
  var Thumbnail = ({ engine, pageIndex, active, isDark, accentColor, onClick }) => {
790
805
  const wrapperRef = (0, import_react2.useRef)(null);
791
806
  const canvasRef = (0, import_react2.useRef)(null);
792
807
  const htmlRef = (0, import_react2.useRef)(null);
793
808
  const accentSoft = withAlpha(accentColor, 0.12);
794
809
  const renderTargetType = engine.getRenderTargetType?.() ?? "canvas";
810
+ const isElementRender = renderTargetType === "element";
795
811
  const [isVisible, setIsVisible] = (0, import_react2.useState)(false);
796
812
  (0, import_react2.useEffect)(() => {
797
813
  const target = wrapperRef.current;
@@ -816,14 +832,14 @@ var Thumbnail = ({ engine, pageIndex, active, isDark, accentColor, onClick }) =>
816
832
  return () => observer.disconnect();
817
833
  }, []);
818
834
  (0, import_react2.useEffect)(() => {
819
- if (renderTargetType === "element" || !isVisible) return;
820
- const target = canvasRef.current;
835
+ if (!isVisible || isElementRender) return;
836
+ const target = renderTargetType === "element" ? htmlRef.current : canvasRef.current;
821
837
  if (target) {
822
838
  engine.renderPage(pageIndex, target, 0.15).catch((err) => {
823
839
  console.error("[Papyrus] Thumbnail render failed:", err);
824
840
  });
825
841
  }
826
- }, [engine, pageIndex, renderTargetType, isVisible]);
842
+ }, [engine, pageIndex, renderTargetType, isVisible, isElementRender]);
827
843
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
828
844
  "div",
829
845
  {
@@ -837,13 +853,20 @@ var Thumbnail = ({ engine, pageIndex, active, isDark, accentColor, onClick }) =>
837
853
  {
838
854
  className: `shadow-lg rounded overflow-hidden mb-2 border ${isDark ? "border-[#333]" : "border-gray-200"}`,
839
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
+ ),
840
863
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
841
864
  "canvas",
842
865
  {
843
866
  ref: canvasRef,
844
867
  className: "max-w-full h-auto bg-white",
845
868
  style: {
846
- display: renderTargetType === "element" ? "none" : "block"
869
+ display: isElementRender ? "none" : "block"
847
870
  }
848
871
  }
849
872
  ),
@@ -855,10 +878,9 @@ var Thumbnail = ({ engine, pageIndex, active, isDark, accentColor, onClick }) =>
855
878
  style: {
856
879
  width: 90,
857
880
  height: 120,
858
- display: renderTargetType === "element" ? "block" : "none",
881
+ display: "none",
859
882
  overflow: "hidden"
860
- },
861
- 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
+ }
862
884
  }
863
885
  )
864
886
  ]
@@ -877,9 +899,11 @@ var Thumbnail = ({ engine, pageIndex, active, isDark, accentColor, onClick }) =>
877
899
  );
878
900
  };
879
901
  var OutlineNode = ({ item, engine, isDark, accentColor, depth = 0 }) => {
880
- const { triggerScrollToPage, outlineSearchQuery } = (0, import_core2.useViewerStore)();
902
+ const { triggerScrollToPage, outlineSearchQuery, setDocumentState } = (0, import_core2.useViewerStore)();
881
903
  const [expanded, setExpanded] = (0, import_react2.useState)(true);
882
904
  const accentSoft = withAlpha(accentColor, 0.2);
905
+ const renderTargetType = engine.getRenderTargetType?.() ?? "canvas";
906
+ const isSingleViewportMode = renderTargetType === "element" || renderTargetType === "webview";
883
907
  const matchesSearch = outlineSearchQuery === "" || item.title.toLowerCase().includes(outlineSearchQuery.toLowerCase());
884
908
  const hasMatchingChildren = item.children?.some(
885
909
  (child) => child.title.toLowerCase().includes(outlineSearchQuery.toLowerCase())
@@ -887,10 +911,53 @@ var OutlineNode = ({ item, engine, isDark, accentColor, depth = 0 }) => {
887
911
  if (!matchesSearch && !hasMatchingChildren && outlineSearchQuery !== "")
888
912
  return null;
889
913
  const handleClick = () => {
890
- if (item.pageIndex >= 0) {
891
- engine.goToPage(item.pageIndex + 1);
892
- triggerScrollToPage(item.pageIndex);
893
- }
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
+ })();
894
961
  };
895
962
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex flex-col", children: [
896
963
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
@@ -972,6 +1039,24 @@ var SidebarLeft = ({ engine, style }) => {
972
1039
  accentColor
973
1040
  } = (0, import_core2.useViewerStore)();
974
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
+ ]);
975
1060
  if (!sidebarLeftOpen) return null;
976
1061
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
977
1062
  "div",
@@ -1085,8 +1170,16 @@ var SidebarLeft = ({ engine, style }) => {
1085
1170
  accentColor,
1086
1171
  active: currentPage === idx + 1,
1087
1172
  onClick: () => {
1088
- engine.goToPage(idx + 1);
1089
- 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
+ }
1090
1183
  }
1091
1184
  },
1092
1185
  idx
@@ -1138,10 +1231,14 @@ var SidebarRight = ({ engine, style }) => {
1138
1231
  } = (0, import_core3.useViewerStore)();
1139
1232
  const [query, setQuery] = (0, import_react3.useState)("");
1140
1233
  const [isSearching, setIsSearching] = (0, import_react3.useState)(false);
1141
- const [contentDrafts, setContentDrafts] = (0, import_react3.useState)({});
1234
+ const [contentDrafts, setContentDrafts] = (0, import_react3.useState)(
1235
+ {}
1236
+ );
1142
1237
  const [replyDrafts, setReplyDrafts] = (0, import_react3.useState)({});
1143
1238
  const searchService = new import_core3.SearchService(engine);
1144
1239
  const isDark = uiTheme === "dark";
1240
+ const renderTargetType = engine.getRenderTargetType?.() ?? "canvas";
1241
+ const isSingleViewportMode = renderTargetType === "element" || renderTargetType === "webview";
1145
1242
  const accentSoft = withAlpha2(accentColor, 0.12);
1146
1243
  const resultsCount = searchResults.length;
1147
1244
  const handleSearch = async (e) => {
@@ -1158,9 +1255,13 @@ var SidebarRight = ({ engine, style }) => {
1158
1255
  const jumpToAnnotation = (annotation) => {
1159
1256
  const page = annotation.pageIndex + 1;
1160
1257
  engine.goToPage(page);
1161
- setDocumentState({ currentPage: page });
1258
+ if (isSingleViewportMode) {
1259
+ setDocumentState({ currentPage: page, scrollToPageSignal: null });
1260
+ } else {
1261
+ setDocumentState({ currentPage: page });
1262
+ triggerScrollToPage(annotation.pageIndex);
1263
+ }
1162
1264
  setSelectedAnnotation(annotation.id);
1163
- triggerScrollToPage(annotation.pageIndex);
1164
1265
  };
1165
1266
  const getContentDraft = (annotation) => {
1166
1267
  if (Object.prototype.hasOwnProperty.call(contentDrafts, annotation.id)) {
@@ -1307,11 +1408,19 @@ var SidebarRight = ({ engine, style }) => {
1307
1408
  onClick: () => {
1308
1409
  const page = res.pageIndex + 1;
1309
1410
  engine.goToPage(page);
1310
- setDocumentState({
1311
- activeSearchIndex: idx,
1312
- currentPage: page
1313
- });
1314
- 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
+ }
1315
1424
  },
1316
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"}`,
1317
1426
  style: idx === activeSearchIndex ? {
@@ -1398,7 +1507,9 @@ var SidebarRight = ({ engine, style }) => {
1398
1507
  const replies = ann.replies ?? [];
1399
1508
  const contentDraft = getContentDraft(ann);
1400
1509
  const replyDraft = getReplyDraft(ann.id);
1401
- const hasExistingContent = Boolean((ann.content ?? "").trim());
1510
+ const hasExistingContent = Boolean(
1511
+ (ann.content ?? "").trim()
1512
+ );
1402
1513
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1403
1514
  "div",
1404
1515
  {
@@ -1558,6 +1669,7 @@ var PageRenderer = ({
1558
1669
  engine,
1559
1670
  pageIndex,
1560
1671
  availableWidth,
1672
+ availableHeight,
1561
1673
  onMeasuredSize
1562
1674
  }) => {
1563
1675
  const containerRef = (0, import_react4.useRef)(null);
@@ -1599,6 +1711,11 @@ var PageRenderer = ({
1599
1711
  } = (0, import_core4.useViewerStore)();
1600
1712
  const renderTargetType = engine.getRenderTargetType?.() ?? "canvas";
1601
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;
1602
1719
  const textMarkupTools = /* @__PURE__ */ new Set([
1603
1720
  "highlight",
1604
1721
  "underline",
@@ -1628,6 +1745,10 @@ var PageRenderer = ({
1628
1745
  },
1629
1746
  []
1630
1747
  );
1748
+ (0, import_react4.useEffect)(() => {
1749
+ if (!isElementRender) return;
1750
+ setPageSize(null);
1751
+ }, [isElementRender, pageIndex]);
1631
1752
  (0, import_react4.useEffect)(() => {
1632
1753
  let active = true;
1633
1754
  const loadSize = async () => {
@@ -1646,12 +1767,13 @@ var PageRenderer = ({
1646
1767
  };
1647
1768
  }, [engine, pageIndex]);
1648
1769
  const fitScale = (0, import_react4.useMemo)(() => {
1770
+ if (isElementRender && isMobileElementViewport) return 1;
1649
1771
  if (!availableWidth || !pageSize?.width) return 1;
1650
1772
  const targetWidth = Math.max(0, availableWidth - 48);
1651
1773
  if (!targetWidth) return 1;
1652
1774
  const rawScale = Math.min(1, targetWidth / pageSize.width);
1653
1775
  return Math.round(rawScale * SCALE_PRECISION) / SCALE_PRECISION;
1654
- }, [availableWidth, pageSize]);
1776
+ }, [isElementRender, isMobileElementViewport, availableWidth, pageSize]);
1655
1777
  const displaySize = (0, import_react4.useMemo)(() => {
1656
1778
  if (!pageSize) return null;
1657
1779
  const scale = zoom * fitScale;
@@ -1687,6 +1809,15 @@ var PageRenderer = ({
1687
1809
  canvasRef.current.style.height = `${displaySize.height}px`;
1688
1810
  }
1689
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
+ }
1690
1821
  if (!isElementRender && !pageSize && canvasRef.current) {
1691
1822
  const denom = canvasRenderScale * Math.max(zoom, 0.01);
1692
1823
  if (denom > 0) {
@@ -1729,13 +1860,24 @@ var PageRenderer = ({
1729
1860
  }, [
1730
1861
  engine,
1731
1862
  pageIndex,
1732
- zoom,
1733
- rotation,
1734
1863
  isElementRender,
1864
+ availableWidth,
1735
1865
  fitScale,
1736
1866
  displaySize,
1737
- pageSize
1867
+ pageSize,
1868
+ renderZoomDependency,
1869
+ renderRotationDependency
1738
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]);
1739
1881
  (0, import_react4.useEffect)(() => {
1740
1882
  if (isElementRender) return;
1741
1883
  const layer = textLayerRef.current;
@@ -1797,8 +1939,12 @@ var PageRenderer = ({
1797
1939
  activeSearchIndex,
1798
1940
  textLayerVersion
1799
1941
  ]);
1800
- const handleMouseDown = (e) => {
1801
- const target = e.target;
1942
+ const getTouchPoint = (event) => {
1943
+ const touch = event.touches[0] ?? event.changedTouches[0];
1944
+ if (!touch) return null;
1945
+ return { x: touch.clientX, y: touch.clientY };
1946
+ };
1947
+ const handlePointerDown = (clientX, clientY, target) => {
1802
1948
  const clickedInsideAnnotation = Boolean(
1803
1949
  target?.closest("[data-papyrus-annotation-id]")
1804
1950
  );
@@ -1812,8 +1958,8 @@ var PageRenderer = ({
1812
1958
  if (activeTool === "ink") {
1813
1959
  const rect2 = containerRef.current?.getBoundingClientRect();
1814
1960
  if (!rect2) return;
1815
- const x2 = (e.clientX - rect2.left) / rect2.width;
1816
- const y2 = (e.clientY - rect2.top) / rect2.height;
1961
+ const x2 = (clientX - rect2.left) / rect2.width;
1962
+ const y2 = (clientY - rect2.top) / rect2.height;
1817
1963
  setIsInkDrawing(true);
1818
1964
  setInkPoints([{ x: x2, y: y2 }]);
1819
1965
  return;
@@ -1822,25 +1968,25 @@ var PageRenderer = ({
1822
1968
  const rect = containerRef.current?.getBoundingClientRect();
1823
1969
  if (!rect) return;
1824
1970
  setIsDragging(true);
1825
- const x = e.clientX - rect.left;
1826
- const y = e.clientY - rect.top;
1971
+ const x = clientX - rect.left;
1972
+ const y = clientY - rect.top;
1827
1973
  setStartPos({ x, y });
1828
1974
  setCurrentRect({ x, y, w: 0, h: 0 });
1829
1975
  };
1830
- const handleMouseMove = (e) => {
1976
+ const handlePointerMove = (clientX, clientY) => {
1831
1977
  if (isInkDrawing) {
1832
1978
  const rect2 = containerRef.current?.getBoundingClientRect();
1833
1979
  if (!rect2) return;
1834
- const x = (e.clientX - rect2.left) / rect2.width;
1835
- const y = (e.clientY - rect2.top) / rect2.height;
1980
+ const x = (clientX - rect2.left) / rect2.width;
1981
+ const y = (clientY - rect2.top) / rect2.height;
1836
1982
  setInkPoints((prev) => [...prev, { x, y }]);
1837
1983
  return;
1838
1984
  }
1839
1985
  if (!isDragging) return;
1840
1986
  const rect = containerRef.current?.getBoundingClientRect();
1841
1987
  if (!rect) return;
1842
- const currentX = e.clientX - rect.left;
1843
- const currentY = e.clientY - rect.top;
1988
+ const currentX = clientX - rect.left;
1989
+ const currentY = clientY - rect.top;
1844
1990
  setCurrentRect({
1845
1991
  x: Math.min(startPos.x, currentX),
1846
1992
  y: Math.min(startPos.y, currentY),
@@ -1848,7 +1994,7 @@ var PageRenderer = ({
1848
1994
  h: Math.abs(currentY - startPos.y)
1849
1995
  });
1850
1996
  };
1851
- const handleMouseUp = (e) => {
1997
+ const handlePointerUp = () => {
1852
1998
  if (isInkDrawing) {
1853
1999
  setIsInkDrawing(false);
1854
2000
  if (inkPoints.length > 1) {
@@ -2013,6 +2159,37 @@ var PageRenderer = ({
2013
2159
  }
2014
2160
  }
2015
2161
  };
2162
+ const handleMouseDown = (e) => {
2163
+ handlePointerDown(e.clientX, e.clientY, e.target);
2164
+ };
2165
+ const handleMouseMove = (e) => {
2166
+ handlePointerMove(e.clientX, e.clientY);
2167
+ };
2168
+ const handleMouseUp = () => {
2169
+ handlePointerUp();
2170
+ };
2171
+ const handleTouchStart = (event) => {
2172
+ if (event.touches.length > 1) return;
2173
+ const point = getTouchPoint(event);
2174
+ if (!point) return;
2175
+ handlePointerDown(point.x, point.y, event.target);
2176
+ if ((activeTool === "ink" || !canSelectText) && event.cancelable) {
2177
+ event.preventDefault();
2178
+ }
2179
+ };
2180
+ const handleTouchMove = (event) => {
2181
+ if (event.touches.length > 1) return;
2182
+ const point = getTouchPoint(event);
2183
+ if (!point) return;
2184
+ handlePointerMove(point.x, point.y);
2185
+ if ((isInkDrawing || isDragging) && event.cancelable) {
2186
+ event.preventDefault();
2187
+ }
2188
+ };
2189
+ const handleTouchEnd = (event) => {
2190
+ if (event.touches.length > 0) return;
2191
+ handlePointerUp();
2192
+ };
2016
2193
  const getPageFilter = () => {
2017
2194
  switch (pageTheme) {
2018
2195
  case "sepia":
@@ -2025,15 +2202,35 @@ var PageRenderer = ({
2025
2202
  return "none";
2026
2203
  }
2027
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;
2028
2215
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
2029
2216
  "div",
2030
2217
  {
2031
2218
  ref: containerRef,
2032
- className: `relative inline-block shadow-2xl bg-white mb-10 ${canSelectText ? "" : "no-select cursor-crosshair"}`,
2033
- style: { scrollMarginTop: "20px", minHeight: "100px" },
2219
+ className: `relative inline-block shadow-2xl bg-white ${isMobileElementViewport ? "mb-0" : "mb-10"} ${canSelectText ? "" : "no-select cursor-crosshair"}`,
2220
+ style: {
2221
+ scrollMarginTop: "20px",
2222
+ minHeight: "100px",
2223
+ overflow: "hidden",
2224
+ ...elementContainerStyle,
2225
+ touchAction: activeTool === "ink" || activeTool === "text" || activeTool === "comment" ? "none" : "auto"
2226
+ },
2034
2227
  onMouseDown: handleMouseDown,
2035
2228
  onMouseMove: handleMouseMove,
2036
2229
  onMouseUp: handleMouseUp,
2230
+ onTouchStart: handleTouchStart,
2231
+ onTouchMove: handleTouchMove,
2232
+ onTouchEnd: handleTouchEnd,
2233
+ onTouchCancel: handleTouchEnd,
2037
2234
  children: [
2038
2235
  loading && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "absolute inset-0 bg-gray-50 flex items-center justify-center z-10 animate-pulse", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-[10px] font-black text-gray-400 uppercase tracking-widest", children: "Sincronizando..." }) }),
2039
2236
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
@@ -2054,7 +2251,11 @@ var PageRenderer = ({
2054
2251
  className: "block",
2055
2252
  style: {
2056
2253
  filter: getPageFilter(),
2057
- 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"
2058
2259
  }
2059
2260
  }
2060
2261
  ),
@@ -2504,14 +2705,26 @@ var PageRenderer_default = PageRenderer;
2504
2705
 
2505
2706
  // components/Viewer.tsx
2506
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
+ };
2507
2717
  var BASE_OVERSCAN = 6;
2508
2718
  var MIN_ZOOM = 0.2;
2509
2719
  var MAX_ZOOM = 5;
2510
2720
  var WIDTH_SNAP_PX = 4;
2511
2721
  var WIDTH_HYSTERESIS_PX = 6;
2722
+ var HEIGHT_SNAP_PX = 4;
2723
+ var HEIGHT_HYSTERESIS_PX = 6;
2512
2724
  var MOBILE_HEADER_HIDE_DELTA_PX = 28;
2513
2725
  var MOBILE_HEADER_SHOW_DELTA_PX = 16;
2514
2726
  var MOBILE_HEADER_TOP_RESET_PX = 12;
2727
+ var MOBILE_LANDSCAPE_MAX_HEIGHT_PX2 = 500;
2515
2728
  var Viewer = ({ engine, style }) => {
2516
2729
  const viewerState = (0, import_core5.useViewerStore)();
2517
2730
  const {
@@ -2522,6 +2735,7 @@ var Viewer = ({ engine, style }) => {
2522
2735
  uiTheme,
2523
2736
  scrollToPageSignal,
2524
2737
  setDocumentState,
2738
+ triggerScrollToPage,
2525
2739
  accentColor,
2526
2740
  annotationColor,
2527
2741
  setAnnotationColor,
@@ -2529,7 +2743,10 @@ var Viewer = ({ engine, style }) => {
2529
2743
  } = viewerState;
2530
2744
  const mobileTopbarVisible = viewerState.mobileTopbarVisible ?? true;
2531
2745
  const isDark = uiTheme === "dark";
2746
+ const renderTargetType = engine.getRenderTargetType?.() ?? "canvas";
2747
+ const isSingleViewportMode = renderTargetType === "element" || renderTargetType === "webview";
2532
2748
  const viewerRef = (0, import_react5.useRef)(null);
2749
+ const singleNavInFlightRef = (0, import_react5.useRef)(false);
2533
2750
  const colorPickerRef = (0, import_react5.useRef)(null);
2534
2751
  const pageRefs = (0, import_react5.useRef)([]);
2535
2752
  const intersectionRatiosRef = (0, import_react5.useRef)({});
@@ -2537,6 +2754,7 @@ var Viewer = ({ engine, style }) => {
2537
2754
  const jumpRef = (0, import_react5.useRef)(false);
2538
2755
  const jumpTimerRef = (0, import_react5.useRef)(null);
2539
2756
  const lastWidthRef = (0, import_react5.useRef)(null);
2757
+ const lastHeightRef = (0, import_react5.useRef)(null);
2540
2758
  const lastScrollTopRef = (0, import_react5.useRef)(0);
2541
2759
  const scrollDownAccumulatorRef = (0, import_react5.useRef)(0);
2542
2760
  const scrollUpAccumulatorRef = (0, import_react5.useRef)(0);
@@ -2550,12 +2768,16 @@ var Viewer = ({ engine, style }) => {
2550
2768
  rafId: null
2551
2769
  });
2552
2770
  const [availableWidth, setAvailableWidth] = (0, import_react5.useState)(null);
2771
+ const [availableHeight, setAvailableHeight] = (0, import_react5.useState)(null);
2772
+ const [viewerBounds, setViewerBounds] = (0, import_react5.useState)(null);
2553
2773
  const [basePageSize, setBasePageSize] = (0, import_react5.useState)(null);
2554
2774
  const [pageSizes, setPageSizes] = (0, import_react5.useState)({});
2555
2775
  const [colorPickerOpen, setColorPickerOpen] = (0, import_react5.useState)(false);
2556
- const isCompact = availableWidth !== null && availableWidth < 820;
2557
- const isMobileViewport = availableWidth !== null && availableWidth < 640;
2558
- const paddingY = isCompact ? "py-10" : "py-16";
2776
+ const isLandscape = availableWidth !== null && availableHeight !== null && availableWidth > availableHeight;
2777
+ const isLandscapeShort = isLandscape && availableHeight !== null && availableHeight <= MOBILE_LANDSCAPE_MAX_HEIGHT_PX2;
2778
+ const isCompact = availableWidth !== null && (availableWidth < 820 || isLandscapeShort);
2779
+ const isMobileViewport = availableWidth !== null && (availableWidth < 640 || isLandscapeShort);
2780
+ const paddingY = isSingleViewportMode && isMobileViewport ? "py-0" : isCompact ? "py-10" : "py-16";
2559
2781
  const toolDockPosition = isCompact ? "bottom-4" : "bottom-8";
2560
2782
  const colorPalette = [
2561
2783
  "#fbbf24",
@@ -2567,6 +2789,45 @@ var Viewer = ({ engine, style }) => {
2567
2789
  "#8b5cf6",
2568
2790
  "#111827"
2569
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
+ };
2570
2831
  const setMobileTopbarVisibility = (visible) => {
2571
2832
  if (mobileTopbarVisibleRef.current === visible) return;
2572
2833
  mobileTopbarVisibleRef.current = visible;
@@ -2605,21 +2866,32 @@ var Viewer = ({ engine, style }) => {
2605
2866
  const measurementTarget = viewerElement.parentElement ?? viewerElement;
2606
2867
  let rafId = null;
2607
2868
  const normalizeWidth = (rawWidth) => Math.max(0, Math.floor(rawWidth / WIDTH_SNAP_PX) * WIDTH_SNAP_PX);
2608
- const updateWidth = () => {
2869
+ const normalizeHeight = (rawHeight) => Math.max(0, Math.floor(rawHeight / HEIGHT_SNAP_PX) * HEIGHT_SNAP_PX);
2870
+ const updateSize = () => {
2609
2871
  const rawWidth = measurementTarget.getBoundingClientRect?.().width ?? measurementTarget.clientWidth ?? measurementTarget.offsetWidth;
2872
+ const rawHeight = measurementTarget.getBoundingClientRect?.().height ?? measurementTarget.clientHeight ?? measurementTarget.offsetHeight;
2610
2873
  const nextWidth = normalizeWidth(rawWidth);
2611
- if (nextWidth <= 0) return;
2874
+ const nextHeight = normalizeHeight(rawHeight);
2875
+ if (nextWidth <= 0 || nextHeight <= 0) return;
2612
2876
  const previousWidth = lastWidthRef.current;
2613
- if (previousWidth != null && Math.abs(nextWidth - previousWidth) < WIDTH_HYSTERESIS_PX)
2614
- return;
2615
- lastWidthRef.current = nextWidth;
2616
- setAvailableWidth(nextWidth);
2877
+ const previousHeight = lastHeightRef.current;
2878
+ const widthChanged = previousWidth == null || Math.abs(nextWidth - previousWidth) >= WIDTH_HYSTERESIS_PX;
2879
+ const heightChanged = previousHeight == null || Math.abs(nextHeight - previousHeight) >= HEIGHT_HYSTERESIS_PX;
2880
+ if (!widthChanged && !heightChanged) return;
2881
+ if (widthChanged) {
2882
+ lastWidthRef.current = nextWidth;
2883
+ setAvailableWidth(nextWidth);
2884
+ }
2885
+ if (heightChanged) {
2886
+ lastHeightRef.current = nextHeight;
2887
+ setAvailableHeight(nextHeight);
2888
+ }
2617
2889
  };
2618
2890
  const scheduleWidthUpdate = () => {
2619
2891
  if (rafId != null) cancelAnimationFrame(rafId);
2620
2892
  rafId = requestAnimationFrame(() => {
2621
2893
  rafId = null;
2622
- updateWidth();
2894
+ updateSize();
2623
2895
  });
2624
2896
  };
2625
2897
  scheduleWidthUpdate();
@@ -2642,10 +2914,48 @@ var Viewer = ({ engine, style }) => {
2642
2914
  observer.disconnect();
2643
2915
  };
2644
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]);
2645
2955
  (0, import_react5.useEffect)(() => {
2646
2956
  const root = viewerRef.current;
2647
2957
  if (!root) return;
2648
- if (!isMobileViewport) {
2958
+ if (isSingleViewportMode || !isMobileViewport) {
2649
2959
  lastScrollTopRef.current = root.scrollTop;
2650
2960
  scrollDownAccumulatorRef.current = 0;
2651
2961
  scrollUpAccumulatorRef.current = 0;
@@ -2689,7 +2999,7 @@ var Viewer = ({ engine, style }) => {
2689
2999
  return () => {
2690
3000
  root.removeEventListener("scroll", handleScroll);
2691
3001
  };
2692
- }, [isMobileViewport, setDocumentState]);
3002
+ }, [isSingleViewportMode, isMobileViewport, setDocumentState]);
2693
3003
  (0, import_react5.useEffect)(() => {
2694
3004
  const previousPage = previousCurrentPageRef.current;
2695
3005
  previousCurrentPageRef.current = currentPage;
@@ -2718,6 +3028,19 @@ var Viewer = ({ engine, style }) => {
2718
3028
  }, [engine, pageCount]);
2719
3029
  (0, import_react5.useEffect)(() => {
2720
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
+ }
2721
3044
  const root = viewerRef.current;
2722
3045
  const target = pageRefs.current[scrollToPageSignal];
2723
3046
  if (root) {
@@ -2754,16 +3077,57 @@ var Viewer = ({ engine, style }) => {
2754
3077
  setDocumentState({ scrollToPageSignal: null });
2755
3078
  }, [
2756
3079
  scrollToPageSignal,
3080
+ isSingleViewportMode,
2757
3081
  setDocumentState,
2758
3082
  basePageSize,
2759
3083
  availableWidth,
2760
3084
  zoom,
2761
3085
  pageCount
2762
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]);
2763
3093
  (0, import_react5.useEffect)(() => {
2764
3094
  setPageSizes({});
2765
3095
  }, [zoom]);
2766
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;
2767
3131
  const root = viewerRef.current;
2768
3132
  if (!root) return;
2769
3133
  const flushCurrentPage = () => {
@@ -2809,11 +3173,15 @@ var Viewer = ({ engine, style }) => {
2809
3173
  pageElements.forEach((el) => observer.unobserve(el));
2810
3174
  observer.disconnect();
2811
3175
  };
2812
- }, [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
+ );
2813
3181
  const virtualOverscan = zoom > 1.35 ? 4 : BASE_OVERSCAN;
2814
- const virtualAnchor = currentPage - 1;
2815
- const virtualStart = Math.max(0, virtualAnchor - virtualOverscan);
2816
- 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);
2817
3185
  const fallbackSize = (0, import_react5.useMemo)(() => {
2818
3186
  if (basePageSize && availableWidth) {
2819
3187
  const fitScale = Math.min(
@@ -2837,7 +3205,17 @@ var Viewer = ({ engine, style }) => {
2837
3205
  return availableWidth ? Math.max(680, availableWidth * 1.3) : 1100;
2838
3206
  return Math.round(heights.reduce((sum, h) => sum + h, 0) / heights.length);
2839
3207
  }, [pageSizes, availableWidth]);
2840
- 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
+ );
2841
3219
  const handlePageMeasured = (pageIndex, size) => {
2842
3220
  setPageSizes((prev) => {
2843
3221
  const current = prev[pageIndex];
@@ -2930,8 +3308,8 @@ var Viewer = ({ engine, style }) => {
2930
3308
  onTouchMove: handleTouchMove,
2931
3309
  onTouchEnd: handleTouchEnd,
2932
3310
  onTouchCancel: handleTouchEnd,
2933
- 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]"}`,
2934
- 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,
2935
3313
  children: [
2936
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)(
2937
3315
  "div",
@@ -2940,13 +3318,14 @@ var Viewer = ({ engine, style }) => {
2940
3318
  pageRefs.current[idx] = element;
2941
3319
  },
2942
3320
  "data-page-index": idx,
2943
- className: "page-container",
3321
+ className: `page-container ${isSingleViewportMode ? "relative" : ""}`,
2944
3322
  children: idx >= virtualStart && idx <= virtualEnd ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2945
3323
  PageRenderer_default,
2946
3324
  {
2947
3325
  engine,
2948
3326
  pageIndex: idx,
2949
3327
  availableWidth: availableWidth ?? void 0,
3328
+ availableHeight: availableHeight ?? void 0,
2950
3329
  onMeasuredSize: handlePageMeasured
2951
3330
  }
2952
3331
  ) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
@@ -2960,8 +3339,94 @@ var Viewer = ({ engine, style }) => {
2960
3339
  }
2961
3340
  )
2962
3341
  },
2963
- idx
3342
+ isSingleViewportMode ? "single-viewport" : idx
2964
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
+ ),
2965
3430
  toolDockOpen && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2966
3431
  "div",
2967
3432
  {