@papyrus-sdk/ui-react-native 0.2.7 → 0.2.8

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
@@ -575,12 +575,30 @@ https://github.com/nodeca/pako/blob/main/LICENSE\r
575
575
  });
576
576
 
577
577
  // components/Viewer.tsx
578
- import { useMemo as useMemo3, useRef as useRef3, useEffect as useEffect3, useCallback } from "react";
579
- import { FlatList, ScrollView as ScrollView2, StyleSheet as StyleSheet3, View as View3, useWindowDimensions as useWindowDimensions2 } from "react-native";
578
+ import {
579
+ useMemo as useMemo3,
580
+ useRef as useRef3,
581
+ useEffect as useEffect3,
582
+ useCallback as useCallback2,
583
+ useState as useState2
584
+ } from "react";
585
+ import {
586
+ FlatList,
587
+ ScrollView as ScrollView2,
588
+ StyleSheet as StyleSheet3,
589
+ View as View3,
590
+ useWindowDimensions as useWindowDimensions2
591
+ } from "react-native";
580
592
  import { useViewerStore as useViewerStore3 } from "@papyrus-sdk/core";
581
593
 
582
594
  // components/PageRenderer.tsx
583
- import { useEffect, useMemo, useRef, useState } from "react";
595
+ import {
596
+ useCallback,
597
+ useEffect,
598
+ useMemo,
599
+ useRef,
600
+ useState
601
+ } from "react";
584
602
  import {
585
603
  View,
586
604
  StyleSheet,
@@ -592,7 +610,193 @@ import {
592
610
  useWindowDimensions
593
611
  } from "react-native";
594
612
  import { useViewerStore } from "@papyrus-sdk/core";
595
- import { PapyrusPageView } from "@papyrus-sdk/engine-native";
613
+ import {
614
+ PapyrusPageView
615
+ } from "@papyrus-sdk/engine-native";
616
+
617
+ // perf/mobilePerf.ts
618
+ var DEFAULT_PREFIX = "[Papyrus Perf]";
619
+ var FRAME_BUDGET_MS = 1e3 / 60;
620
+ var getPerfGlobal = () => globalThis.__PAPYRUS_MOBILE_PERF__;
621
+ var getPerfConfig = () => {
622
+ const value = getPerfGlobal();
623
+ if (value === true) {
624
+ return {
625
+ enabled: true,
626
+ sampleMemory: true,
627
+ logPrefix: DEFAULT_PREFIX,
628
+ verbose: false
629
+ };
630
+ }
631
+ if (!value || typeof value !== "object") {
632
+ return {
633
+ enabled: false,
634
+ sampleMemory: false,
635
+ logPrefix: DEFAULT_PREFIX,
636
+ verbose: false
637
+ };
638
+ }
639
+ return {
640
+ enabled: value.enabled ?? true,
641
+ sampleMemory: value.sampleMemory ?? true,
642
+ logPrefix: value.logPrefix ?? DEFAULT_PREFIX,
643
+ verbose: value.verbose ?? false
644
+ };
645
+ };
646
+ var round = (value) => Math.round(value * 100) / 100;
647
+ var bytesToMb = (bytes) => round(bytes / (1024 * 1024));
648
+ var getNumericValue = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
649
+ var readHermesHeapBytes = () => {
650
+ const runtimeProperties = globalThis.HermesInternal?.getRuntimeProperties?.();
651
+ if (!runtimeProperties || typeof runtimeProperties !== "object") return null;
652
+ const candidates = [
653
+ "JSHeapSize",
654
+ "js_heap_size",
655
+ "HeapSize",
656
+ "heapSize",
657
+ "TotalAllocatedBytes",
658
+ "totalAllocatedBytes",
659
+ "mallocSize"
660
+ ];
661
+ for (const key of candidates) {
662
+ const value = getNumericValue(runtimeProperties[key]);
663
+ if (value !== null) return value;
664
+ }
665
+ return null;
666
+ };
667
+ var logPerf = (scope, event, payload) => {
668
+ const config = getPerfConfig();
669
+ if (!config.enabled) return;
670
+ const line = `${config.logPrefix}[${scope}] ${event}`;
671
+ if (payload) {
672
+ console.log(line, payload);
673
+ return;
674
+ }
675
+ console.log(line);
676
+ };
677
+ var isMobilePerfEnabled = () => getPerfConfig().enabled;
678
+ var perfNow = () => {
679
+ if (typeof performance !== "undefined" && typeof performance.now === "function") {
680
+ return performance.now();
681
+ }
682
+ return Date.now();
683
+ };
684
+ var sampleMemory = (scope, event, payload) => {
685
+ const config = getPerfConfig();
686
+ if (!config.enabled || !config.sampleMemory) return;
687
+ const performanceMemory = globalThis.performance?.memory;
688
+ const jsHeapUsedBytes = getNumericValue(performanceMemory?.usedJSHeapSize);
689
+ const jsHeapTotalBytes = getNumericValue(performanceMemory?.totalJSHeapSize);
690
+ const hermesHeapBytes = readHermesHeapBytes();
691
+ if (jsHeapUsedBytes === null && jsHeapTotalBytes === null && hermesHeapBytes === null) return;
692
+ logPerf(scope, `memory.${event}`, {
693
+ ...payload,
694
+ jsHeapUsedMb: jsHeapUsedBytes === null ? void 0 : bytesToMb(jsHeapUsedBytes),
695
+ jsHeapTotalMb: jsHeapTotalBytes === null ? void 0 : bytesToMb(jsHeapTotalBytes),
696
+ hermesHeapMb: hermesHeapBytes === null ? void 0 : bytesToMb(hermesHeapBytes)
697
+ });
698
+ };
699
+ var createBurstMonitor = (scope, label, threshold = 12, windowMs = 1e3) => {
700
+ let windowStart = 0;
701
+ let calls = 0;
702
+ return (payload) => {
703
+ const config = getPerfConfig();
704
+ if (!config.enabled) return;
705
+ const now = perfNow();
706
+ if (windowStart === 0 || now - windowStart > windowMs) {
707
+ windowStart = now;
708
+ calls = 0;
709
+ }
710
+ calls += 1;
711
+ if (calls === threshold || config.verbose) {
712
+ logPerf(scope, `${label}.burst`, {
713
+ calls,
714
+ windowMs: round(now - windowStart),
715
+ ...payload
716
+ });
717
+ }
718
+ };
719
+ };
720
+ var createRenderCounter = (scope, label = "render", reportEvery = 30) => {
721
+ let count = 0;
722
+ return (payload) => {
723
+ const config = getPerfConfig();
724
+ if (!config.enabled) return;
725
+ count += 1;
726
+ if (count === 1 || count % reportEvery === 0 || config.verbose) {
727
+ logPerf(scope, label, {
728
+ count,
729
+ ...payload
730
+ });
731
+ }
732
+ };
733
+ };
734
+ var createScrollPerfMonitor = (scope, label = "scroll") => {
735
+ let active = false;
736
+ let startAt = 0;
737
+ let lastEventAt = 0;
738
+ let sampleEvents = 0;
739
+ let droppedFrames = 0;
740
+ let maxFrameGapMs = 0;
741
+ const reset = () => {
742
+ active = false;
743
+ startAt = 0;
744
+ lastEventAt = 0;
745
+ sampleEvents = 0;
746
+ droppedFrames = 0;
747
+ maxFrameGapMs = 0;
748
+ };
749
+ return {
750
+ begin: (reason, payload) => {
751
+ const config = getPerfConfig();
752
+ if (!config.enabled) return;
753
+ if (active) return;
754
+ active = true;
755
+ startAt = perfNow();
756
+ if (reason && config.verbose) {
757
+ logPerf(scope, `${label}.begin`, {
758
+ reason,
759
+ ...payload
760
+ });
761
+ }
762
+ },
763
+ track: (timestampMs) => {
764
+ if (!isMobilePerfEnabled()) return;
765
+ const now = typeof timestampMs === "number" && Number.isFinite(timestampMs) ? timestampMs : perfNow();
766
+ if (!active) {
767
+ active = true;
768
+ startAt = now;
769
+ }
770
+ if (lastEventAt > 0) {
771
+ const frameGap = Math.max(0, now - lastEventAt);
772
+ maxFrameGapMs = Math.max(maxFrameGapMs, frameGap);
773
+ droppedFrames += Math.max(0, Math.round(frameGap / FRAME_BUDGET_MS) - 1);
774
+ }
775
+ sampleEvents += 1;
776
+ lastEventAt = now;
777
+ },
778
+ end: (reason, payload) => {
779
+ if (!isMobilePerfEnabled() || !active) return;
780
+ const stopAt = lastEventAt || perfNow();
781
+ const durationMs = Math.max(0, stopAt - startAt);
782
+ const estimatedFrameTotal = Math.max(sampleEvents + droppedFrames, 1);
783
+ const fpsEstimate = durationMs > 0 ? sampleEvents * 1e3 / durationMs : 0;
784
+ logPerf(scope, `${label}.${reason}`, {
785
+ durationMs: round(durationMs),
786
+ sampleEvents,
787
+ droppedFrames,
788
+ droppedFramesPct: round(droppedFrames / estimatedFrameTotal * 100),
789
+ fpsEstimate: round(fpsEstimate),
790
+ maxFrameGapMs: round(maxFrameGapMs),
791
+ ...payload
792
+ });
793
+ reset();
794
+ }
795
+ };
796
+ };
797
+ var logPerfEvent = logPerf;
798
+
799
+ // components/PageRenderer.tsx
596
800
  import { jsx, jsxs } from "react/jsx-runtime";
597
801
  var PageRenderer = ({
598
802
  engine,
@@ -609,6 +813,11 @@ var PageRenderer = ({
609
813
  const { width: windowWidth } = useWindowDimensions();
610
814
  const isAndroid = Platform.OS === "android";
611
815
  const isNative = Platform.OS === "android" || Platform.OS === "ios";
816
+ const perfEnabled = isMobilePerfEnabled();
817
+ const renderCountRef = useRef(0);
818
+ const setStateBurstRef = useRef(
819
+ createBurstMonitor("PageRenderer", "setDocumentState", 18, 700)
820
+ );
612
821
  const {
613
822
  zoom,
614
823
  rotation,
@@ -625,6 +834,19 @@ var PageRenderer = ({
625
834
  activeSearchIndex,
626
835
  setSelectionActive
627
836
  } = useViewerStore();
837
+ const setDocumentStateTracked = useCallback(
838
+ (state, reason) => {
839
+ if (perfEnabled) {
840
+ setStateBurstRef.current({
841
+ reason,
842
+ page: pageIndex + 1,
843
+ keys: Object.keys(state).join(",")
844
+ });
845
+ }
846
+ setDocumentState(state);
847
+ },
848
+ [pageIndex, perfEnabled, setDocumentState]
849
+ );
628
850
  const pageAnnotations = useMemo(
629
851
  () => annotations.filter((ann) => ann.pageIndex === pageIndex),
630
852
  [annotations, pageIndex]
@@ -633,6 +855,17 @@ var PageRenderer = ({
633
855
  () => searchResults.map((result, index) => ({ result, index })).filter(({ result }) => result.pageIndex === pageIndex),
634
856
  [searchResults, pageIndex]
635
857
  );
858
+ renderCountRef.current += 1;
859
+ if (perfEnabled && (renderCountRef.current === 1 || renderCountRef.current % 20 === 0)) {
860
+ logPerfEvent("PageRenderer", "render", {
861
+ page: pageIndex + 1,
862
+ renderCount: renderCountRef.current,
863
+ zoom,
864
+ rotation,
865
+ annotationCount: pageAnnotations.length,
866
+ searchHits: pageSearchHits.length
867
+ });
868
+ }
636
869
  const [selectionRect, setSelectionRect] = useState(null);
637
870
  const [selectionRects, setSelectionRects] = useState([]);
638
871
  const [selectionBounds, setSelectionBounds] = useState(null);
@@ -642,30 +875,72 @@ var PageRenderer = ({
642
875
  const selectionRectRef = useRef(null);
643
876
  const selectionBoundsRef = useRef(null);
644
877
  const selectionBoundsStart = useRef(null);
645
- const lastTapRef = useRef(null);
878
+ const lastTapRef = useRef(
879
+ null
880
+ );
646
881
  const pinchRef = useRef(null);
647
882
  useEffect(() => {
648
883
  if (!layout.width || !layout.height) return;
649
884
  const viewTag = findNodeHandle(viewRef.current);
650
885
  if (viewTag) {
651
886
  const renderScale = isNative ? scale / Math.max(zoom, 0.5) : scale;
652
- void engine.renderPage(pageIndex, viewTag, renderScale);
887
+ const startedAt = perfEnabled ? perfNow() : 0;
888
+ void Promise.resolve(engine.renderPage(pageIndex, viewTag, renderScale)).then(() => {
889
+ if (!perfEnabled) return;
890
+ const renderDurationMs = perfNow() - startedAt;
891
+ if (renderDurationMs >= 40) {
892
+ logPerfEvent("PageRenderer", "renderPage.slow", {
893
+ page: pageIndex + 1,
894
+ renderDurationMs: Math.round(renderDurationMs * 100) / 100,
895
+ layoutWidth: layout.width,
896
+ layoutHeight: layout.height,
897
+ renderScale: Math.round(renderScale * 100) / 100
898
+ });
899
+ }
900
+ }).catch((error) => {
901
+ logPerfEvent("PageRenderer", "renderPage.error", {
902
+ page: pageIndex + 1,
903
+ message: error instanceof Error ? error.message : String(error)
904
+ });
905
+ });
653
906
  }
654
- }, [engine, pageIndex, scale, zoom, rotation, layout.width, layout.height, isNative]);
907
+ }, [
908
+ engine,
909
+ pageIndex,
910
+ scale,
911
+ zoom,
912
+ rotation,
913
+ layout.width,
914
+ layout.height,
915
+ isNative,
916
+ perfEnabled
917
+ ]);
655
918
  useEffect(() => {
656
919
  let active = true;
657
920
  const loadDimensions = async () => {
921
+ const startedAt = perfEnabled ? perfNow() : 0;
658
922
  const dims = await engine.getPageDimensions(pageIndex);
659
923
  if (!active) return;
660
924
  if (dims.width > 0 && dims.height > 0) {
661
925
  setPageSize({ width: dims.width, height: dims.height });
662
926
  }
927
+ if (perfEnabled) {
928
+ const durationMs = perfNow() - startedAt;
929
+ if (durationMs >= 20 || pageIndex === 0) {
930
+ logPerfEvent("PageRenderer", "pageDimensions", {
931
+ page: pageIndex + 1,
932
+ durationMs: Math.round(durationMs * 100) / 100,
933
+ width: dims.width,
934
+ height: dims.height
935
+ });
936
+ }
937
+ }
663
938
  };
664
939
  void loadDimensions();
665
940
  return () => {
666
941
  active = false;
667
942
  };
668
- }, [engine, pageIndex]);
943
+ }, [engine, pageIndex, perfEnabled]);
669
944
  const handleLayout = (event) => {
670
945
  const { width, height } = event.nativeEvent.layout;
671
946
  if (width !== layout.width || height !== layout.height) {
@@ -765,7 +1040,7 @@ var PageRenderer = ({
765
1040
  if (!distance) return;
766
1041
  const scale2 = distance / pinchRef.current.distance;
767
1042
  const nextZoom = clamp(pinchRef.current.zoom * scale2, 0.5, 4);
768
- setDocumentState({ zoom: nextZoom });
1043
+ setDocumentStateTracked({ zoom: nextZoom }, "pinchMove");
769
1044
  engine.setZoom(nextZoom);
770
1045
  };
771
1046
  const handlePinchEnd = () => {
@@ -813,7 +1088,12 @@ var PageRenderer = ({
813
1088
  const top = Math.max(0, Math.min(start.y, currentY));
814
1089
  const right = Math.min(layout.width, Math.max(start.x, currentX));
815
1090
  const bottom = Math.min(layout.height, Math.max(start.y, currentY));
816
- const rect = { x: left, y: top, width: right - left, height: bottom - top };
1091
+ const rect = {
1092
+ x: left,
1093
+ y: top,
1094
+ width: right - left,
1095
+ height: bottom - top
1096
+ };
817
1097
  selectionRectRef.current = rect;
818
1098
  setSelectionRect(rect);
819
1099
  },
@@ -909,7 +1189,13 @@ var PageRenderer = ({
909
1189
  if (selectionRects.length === 0) return;
910
1190
  if (type === "comment") {
911
1191
  const first = selectionRects[0];
912
- addAnnotationAt(first.x, first.y, Math.max(0.08, first.width), Math.max(0.06, first.height), "comment");
1192
+ addAnnotationAt(
1193
+ first.x,
1194
+ first.y,
1195
+ Math.max(0.08, first.width),
1196
+ Math.max(0.06, first.height),
1197
+ "comment"
1198
+ );
913
1199
  clearSelection();
914
1200
  return;
915
1201
  }
@@ -943,12 +1229,18 @@ var PageRenderer = ({
943
1229
  horizontal: true,
944
1230
  scrollEnabled,
945
1231
  showsHorizontalScrollIndicator: false,
946
- contentContainerStyle: [styles.scrollContent, { paddingHorizontal: horizontalPadding }],
1232
+ contentContainerStyle: [
1233
+ styles.scrollContent,
1234
+ { paddingHorizontal: horizontalPadding }
1235
+ ],
947
1236
  children: /* @__PURE__ */ jsxs(
948
1237
  Pressable,
949
1238
  {
950
1239
  ...panResponder.panHandlers,
951
- style: [styles.container, { width: pageWidth, height: pageHeight, marginBottom: spacing }],
1240
+ style: [
1241
+ styles.container,
1242
+ { width: pageWidth, height: pageHeight, marginBottom: spacing }
1243
+ ],
952
1244
  onLayout: handleLayout,
953
1245
  onPress: handlePress,
954
1246
  onStartShouldSetResponder: (event) => shouldHandlePinch(event.nativeEvent.touches),
@@ -959,7 +1251,13 @@ var PageRenderer = ({
959
1251
  onResponderTerminate: handlePinchEnd,
960
1252
  children: [
961
1253
  /* @__PURE__ */ jsx(PageViewComponent, { ref: viewRef, style: styles.page }),
962
- /* @__PURE__ */ jsx(View, { pointerEvents: "none", style: [styles.themeOverlay, themeOverlayStyle] }),
1254
+ /* @__PURE__ */ jsx(
1255
+ View,
1256
+ {
1257
+ pointerEvents: "none",
1258
+ style: [styles.themeOverlay, themeOverlayStyle]
1259
+ }
1260
+ ),
963
1261
  /* @__PURE__ */ jsxs(View, { pointerEvents: "box-none", style: styles.selectionLayer, children: [
964
1262
  /* @__PURE__ */ jsx(View, { pointerEvents: "none", children: selectionRects.map((rect, index) => {
965
1263
  const style = {
@@ -968,7 +1266,13 @@ var PageRenderer = ({
968
1266
  width: `${rect.width * 100}%`,
969
1267
  height: `${rect.height * 100}%`
970
1268
  };
971
- return /* @__PURE__ */ jsx(View, { style: [styles.selectionHighlight, style] }, `sel-${index}`);
1269
+ return /* @__PURE__ */ jsx(
1270
+ View,
1271
+ {
1272
+ style: [styles.selectionHighlight, style]
1273
+ },
1274
+ `sel-${index}`
1275
+ );
972
1276
  }) }),
973
1277
  selectionBoundsPx ? /* @__PURE__ */ jsxs(
974
1278
  View,
@@ -989,14 +1293,20 @@ var PageRenderer = ({
989
1293
  View,
990
1294
  {
991
1295
  ...startHandleResponder.panHandlers,
992
- style: [styles.selectionHandle, { left: -8, top: -8, borderColor: accentColor }]
1296
+ style: [
1297
+ styles.selectionHandle,
1298
+ { left: -8, top: -8, borderColor: accentColor }
1299
+ ]
993
1300
  }
994
1301
  ),
995
1302
  /* @__PURE__ */ jsx(
996
1303
  View,
997
1304
  {
998
1305
  ...endHandleResponder.panHandlers,
999
- style: [styles.selectionHandle, { right: -8, bottom: -8, borderColor: accentColor }]
1306
+ style: [
1307
+ styles.selectionHandle,
1308
+ { right: -8, bottom: -8, borderColor: accentColor }
1309
+ ]
1000
1310
  }
1001
1311
  )
1002
1312
  ]
@@ -1035,9 +1345,15 @@ var PageRenderer = ({
1035
1345
  {
1036
1346
  style: [
1037
1347
  styles.searchHighlight,
1038
- { borderColor: accentColor, backgroundColor: `${accentColor}26` },
1348
+ {
1349
+ borderColor: accentColor,
1350
+ backgroundColor: `${accentColor}26`
1351
+ },
1039
1352
  isActive && styles.searchHighlightActive,
1040
- isActive && { borderColor: accentColor, backgroundColor: `${accentColor}40` },
1353
+ isActive && {
1354
+ borderColor: accentColor,
1355
+ backgroundColor: `${accentColor}40`
1356
+ },
1041
1357
  highlightStyle
1042
1358
  ]
1043
1359
  },
@@ -1067,8 +1383,29 @@ var PageRenderer = ({
1067
1383
  isSelected && { borderColor: accentColor }
1068
1384
  ],
1069
1385
  children: [
1070
- (ann.type === "comment" || ann.type === "text") && /* @__PURE__ */ jsx(View, { style: [styles.annotationBadge, { borderColor: ann.color }], children: /* @__PURE__ */ jsx(View, { style: [styles.annotationDot, { backgroundColor: ann.color }] }) }),
1071
- isSelected && /* @__PURE__ */ jsx(Pressable, { onPress: () => removeAnnotation(ann.id), style: styles.deleteButton, children: /* @__PURE__ */ jsx(View, { style: styles.deleteDot }) })
1386
+ (ann.type === "comment" || ann.type === "text") && /* @__PURE__ */ jsx(
1387
+ View,
1388
+ {
1389
+ style: [styles.annotationBadge, { borderColor: ann.color }],
1390
+ children: /* @__PURE__ */ jsx(
1391
+ View,
1392
+ {
1393
+ style: [
1394
+ styles.annotationDot,
1395
+ { backgroundColor: ann.color }
1396
+ ]
1397
+ }
1398
+ )
1399
+ }
1400
+ ),
1401
+ isSelected && /* @__PURE__ */ jsx(
1402
+ Pressable,
1403
+ {
1404
+ onPress: () => removeAnnotation(ann.id),
1405
+ style: styles.deleteButton,
1406
+ children: /* @__PURE__ */ jsx(View, { style: styles.deleteDot })
1407
+ }
1408
+ )
1072
1409
  ]
1073
1410
  },
1074
1411
  ann.id
@@ -1086,9 +1423,46 @@ var PageRenderer = ({
1086
1423
  }
1087
1424
  ],
1088
1425
  children: [
1089
- /* @__PURE__ */ jsx(Pressable, { onPress: () => applySelection("comment"), style: styles.selectionAction, children: /* @__PURE__ */ jsx(View, { style: styles.selectionActionDot }) }),
1090
- /* @__PURE__ */ jsx(Pressable, { onPress: () => applySelection("highlight"), style: styles.selectionAction, children: /* @__PURE__ */ jsx(View, { style: [styles.selectionSwatch, { backgroundColor: annotationColor }] }) }),
1091
- /* @__PURE__ */ jsx(Pressable, { onPress: () => applySelection("strikeout"), style: [styles.selectionAction, styles.selectionActionLast], children: /* @__PURE__ */ jsx(View, { style: [styles.selectionStrike, { backgroundColor: annotationColor }] }) })
1426
+ /* @__PURE__ */ jsx(
1427
+ Pressable,
1428
+ {
1429
+ onPress: () => applySelection("comment"),
1430
+ style: styles.selectionAction,
1431
+ children: /* @__PURE__ */ jsx(View, { style: styles.selectionActionDot })
1432
+ }
1433
+ ),
1434
+ /* @__PURE__ */ jsx(
1435
+ Pressable,
1436
+ {
1437
+ onPress: () => applySelection("highlight"),
1438
+ style: styles.selectionAction,
1439
+ children: /* @__PURE__ */ jsx(
1440
+ View,
1441
+ {
1442
+ style: [
1443
+ styles.selectionSwatch,
1444
+ { backgroundColor: annotationColor }
1445
+ ]
1446
+ }
1447
+ )
1448
+ }
1449
+ ),
1450
+ /* @__PURE__ */ jsx(
1451
+ Pressable,
1452
+ {
1453
+ onPress: () => applySelection("strikeout"),
1454
+ style: [styles.selectionAction, styles.selectionActionLast],
1455
+ children: /* @__PURE__ */ jsx(
1456
+ View,
1457
+ {
1458
+ style: [
1459
+ styles.selectionStrike,
1460
+ { backgroundColor: annotationColor }
1461
+ ]
1462
+ }
1463
+ )
1464
+ }
1465
+ )
1092
1466
  ]
1093
1467
  }
1094
1468
  ) : null
@@ -1386,8 +1760,27 @@ var WebViewViewer_default = WebViewViewer;
1386
1760
 
1387
1761
  // components/Viewer.tsx
1388
1762
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1763
+ var LIST_TOP_PADDING = 18;
1764
+ var LIST_BOTTOM_PADDING = 120;
1765
+ var CONTINUOUS_PAGE_SPACING = 28;
1766
+ var DOUBLE_PAGE_SPACING = 20;
1767
+ var DEFAULT_PAGE_ASPECT_RATIO = 0.77;
1768
+ var FLATLIST_WINDOW_SIZE = 8;
1769
+ var FLATLIST_MAX_TO_RENDER_PER_BATCH = 6;
1770
+ var FLATLIST_UPDATE_CELLS_BATCHING_PERIOD = 40;
1771
+ var FLATLIST_INITIAL_NUM_TO_RENDER = 6;
1772
+ var SCROLL_RETRY_DELAY_MS = 120;
1773
+ var SCROLL_MAX_RETRIES = 10;
1389
1774
  var Viewer = ({ engine }) => {
1390
- const { pageCount, currentPage, scrollToPageSignal, setDocumentState, uiTheme, viewMode } = useViewerStore3();
1775
+ const {
1776
+ pageCount,
1777
+ currentPage,
1778
+ scrollToPageSignal,
1779
+ setDocumentState,
1780
+ uiTheme,
1781
+ viewMode,
1782
+ zoom
1783
+ } = useViewerStore3();
1391
1784
  const listRef = useRef3(null);
1392
1785
  const isDark = uiTheme === "dark";
1393
1786
  const { width: windowWidth } = useWindowDimensions2();
@@ -1395,7 +1788,38 @@ var Viewer = ({ engine }) => {
1395
1788
  const isSingle = viewMode === "single";
1396
1789
  const renderTargetType = engine.getRenderTargetType?.() ?? "canvas";
1397
1790
  const isWebView = renderTargetType === "webview";
1398
- const pages = useMemo3(() => Array.from({ length: pageCount }).map((_, i) => i), [pageCount]);
1791
+ const perfEnabled = isMobilePerfEnabled();
1792
+ const mountedAtRef = useRef3(perfNow());
1793
+ const readyLoggedRef = useRef3(false);
1794
+ const renderCounterRef = useRef3(createRenderCounter("Viewer", "render", 40));
1795
+ const setStateBurstRef = useRef3(
1796
+ createBurstMonitor("Viewer", "setDocumentState", 12, 800)
1797
+ );
1798
+ const viewableBurstRef = useRef3(
1799
+ createBurstMonitor("Viewer", "onViewableItemsChanged", 12, 800)
1800
+ );
1801
+ const scrollMonitorRef = useRef3(createScrollPerfMonitor("Viewer"));
1802
+ const dimensionsCacheRef = useRef3(/* @__PURE__ */ new Map());
1803
+ const dimensionsPendingRef = useRef3(/* @__PURE__ */ new Set());
1804
+ const layoutRefreshTimeoutRef = useRef3(
1805
+ null
1806
+ );
1807
+ const pendingScrollIndexRef = useRef3(null);
1808
+ const pendingScrollAttemptsRef = useRef3(0);
1809
+ const pendingScrollTimeoutRef = useRef3(
1810
+ null
1811
+ );
1812
+ const [layoutRevision, setLayoutRevision] = useState2(0);
1813
+ renderCounterRef.current({
1814
+ pageCount,
1815
+ currentPage,
1816
+ viewMode,
1817
+ renderTargetType
1818
+ });
1819
+ const pages = useMemo3(
1820
+ () => Array.from({ length: pageCount }).map((_, i) => i),
1821
+ [pageCount]
1822
+ );
1399
1823
  const rows = useMemo3(() => {
1400
1824
  if (!isDouble) return [];
1401
1825
  const result = [];
@@ -1404,48 +1828,354 @@ var Viewer = ({ engine }) => {
1404
1828
  }
1405
1829
  return result;
1406
1830
  }, [isDouble, pageCount]);
1831
+ const scheduleLayoutRefresh = useCallback2(() => {
1832
+ if (layoutRefreshTimeoutRef.current) return;
1833
+ layoutRefreshTimeoutRef.current = setTimeout(() => {
1834
+ layoutRefreshTimeoutRef.current = null;
1835
+ setLayoutRevision((value) => value + 1);
1836
+ }, 120);
1837
+ }, []);
1838
+ const clearPendingScrollRetry = useCallback2(() => {
1839
+ if (pendingScrollTimeoutRef.current) {
1840
+ clearTimeout(pendingScrollTimeoutRef.current);
1841
+ pendingScrollTimeoutRef.current = null;
1842
+ }
1843
+ }, []);
1844
+ const clearPendingScrollTarget = useCallback2(() => {
1845
+ pendingScrollIndexRef.current = null;
1846
+ pendingScrollAttemptsRef.current = 0;
1847
+ clearPendingScrollRetry();
1848
+ }, [clearPendingScrollRetry]);
1849
+ const scheduleScrollRetry = useCallback2(
1850
+ (reason) => {
1851
+ const pendingIndex = pendingScrollIndexRef.current;
1852
+ if (pendingIndex === null) return;
1853
+ if (pendingScrollAttemptsRef.current >= SCROLL_MAX_RETRIES) {
1854
+ if (perfEnabled) {
1855
+ logPerfEvent("Viewer", "scroll.retry.giveup", {
1856
+ reason,
1857
+ targetIndex: pendingIndex,
1858
+ attempts: pendingScrollAttemptsRef.current
1859
+ });
1860
+ }
1861
+ clearPendingScrollTarget();
1862
+ return;
1863
+ }
1864
+ clearPendingScrollRetry();
1865
+ pendingScrollTimeoutRef.current = setTimeout(() => {
1866
+ pendingScrollTimeoutRef.current = null;
1867
+ const targetIndex = pendingScrollIndexRef.current;
1868
+ if (targetIndex === null) return;
1869
+ pendingScrollAttemptsRef.current += 1;
1870
+ listRef.current?.scrollToIndex({
1871
+ index: targetIndex,
1872
+ animated: false,
1873
+ viewPosition: 0
1874
+ });
1875
+ if (perfEnabled) {
1876
+ logPerfEvent("Viewer", "scroll.retry", {
1877
+ reason,
1878
+ targetIndex,
1879
+ attempt: pendingScrollAttemptsRef.current
1880
+ });
1881
+ }
1882
+ }, SCROLL_RETRY_DELAY_MS);
1883
+ },
1884
+ [clearPendingScrollRetry, clearPendingScrollTarget, perfEnabled]
1885
+ );
1886
+ useEffect3(
1887
+ () => () => {
1888
+ if (layoutRefreshTimeoutRef.current) {
1889
+ clearTimeout(layoutRefreshTimeoutRef.current);
1890
+ }
1891
+ clearPendingScrollRetry();
1892
+ },
1893
+ [clearPendingScrollRetry]
1894
+ );
1895
+ const ensurePageDimensions = useCallback2(
1896
+ (pageIndex) => {
1897
+ if (pageIndex < 0 || pageIndex >= pageCount) return;
1898
+ if (dimensionsCacheRef.current.has(pageIndex)) return;
1899
+ if (dimensionsPendingRef.current.has(pageIndex)) return;
1900
+ dimensionsPendingRef.current.add(pageIndex);
1901
+ void engine.getPageDimensions(pageIndex).then((dims) => {
1902
+ if (dims.width <= 0 || dims.height <= 0) return;
1903
+ const previous = dimensionsCacheRef.current.get(pageIndex);
1904
+ if (previous && previous.width === dims.width && previous.height === dims.height) {
1905
+ return;
1906
+ }
1907
+ dimensionsCacheRef.current.set(pageIndex, {
1908
+ width: dims.width,
1909
+ height: dims.height
1910
+ });
1911
+ scheduleLayoutRefresh();
1912
+ }).catch((error) => {
1913
+ if (!perfEnabled) return;
1914
+ logPerfEvent("Viewer", "pageDimensions.error", {
1915
+ page: pageIndex + 1,
1916
+ message: error instanceof Error ? error.message : String(error)
1917
+ });
1918
+ }).finally(() => {
1919
+ dimensionsPendingRef.current.delete(pageIndex);
1920
+ });
1921
+ },
1922
+ [engine, pageCount, perfEnabled, scheduleLayoutRefresh]
1923
+ );
1924
+ const getPageAspectRatio = useCallback2((pageIndex) => {
1925
+ const dims = dimensionsCacheRef.current.get(pageIndex);
1926
+ if (!dims || dims.width <= 0 || dims.height <= 0) {
1927
+ return DEFAULT_PAGE_ASPECT_RATIO;
1928
+ }
1929
+ return dims.width / dims.height;
1930
+ }, []);
1931
+ useEffect3(() => {
1932
+ if (!perfEnabled) return;
1933
+ logPerfEvent("Viewer", "mount", { viewMode, renderTargetType });
1934
+ sampleMemory("Viewer", "mount", { pageCount });
1935
+ return () => {
1936
+ logPerfEvent("Viewer", "unmount");
1937
+ };
1938
+ }, [perfEnabled]);
1939
+ useEffect3(() => {
1940
+ if (!perfEnabled || readyLoggedRef.current || pageCount <= 0) return;
1941
+ readyLoggedRef.current = true;
1942
+ logPerfEvent("Viewer", "document.ready", {
1943
+ pageCount,
1944
+ initialLoadMs: Math.round((perfNow() - mountedAtRef.current) * 100) / 100
1945
+ });
1946
+ sampleMemory("Viewer", "document.ready", { pageCount });
1947
+ }, [pageCount, perfEnabled]);
1948
+ useEffect3(() => {
1949
+ if (isWebView || isSingle || pageCount <= 0) return;
1950
+ const warmupCount = Math.min(pageCount, 12);
1951
+ for (let i = 0; i < warmupCount; i += 1) {
1952
+ ensurePageDimensions(i);
1953
+ }
1954
+ }, [ensurePageDimensions, isSingle, isWebView, pageCount]);
1955
+ const setDocumentStateTracked = useCallback2(
1956
+ (state, reason) => {
1957
+ if (perfEnabled) {
1958
+ setStateBurstRef.current({
1959
+ reason,
1960
+ keys: Object.keys(state).join(",")
1961
+ });
1962
+ }
1963
+ setDocumentState(state);
1964
+ },
1965
+ [perfEnabled, setDocumentState]
1966
+ );
1967
+ const columnGap = 12;
1968
+ const horizontalPadding = 16;
1969
+ const columnWidth = isDouble ? (windowWidth - horizontalPadding * 2 - columnGap) / 2 : windowWidth;
1970
+ const listLayoutMetrics = useMemo3(() => {
1971
+ const offsets = [];
1972
+ const lengths = [];
1973
+ let offset = LIST_TOP_PADDING;
1974
+ if (isDouble) {
1975
+ const safeZoom2 = Math.max(zoom, 0.25);
1976
+ const pageWidth2 = columnWidth * 0.92 * safeZoom2;
1977
+ const estimatedLength2 = pageWidth2 / DEFAULT_PAGE_ASPECT_RATIO + DOUBLE_PAGE_SPACING;
1978
+ for (let i = 0; i < rows.length; i += 1) {
1979
+ const row = rows[i];
1980
+ const leftRatio = getPageAspectRatio(row.left);
1981
+ const rightRatio = row.right === null ? leftRatio : getPageAspectRatio(row.right);
1982
+ const leftLength = pageWidth2 / leftRatio + DOUBLE_PAGE_SPACING;
1983
+ const rightLength = pageWidth2 / rightRatio + DOUBLE_PAGE_SPACING;
1984
+ const rowLength = Math.max(leftLength, rightLength);
1985
+ offsets.push(offset);
1986
+ lengths.push(rowLength);
1987
+ offset += rowLength;
1988
+ }
1989
+ return { offsets, lengths, estimatedLength: estimatedLength2 };
1990
+ }
1991
+ const safeZoom = Math.max(zoom, 0.25);
1992
+ const pageWidth = windowWidth * 0.92 * safeZoom;
1993
+ const estimatedLength = pageWidth / DEFAULT_PAGE_ASPECT_RATIO + CONTINUOUS_PAGE_SPACING;
1994
+ for (let i = 0; i < pageCount; i += 1) {
1995
+ const ratio = getPageAspectRatio(i);
1996
+ const length = pageWidth / ratio + CONTINUOUS_PAGE_SPACING;
1997
+ offsets.push(offset);
1998
+ lengths.push(length);
1999
+ offset += length;
2000
+ }
2001
+ return { offsets, lengths, estimatedLength };
2002
+ }, [
2003
+ columnWidth,
2004
+ getPageAspectRatio,
2005
+ isDouble,
2006
+ layoutRevision,
2007
+ pageCount,
2008
+ rows,
2009
+ windowWidth,
2010
+ zoom
2011
+ ]);
2012
+ const getFallbackOffsetForIndex = useCallback2(
2013
+ (index) => {
2014
+ if (listLayoutMetrics.lengths.length === 0) {
2015
+ return LIST_TOP_PADDING;
2016
+ }
2017
+ const safeIndex = Math.max(
2018
+ 0,
2019
+ Math.min(index, listLayoutMetrics.lengths.length - 1)
2020
+ );
2021
+ const cachedOffset = listLayoutMetrics.offsets[safeIndex];
2022
+ if (typeof cachedOffset === "number") return cachedOffset;
2023
+ return LIST_TOP_PADDING + listLayoutMetrics.estimatedLength * safeIndex;
2024
+ },
2025
+ [listLayoutMetrics]
2026
+ );
2027
+ const getItemLayout = useCallback2(
2028
+ (_, index) => {
2029
+ if (listLayoutMetrics.lengths.length === 0) {
2030
+ return {
2031
+ index,
2032
+ length: listLayoutMetrics.estimatedLength,
2033
+ offset: LIST_TOP_PADDING
2034
+ };
2035
+ }
2036
+ const safeIndex = Math.max(
2037
+ 0,
2038
+ Math.min(index, listLayoutMetrics.lengths.length - 1)
2039
+ );
2040
+ if (isDouble) {
2041
+ const row = rows[safeIndex];
2042
+ if (row) {
2043
+ ensurePageDimensions(row.left);
2044
+ if (row.right !== null) {
2045
+ ensurePageDimensions(row.right);
2046
+ }
2047
+ }
2048
+ } else {
2049
+ ensurePageDimensions(safeIndex);
2050
+ }
2051
+ const cachedLength = listLayoutMetrics.lengths[safeIndex];
2052
+ const cachedOffset = listLayoutMetrics.offsets[safeIndex];
2053
+ return {
2054
+ index,
2055
+ length: typeof cachedLength === "number" ? cachedLength : listLayoutMetrics.estimatedLength,
2056
+ offset: typeof cachedOffset === "number" ? cachedOffset : LIST_TOP_PADDING + listLayoutMetrics.estimatedLength * safeIndex
2057
+ };
2058
+ },
2059
+ [ensurePageDimensions, isDouble, listLayoutMetrics, rows]
2060
+ );
1407
2061
  useEffect3(() => {
1408
2062
  if (isWebView) {
2063
+ clearPendingScrollTarget();
1409
2064
  if (scrollToPageSignal === null) return;
1410
2065
  if (pageCount === 0) return;
1411
2066
  if (scrollToPageSignal < 0 || scrollToPageSignal >= pageCount) return;
1412
2067
  const nextPage = scrollToPageSignal + 1;
1413
2068
  engine.goToPage(nextPage);
1414
- setDocumentState({ currentPage: nextPage, scrollToPageSignal: null });
2069
+ setDocumentStateTracked(
2070
+ { currentPage: nextPage, scrollToPageSignal: null },
2071
+ "scrollToPageSignal.webview"
2072
+ );
1415
2073
  return;
1416
2074
  }
1417
2075
  if (scrollToPageSignal === null) return;
1418
2076
  if (pageCount === 0) return;
1419
2077
  if (scrollToPageSignal < 0 || scrollToPageSignal >= pageCount) return;
1420
2078
  if (isSingle) {
1421
- setDocumentState({ currentPage: scrollToPageSignal + 1, scrollToPageSignal: null });
2079
+ clearPendingScrollTarget();
2080
+ setDocumentStateTracked(
2081
+ { currentPage: scrollToPageSignal + 1, scrollToPageSignal: null },
2082
+ "scrollToPageSignal.single"
2083
+ );
1422
2084
  return;
1423
2085
  }
2086
+ ensurePageDimensions(scrollToPageSignal);
2087
+ if (isDouble) {
2088
+ ensurePageDimensions(scrollToPageSignal - 1);
2089
+ ensurePageDimensions(scrollToPageSignal + 1);
2090
+ }
1424
2091
  const targetIndex = isDouble ? Math.floor(scrollToPageSignal / 2) : scrollToPageSignal;
1425
- listRef.current?.scrollToIndex({ index: targetIndex, animated: true });
1426
- setDocumentState({ scrollToPageSignal: null });
1427
- }, [scrollToPageSignal, pageCount, setDocumentState, isDouble, isSingle, isWebView, engine]);
1428
- const onViewableItemsChanged = useCallback(
2092
+ pendingScrollIndexRef.current = targetIndex;
2093
+ pendingScrollAttemptsRef.current = 0;
2094
+ clearPendingScrollRetry();
2095
+ setDocumentStateTracked(
2096
+ {
2097
+ currentPage: scrollToPageSignal + 1,
2098
+ scrollToPageSignal: null
2099
+ },
2100
+ "scrollToPageSignal.flatList"
2101
+ );
2102
+ listRef.current?.scrollToIndex({
2103
+ index: targetIndex,
2104
+ animated: true,
2105
+ viewPosition: 0
2106
+ });
2107
+ }, [
2108
+ clearPendingScrollRetry,
2109
+ clearPendingScrollTarget,
2110
+ ensurePageDimensions,
2111
+ scrollToPageSignal,
2112
+ pageCount,
2113
+ setDocumentStateTracked,
2114
+ isDouble,
2115
+ isSingle,
2116
+ isWebView,
2117
+ engine
2118
+ ]);
2119
+ const onViewableItemsChanged = useCallback2(
1429
2120
  ({ viewableItems }) => {
2121
+ if (perfEnabled) {
2122
+ viewableBurstRef.current({
2123
+ viewableCount: viewableItems.length,
2124
+ mode: isDouble ? "double" : "continuous"
2125
+ });
2126
+ }
2127
+ const pendingIndex = pendingScrollIndexRef.current;
2128
+ if (pendingIndex !== null) {
2129
+ const reachedTarget = viewableItems.some((token) => {
2130
+ if (isDouble) {
2131
+ const row = token.item;
2132
+ if (!row) return false;
2133
+ if (row.left === pendingIndex) return true;
2134
+ return row.right === pendingIndex;
2135
+ }
2136
+ return token.index === pendingIndex;
2137
+ });
2138
+ if (reachedTarget) {
2139
+ if (perfEnabled) {
2140
+ logPerfEvent("Viewer", "scroll.retry.resolved", {
2141
+ targetIndex: pendingIndex,
2142
+ attempts: pendingScrollAttemptsRef.current
2143
+ });
2144
+ }
2145
+ clearPendingScrollTarget();
2146
+ }
2147
+ }
1430
2148
  const first = viewableItems[0];
1431
2149
  if (!first) return;
1432
2150
  if (isDouble) {
1433
2151
  const item = first.item;
1434
2152
  if (!item) return;
2153
+ ensurePageDimensions(item.left);
2154
+ if (item.right !== null) {
2155
+ ensurePageDimensions(item.right);
2156
+ }
1435
2157
  const page = item.left + 1;
1436
2158
  if (page !== currentPage) {
1437
- setDocumentState({ currentPage: page });
2159
+ setDocumentStateTracked({ currentPage: page }, "viewable.double");
1438
2160
  }
1439
2161
  return;
1440
2162
  }
1441
2163
  if (first.index !== null && first.index !== void 0) {
2164
+ ensurePageDimensions(first.index);
1442
2165
  const page = first.index + 1;
1443
2166
  if (page !== currentPage) {
1444
- setDocumentState({ currentPage: page });
2167
+ setDocumentStateTracked({ currentPage: page }, "viewable.continuous");
1445
2168
  }
1446
2169
  }
1447
2170
  },
1448
- [currentPage, isDouble, setDocumentState]
2171
+ [
2172
+ clearPendingScrollTarget,
2173
+ currentPage,
2174
+ ensurePageDimensions,
2175
+ isDouble,
2176
+ perfEnabled,
2177
+ setDocumentStateTracked
2178
+ ]
1449
2179
  );
1450
2180
  if (isWebView) {
1451
2181
  return /* @__PURE__ */ jsx3(View3, { style: [styles3.container, isDark && styles3.containerDark], children: /* @__PURE__ */ jsx3(WebViewViewer_default, { engine }) });
@@ -1457,50 +2187,137 @@ var Viewer = ({ engine }) => {
1457
2187
  contentContainerStyle: styles3.singleContent,
1458
2188
  showsVerticalScrollIndicator: false,
1459
2189
  scrollEnabled: true,
1460
- children: /* @__PURE__ */ jsx3(PageRenderer_default, { engine, pageIndex: Math.max(0, currentPage - 1), spacing: 32 })
2190
+ onScroll: perfEnabled ? (event) => {
2191
+ const timestampValue = event.nativeEvent.timestamp;
2192
+ const timestamp = typeof timestampValue === "number" ? timestampValue : void 0;
2193
+ scrollMonitorRef.current.track(timestamp);
2194
+ } : void 0,
2195
+ onScrollBeginDrag: perfEnabled ? () => {
2196
+ scrollMonitorRef.current.begin("single.beginDrag");
2197
+ } : void 0,
2198
+ onMomentumScrollBegin: perfEnabled ? () => {
2199
+ scrollMonitorRef.current.begin("single.momentumBegin");
2200
+ } : void 0,
2201
+ onScrollEndDrag: perfEnabled ? () => {
2202
+ scrollMonitorRef.current.end("single.endDrag");
2203
+ sampleMemory("Viewer", "single.endDrag", { pageCount });
2204
+ } : void 0,
2205
+ onMomentumScrollEnd: perfEnabled ? () => {
2206
+ scrollMonitorRef.current.end("single.momentumEnd");
2207
+ sampleMemory("Viewer", "single.momentumEnd", { pageCount });
2208
+ } : void 0,
2209
+ scrollEventThrottle: perfEnabled ? 16 : void 0,
2210
+ children: /* @__PURE__ */ jsx3(
2211
+ PageRenderer_default,
2212
+ {
2213
+ engine,
2214
+ pageIndex: Math.max(0, currentPage - 1),
2215
+ spacing: 32
2216
+ }
2217
+ )
1461
2218
  }
1462
2219
  ) });
1463
2220
  }
1464
- const columnGap = 12;
1465
- const horizontalPadding = 16;
1466
- const columnWidth = isDouble ? (windowWidth - horizontalPadding * 2 - columnGap) / 2 : windowWidth;
1467
2221
  return /* @__PURE__ */ jsx3(View3, { style: [styles3.container, isDark && styles3.containerDark], children: /* @__PURE__ */ jsx3(
1468
2222
  FlatList,
1469
2223
  {
1470
2224
  ref: listRef,
1471
2225
  data: isDouble ? rows : pages,
2226
+ initialNumToRender: FLATLIST_INITIAL_NUM_TO_RENDER,
2227
+ windowSize: FLATLIST_WINDOW_SIZE,
2228
+ maxToRenderPerBatch: FLATLIST_MAX_TO_RENDER_PER_BATCH,
2229
+ updateCellsBatchingPeriod: FLATLIST_UPDATE_CELLS_BATCHING_PERIOD,
2230
+ removeClippedSubviews: true,
2231
+ getItemLayout,
1472
2232
  keyExtractor: (item) => isDouble ? `row-${item.left}` : `page-${item}`,
1473
2233
  contentContainerStyle: styles3.listContent,
1474
- renderItem: ({ item }) => isDouble ? /* @__PURE__ */ jsxs3(View3, { style: [styles3.row, { paddingHorizontal: horizontalPadding }], children: [
1475
- /* @__PURE__ */ jsx3(View3, { style: { width: columnWidth }, children: /* @__PURE__ */ jsx3(
1476
- PageRenderer_default,
1477
- {
1478
- engine,
1479
- pageIndex: item.left,
1480
- availableWidth: columnWidth,
1481
- horizontalPadding: 8,
1482
- spacing: 20
1483
- }
1484
- ) }),
1485
- item.right !== null ? /* @__PURE__ */ jsx3(View3, { style: { width: columnWidth }, children: /* @__PURE__ */ jsx3(
1486
- PageRenderer_default,
1487
- {
1488
- engine,
1489
- pageIndex: item.right,
1490
- availableWidth: columnWidth,
1491
- horizontalPadding: 8,
1492
- spacing: 20
1493
- }
1494
- ) }) : /* @__PURE__ */ jsx3(View3, { style: { width: columnWidth } })
1495
- ] }) : /* @__PURE__ */ jsx3(PageRenderer_default, { engine, pageIndex: item, spacing: 28 }),
2234
+ renderItem: ({ item }) => isDouble ? /* @__PURE__ */ jsxs3(
2235
+ View3,
2236
+ {
2237
+ style: [styles3.row, { paddingHorizontal: horizontalPadding }],
2238
+ children: [
2239
+ /* @__PURE__ */ jsx3(View3, { style: { width: columnWidth }, children: /* @__PURE__ */ jsx3(
2240
+ PageRenderer_default,
2241
+ {
2242
+ engine,
2243
+ pageIndex: item.left,
2244
+ availableWidth: columnWidth,
2245
+ horizontalPadding: 8,
2246
+ spacing: DOUBLE_PAGE_SPACING
2247
+ }
2248
+ ) }),
2249
+ item.right !== null ? /* @__PURE__ */ jsx3(View3, { style: { width: columnWidth }, children: /* @__PURE__ */ jsx3(
2250
+ PageRenderer_default,
2251
+ {
2252
+ engine,
2253
+ pageIndex: item.right,
2254
+ availableWidth: columnWidth,
2255
+ horizontalPadding: 8,
2256
+ spacing: DOUBLE_PAGE_SPACING
2257
+ }
2258
+ ) }) : /* @__PURE__ */ jsx3(View3, { style: { width: columnWidth } })
2259
+ ]
2260
+ }
2261
+ ) : /* @__PURE__ */ jsx3(
2262
+ PageRenderer_default,
2263
+ {
2264
+ engine,
2265
+ pageIndex: item,
2266
+ spacing: CONTINUOUS_PAGE_SPACING
2267
+ }
2268
+ ),
1496
2269
  onViewableItemsChanged,
1497
2270
  viewabilityConfig: { itemVisiblePercentThreshold: 60 },
1498
2271
  scrollEnabled: true,
1499
2272
  onScrollToIndexFailed: ({ index, averageItemLength }) => {
1500
- if (index < 0 || index >= pageCount) return;
1501
- const offset = Math.max(0, averageItemLength * index);
1502
- listRef.current?.scrollToOffset({ offset, animated: true });
2273
+ const dataLength = isDouble ? rows.length : pages.length;
2274
+ if (index < 0 || index >= dataLength) return;
2275
+ pendingScrollIndexRef.current = index;
2276
+ const offset = Math.max(0, getFallbackOffsetForIndex(index));
2277
+ listRef.current?.scrollToOffset({ offset, animated: false });
2278
+ if (!isDouble) {
2279
+ ensurePageDimensions(index);
2280
+ } else {
2281
+ const row = rows[index];
2282
+ if (row) {
2283
+ ensurePageDimensions(row.left);
2284
+ if (row.right !== null) {
2285
+ ensurePageDimensions(row.right);
2286
+ }
2287
+ }
2288
+ }
2289
+ scheduleScrollRetry("onScrollToIndexFailed");
2290
+ if (perfEnabled) {
2291
+ logPerfEvent("Viewer", "scrollToIndexFailed", {
2292
+ index,
2293
+ averageItemLength,
2294
+ fallbackOffset: offset,
2295
+ fallbackSource: "cached-item-layout",
2296
+ itemCount: dataLength,
2297
+ retryAttempt: pendingScrollAttemptsRef.current
2298
+ });
2299
+ }
1503
2300
  },
2301
+ onScroll: perfEnabled ? (event) => {
2302
+ const timestampValue = event.nativeEvent.timestamp;
2303
+ const timestamp = typeof timestampValue === "number" ? timestampValue : void 0;
2304
+ scrollMonitorRef.current.track(timestamp);
2305
+ } : void 0,
2306
+ onScrollBeginDrag: perfEnabled ? () => {
2307
+ scrollMonitorRef.current.begin("continuous.beginDrag");
2308
+ } : void 0,
2309
+ onMomentumScrollBegin: perfEnabled ? () => {
2310
+ scrollMonitorRef.current.begin("continuous.momentumBegin");
2311
+ } : void 0,
2312
+ onScrollEndDrag: perfEnabled ? () => {
2313
+ scrollMonitorRef.current.end("continuous.endDrag");
2314
+ sampleMemory("Viewer", "continuous.endDrag", { pageCount });
2315
+ } : void 0,
2316
+ onMomentumScrollEnd: perfEnabled ? () => {
2317
+ scrollMonitorRef.current.end("continuous.momentumEnd");
2318
+ sampleMemory("Viewer", "continuous.momentumEnd", { pageCount });
2319
+ } : void 0,
2320
+ scrollEventThrottle: perfEnabled ? 16 : void 0,
1504
2321
  showsVerticalScrollIndicator: false
1505
2322
  }
1506
2323
  ) });
@@ -1514,8 +2331,8 @@ var styles3 = StyleSheet3.create({
1514
2331
  backgroundColor: "#0f1115"
1515
2332
  },
1516
2333
  listContent: {
1517
- paddingTop: 18,
1518
- paddingBottom: 120
2334
+ paddingTop: LIST_TOP_PADDING,
2335
+ paddingBottom: LIST_BOTTOM_PADDING
1519
2336
  },
1520
2337
  singleContent: {
1521
2338
  paddingTop: 18,
@@ -1529,7 +2346,7 @@ var styles3 = StyleSheet3.create({
1529
2346
  var Viewer_default = Viewer;
1530
2347
 
1531
2348
  // components/Topbar.tsx
1532
- import { useEffect as useEffect4, useState as useState2 } from "react";
2349
+ import { useEffect as useEffect4, useState as useState3 } from "react";
1533
2350
  import { View as View4, Text, Pressable as Pressable2, StyleSheet as StyleSheet4 } from "react-native";
1534
2351
  import { useViewerStore as useViewerStore4 } from "@papyrus-sdk/core";
1535
2352
 
@@ -1631,7 +2448,7 @@ var Topbar = ({ engine, onOpenSettings }) => {
1631
2448
  triggerScrollToPage,
1632
2449
  accentColor
1633
2450
  } = useViewerStore4();
1634
- const [pageLabel, setPageLabel] = useState2(`${currentPage}`);
2451
+ const [pageLabel, setPageLabel] = useState3(`${currentPage}`);
1635
2452
  const isDark = uiTheme === "dark";
1636
2453
  const navIconColor = isDark ? "#e5e7eb" : "#111827";
1637
2454
  useEffect4(() => {
@@ -1983,7 +2800,13 @@ var styles5 = StyleSheet5.create({
1983
2800
  var ToolDock_default = ToolDock;
1984
2801
 
1985
2802
  // components/RightSheet.tsx
1986
- import { useEffect as useEffect5, useMemo as useMemo4, useRef as useRef4, useState as useState3 } from "react";
2803
+ import {
2804
+ useCallback as useCallback3,
2805
+ useEffect as useEffect5,
2806
+ useMemo as useMemo4,
2807
+ useRef as useRef4,
2808
+ useState as useState4
2809
+ } from "react";
1987
2810
  import {
1988
2811
  Modal,
1989
2812
  View as View6,
@@ -2025,7 +2848,7 @@ var PageThumbnail = ({
2025
2848
  onPress
2026
2849
  }) => {
2027
2850
  const viewRef = useRef4(null);
2028
- const [layoutReady, setLayoutReady] = useState3(false);
2851
+ const [layoutReady, setLayoutReady] = useState4(false);
2029
2852
  useEffect5(() => {
2030
2853
  if (!layoutReady || !useNativePreview) return;
2031
2854
  const viewTag = findNodeHandle2(viewRef.current);
@@ -2051,7 +2874,29 @@ var PageThumbnail = ({
2051
2874
  isActive && { borderColor: accentColor }
2052
2875
  ],
2053
2876
  children: [
2054
- /* @__PURE__ */ jsx7(View6, { onLayout: handleLayout, style: [styles6.thumbFrame, { width: frameWidth, height: frameHeight }], children: useNativePreview ? /* @__PURE__ */ jsx7(PapyrusPageView2, { ref: viewRef, style: styles6.thumbView }) : /* @__PURE__ */ jsx7(View6, { style: [styles6.thumbFallback, isDark && styles6.thumbFallbackDark], children: /* @__PURE__ */ jsx7(Text3, { style: [styles6.thumbFallbackText, isDark && styles6.thumbFallbackTextDark], children: pageIndex + 1 }) }) }),
2877
+ /* @__PURE__ */ jsx7(
2878
+ View6,
2879
+ {
2880
+ onLayout: handleLayout,
2881
+ style: [styles6.thumbFrame, { width: frameWidth, height: frameHeight }],
2882
+ children: useNativePreview ? /* @__PURE__ */ jsx7(PapyrusPageView2, { ref: viewRef, style: styles6.thumbView }) : /* @__PURE__ */ jsx7(
2883
+ View6,
2884
+ {
2885
+ style: [styles6.thumbFallback, isDark && styles6.thumbFallbackDark],
2886
+ children: /* @__PURE__ */ jsx7(
2887
+ Text3,
2888
+ {
2889
+ style: [
2890
+ styles6.thumbFallbackText,
2891
+ isDark && styles6.thumbFallbackTextDark
2892
+ ],
2893
+ children: pageIndex + 1
2894
+ }
2895
+ )
2896
+ }
2897
+ )
2898
+ }
2899
+ ),
2055
2900
  /* @__PURE__ */ jsx7(Text3, { style: [styles6.thumbLabel, isDark && styles6.thumbLabelDark], children: pageIndex + 1 })
2056
2901
  ]
2057
2902
  }
@@ -2118,14 +2963,20 @@ var RightSheet = ({ engine }) => {
2118
2963
  locale,
2119
2964
  accentColor
2120
2965
  } = useViewerStore6();
2121
- const [pagesMode, setPagesMode] = useState3("thumbnails");
2122
- const [query, setQuery] = useState3("");
2123
- const [isSearching, setIsSearching] = useState3(false);
2966
+ const [pagesMode, setPagesMode] = useState4(
2967
+ "thumbnails"
2968
+ );
2969
+ const [query, setQuery] = useState4("");
2970
+ const [isSearching, setIsSearching] = useState4(false);
2124
2971
  const searchService = useMemo4(() => new SearchService(engine), [engine]);
2125
2972
  const isDark = uiTheme === "dark";
2126
2973
  const accentSoft = withAlpha(accentColor, 0.2);
2127
2974
  const accentStrong = withAlpha(accentColor, 0.35);
2128
2975
  const t = getStrings(locale);
2976
+ const perfEnabled = isMobilePerfEnabled();
2977
+ const setStateBurstRef = useRef4(
2978
+ createBurstMonitor("RightSheet", "setDocumentState", 10, 800)
2979
+ );
2129
2980
  const sheetHeight = Math.min(640, Dimensions.get("window").height * 0.72);
2130
2981
  const windowWidth = Dimensions.get("window").width;
2131
2982
  const gridGutter = 12;
@@ -2134,24 +2985,76 @@ var RightSheet = ({ engine }) => {
2134
2985
  const frameWidth = cardWidth - 16;
2135
2986
  const frameHeight = frameWidth * 1.28;
2136
2987
  const renderTarget = engine.getRenderTargetType?.();
2137
- const hasNativePageView = Boolean(UIManager.getViewManagerConfig?.("PapyrusPageView"));
2988
+ const hasNativePageView = Boolean(
2989
+ UIManager.getViewManagerConfig?.("PapyrusPageView")
2990
+ );
2138
2991
  const useNativePreview = renderTarget !== "webview" && hasNativePageView;
2139
2992
  const closeSheet = () => toggleSidebarRight();
2993
+ const setDocumentStateTracked = useCallback3(
2994
+ (state, reason) => {
2995
+ if (perfEnabled) {
2996
+ setStateBurstRef.current({
2997
+ reason,
2998
+ keys: Object.keys(state).join(",")
2999
+ });
3000
+ }
3001
+ setDocumentState(state);
3002
+ },
3003
+ [perfEnabled, setDocumentState]
3004
+ );
3005
+ useEffect5(() => {
3006
+ if (!perfEnabled || !sidebarRightOpen) return;
3007
+ logPerfEvent("RightSheet", "open", {
3008
+ tab: sidebarRightTab,
3009
+ pageCount,
3010
+ currentPage
3011
+ });
3012
+ sampleMemory("RightSheet", "open", {
3013
+ tab: sidebarRightTab,
3014
+ pageCount
3015
+ });
3016
+ }, [perfEnabled, sidebarRightOpen]);
3017
+ useEffect5(() => {
3018
+ if (!perfEnabled || !sidebarRightOpen) return;
3019
+ return () => {
3020
+ logPerfEvent("RightSheet", "close");
3021
+ };
3022
+ }, [perfEnabled, sidebarRightOpen]);
3023
+ useEffect5(() => {
3024
+ if (!perfEnabled || !sidebarRightOpen || sidebarRightTab !== "pages" || pagesMode !== "thumbnails")
3025
+ return;
3026
+ sampleMemory("RightSheet", "thumbnails.open.start", { pageCount });
3027
+ const timeout = setTimeout(() => {
3028
+ sampleMemory("RightSheet", "thumbnails.open.steady", { pageCount });
3029
+ }, 1200);
3030
+ return () => clearTimeout(timeout);
3031
+ }, [pageCount, pagesMode, perfEnabled, sidebarRightOpen, sidebarRightTab]);
2140
3032
  const handleSearch = async () => {
2141
3033
  const trimmed = query.trim();
2142
3034
  if (!trimmed) {
2143
3035
  setSearch("", []);
2144
3036
  return;
2145
3037
  }
3038
+ const startedAt = perfEnabled ? perfNow() : 0;
2146
3039
  setIsSearching(true);
2147
3040
  try {
2148
3041
  const results = await searchService.search(trimmed);
2149
3042
  setSearch(trimmed, results);
3043
+ if (perfEnabled) {
3044
+ logPerfEvent("RightSheet", "search.completed", {
3045
+ queryLength: trimmed.length,
3046
+ results: results.length,
3047
+ durationMs: Math.round((perfNow() - startedAt) * 100) / 100
3048
+ });
3049
+ }
2150
3050
  } finally {
2151
3051
  setIsSearching(false);
2152
3052
  }
2153
3053
  };
2154
- const pages = useMemo4(() => Array.from({ length: pageCount }, (_, i) => i), [pageCount]);
3054
+ const pages = useMemo4(
3055
+ () => Array.from({ length: pageCount }, (_, i) => i),
3056
+ [pageCount]
3057
+ );
2155
3058
  const renderHighlightedSnippet = (text, isActive) => {
2156
3059
  const trimmedQuery = searchQuery.trim();
2157
3060
  if (trimmedQuery.length < 2) {
@@ -2185,7 +3088,10 @@ var RightSheet = ({ engine }) => {
2185
3088
  if (index > cursor) {
2186
3089
  parts.push({ text: text.slice(cursor, index), match: false });
2187
3090
  }
2188
- parts.push({ text: text.slice(index, index + trimmedQuery.length), match: true });
3091
+ parts.push({
3092
+ text: text.slice(index, index + trimmedQuery.length),
3093
+ match: true
3094
+ });
2189
3095
  cursor = index + trimmedQuery.length;
2190
3096
  }
2191
3097
  return /* @__PURE__ */ jsx7(
@@ -2204,7 +3110,10 @@ var RightSheet = ({ engine }) => {
2204
3110
  part.match && styles6.matchText,
2205
3111
  part.match && isDark && styles6.matchTextDark,
2206
3112
  part.match && isActive && styles6.matchTextActive,
2207
- part.match && isActive && { backgroundColor: accentStrong, color: accentColor }
3113
+ part.match && isActive && {
3114
+ backgroundColor: accentStrong,
3115
+ color: accentColor
3116
+ }
2208
3117
  ],
2209
3118
  children: part.text
2210
3119
  },
@@ -2214,247 +3123,419 @@ var RightSheet = ({ engine }) => {
2214
3123
  );
2215
3124
  };
2216
3125
  if (!sidebarRightOpen) return null;
2217
- return /* @__PURE__ */ jsx7(Modal, { visible: true, transparent: true, animationType: "slide", onRequestClose: closeSheet, children: /* @__PURE__ */ jsxs7(View6, { style: styles6.modalRoot, children: [
2218
- /* @__PURE__ */ jsx7(Pressable4, { style: styles6.backdrop, onPress: closeSheet }),
2219
- /* @__PURE__ */ jsxs7(View6, { style: [styles6.sheet, { height: sheetHeight }, isDark && styles6.sheetDark], children: [
2220
- /* @__PURE__ */ jsx7(View6, { style: [styles6.handle, isDark && styles6.handleDark] }),
2221
- /* @__PURE__ */ jsx7(View6, { style: styles6.tabs, children: ["pages", "search", "annotations"].map((tab) => /* @__PURE__ */ jsx7(
2222
- Pressable4,
2223
- {
2224
- onPress: () => toggleSidebarRight(tab),
2225
- style: [
2226
- styles6.tabButton,
2227
- isDark && styles6.tabButtonDark,
2228
- sidebarRightTab === tab && styles6.tabButtonActive,
2229
- sidebarRightTab === tab && { backgroundColor: accentColor }
2230
- ],
2231
- children: /* @__PURE__ */ jsx7(
2232
- Text3,
2233
- {
2234
- style: [
2235
- styles6.tabText,
2236
- isDark && styles6.tabTextDark,
2237
- sidebarRightTab === tab && styles6.tabTextActive
2238
- ],
2239
- children: tab === "pages" ? t.pages : tab === "search" ? t.search : t.notes
2240
- }
2241
- )
2242
- },
2243
- tab
2244
- )) }),
2245
- sidebarRightTab === "pages" ? /* @__PURE__ */ jsxs7(View6, { style: styles6.pagesContent, children: [
2246
- /* @__PURE__ */ jsxs7(View6, { style: styles6.pageHeader, children: [
2247
- /* @__PURE__ */ jsxs7(Text3, { style: [styles6.pageStatus, isDark && styles6.pageStatusDark], children: [
2248
- t.page,
2249
- " ",
2250
- currentPage,
2251
- " / ",
2252
- pageCount
2253
- ] }),
2254
- /* @__PURE__ */ jsxs7(View6, { style: [styles6.segmented, isDark && styles6.segmentedDark], children: [
2255
- /* @__PURE__ */ jsx7(
2256
- Pressable4,
2257
- {
2258
- onPress: () => setPagesMode("thumbnails"),
2259
- style: [
2260
- styles6.segmentButton,
2261
- pagesMode === "thumbnails" && styles6.segmentButtonActive,
2262
- pagesMode === "thumbnails" && { backgroundColor: accentColor }
2263
- ],
2264
- children: /* @__PURE__ */ jsx7(
2265
- Text3,
3126
+ return /* @__PURE__ */ jsx7(
3127
+ Modal,
3128
+ {
3129
+ visible: true,
3130
+ transparent: true,
3131
+ animationType: "slide",
3132
+ onRequestClose: closeSheet,
3133
+ children: /* @__PURE__ */ jsxs7(View6, { style: styles6.modalRoot, children: [
3134
+ /* @__PURE__ */ jsx7(Pressable4, { style: styles6.backdrop, onPress: closeSheet }),
3135
+ /* @__PURE__ */ jsxs7(
3136
+ View6,
3137
+ {
3138
+ style: [
3139
+ styles6.sheet,
3140
+ { height: sheetHeight },
3141
+ isDark && styles6.sheetDark
3142
+ ],
3143
+ children: [
3144
+ /* @__PURE__ */ jsx7(View6, { style: [styles6.handle, isDark && styles6.handleDark] }),
3145
+ /* @__PURE__ */ jsx7(View6, { style: styles6.tabs, children: ["pages", "search", "annotations"].map((tab) => /* @__PURE__ */ jsx7(
3146
+ Pressable4,
3147
+ {
3148
+ onPress: () => toggleSidebarRight(tab),
3149
+ style: [
3150
+ styles6.tabButton,
3151
+ isDark && styles6.tabButtonDark,
3152
+ sidebarRightTab === tab && styles6.tabButtonActive,
3153
+ sidebarRightTab === tab && { backgroundColor: accentColor }
3154
+ ],
3155
+ children: /* @__PURE__ */ jsx7(
3156
+ Text3,
3157
+ {
3158
+ style: [
3159
+ styles6.tabText,
3160
+ isDark && styles6.tabTextDark,
3161
+ sidebarRightTab === tab && styles6.tabTextActive
3162
+ ],
3163
+ children: tab === "pages" ? t.pages : tab === "search" ? t.search : t.notes
3164
+ }
3165
+ )
3166
+ },
3167
+ tab
3168
+ )) }),
3169
+ sidebarRightTab === "pages" ? /* @__PURE__ */ jsxs7(View6, { style: styles6.pagesContent, children: [
3170
+ /* @__PURE__ */ jsxs7(View6, { style: styles6.pageHeader, children: [
3171
+ /* @__PURE__ */ jsxs7(
3172
+ Text3,
3173
+ {
3174
+ style: [styles6.pageStatus, isDark && styles6.pageStatusDark],
3175
+ children: [
3176
+ t.page,
3177
+ " ",
3178
+ currentPage,
3179
+ " / ",
3180
+ pageCount
3181
+ ]
3182
+ }
3183
+ ),
3184
+ /* @__PURE__ */ jsxs7(
3185
+ View6,
3186
+ {
3187
+ style: [styles6.segmented, isDark && styles6.segmentedDark],
3188
+ children: [
3189
+ /* @__PURE__ */ jsx7(
3190
+ Pressable4,
3191
+ {
3192
+ onPress: () => setPagesMode("thumbnails"),
3193
+ style: [
3194
+ styles6.segmentButton,
3195
+ pagesMode === "thumbnails" && styles6.segmentButtonActive,
3196
+ pagesMode === "thumbnails" && {
3197
+ backgroundColor: accentColor
3198
+ }
3199
+ ],
3200
+ children: /* @__PURE__ */ jsx7(
3201
+ Text3,
3202
+ {
3203
+ style: [
3204
+ styles6.segmentText,
3205
+ isDark && styles6.segmentTextDark,
3206
+ pagesMode === "thumbnails" && styles6.segmentTextActive
3207
+ ],
3208
+ children: t.pagesTab
3209
+ }
3210
+ )
3211
+ }
3212
+ ),
3213
+ /* @__PURE__ */ jsx7(
3214
+ Pressable4,
3215
+ {
3216
+ onPress: () => setPagesMode("summary"),
3217
+ style: [
3218
+ styles6.segmentButton,
3219
+ pagesMode === "summary" && styles6.segmentButtonActive,
3220
+ pagesMode === "summary" && {
3221
+ backgroundColor: accentColor
3222
+ }
3223
+ ],
3224
+ children: /* @__PURE__ */ jsx7(
3225
+ Text3,
3226
+ {
3227
+ style: [
3228
+ styles6.segmentText,
3229
+ isDark && styles6.segmentTextDark,
3230
+ pagesMode === "summary" && styles6.segmentTextActive
3231
+ ],
3232
+ children: t.summaryTab
3233
+ }
3234
+ )
3235
+ }
3236
+ )
3237
+ ]
3238
+ }
3239
+ )
3240
+ ] }),
3241
+ pagesMode === "thumbnails" ? /* @__PURE__ */ jsx7(
3242
+ FlatList2,
2266
3243
  {
2267
- style: [
2268
- styles6.segmentText,
2269
- isDark && styles6.segmentTextDark,
2270
- pagesMode === "thumbnails" && styles6.segmentTextActive
2271
- ],
2272
- children: t.pagesTab
3244
+ data: pages,
3245
+ keyExtractor: (item) => `thumb-${item}`,
3246
+ numColumns: 2,
3247
+ contentContainerStyle: styles6.thumbGrid,
3248
+ columnWrapperStyle: styles6.thumbRow,
3249
+ showsVerticalScrollIndicator: false,
3250
+ initialNumToRender: 6,
3251
+ renderItem: ({ item }) => /* @__PURE__ */ jsx7(
3252
+ PageThumbnail,
3253
+ {
3254
+ engine,
3255
+ pageIndex: item,
3256
+ isActive: item + 1 === currentPage,
3257
+ isDark,
3258
+ zoom,
3259
+ cardWidth,
3260
+ frameWidth,
3261
+ frameHeight,
3262
+ accentColor,
3263
+ useNativePreview,
3264
+ onPress: () => {
3265
+ engine.goToPage(item + 1);
3266
+ setDocumentStateTracked(
3267
+ { currentPage: item + 1 },
3268
+ "thumbnail.press"
3269
+ );
3270
+ triggerScrollToPage(item);
3271
+ closeSheet();
3272
+ }
3273
+ }
3274
+ )
2273
3275
  }
2274
- )
2275
- }
2276
- ),
2277
- /* @__PURE__ */ jsx7(
2278
- Pressable4,
2279
- {
2280
- onPress: () => setPagesMode("summary"),
2281
- style: [
2282
- styles6.segmentButton,
2283
- pagesMode === "summary" && styles6.segmentButtonActive,
2284
- pagesMode === "summary" && { backgroundColor: accentColor }
2285
- ],
2286
- children: /* @__PURE__ */ jsx7(
2287
- Text3,
3276
+ ) : /* @__PURE__ */ jsx7(
3277
+ ScrollView3,
2288
3278
  {
2289
- style: [
2290
- styles6.segmentText,
2291
- isDark && styles6.segmentTextDark,
2292
- pagesMode === "summary" && styles6.segmentTextActive
2293
- ],
2294
- children: t.summaryTab
3279
+ contentContainerStyle: styles6.summaryContent,
3280
+ showsVerticalScrollIndicator: false,
3281
+ children: outline.length === 0 ? /* @__PURE__ */ jsx7(
3282
+ Text3,
3283
+ {
3284
+ style: [styles6.emptyText, isDark && styles6.emptyTextDark],
3285
+ children: t.noSummary
3286
+ }
3287
+ ) : outline.map((item, index) => /* @__PURE__ */ jsx7(
3288
+ OutlineNode,
3289
+ {
3290
+ item,
3291
+ isDark,
3292
+ untitledLabel: t.untitled,
3293
+ onSelect: (pageIndex) => {
3294
+ engine.goToPage(pageIndex + 1);
3295
+ setDocumentStateTracked(
3296
+ { currentPage: pageIndex + 1 },
3297
+ "outline.select"
3298
+ );
3299
+ triggerScrollToPage(pageIndex);
3300
+ closeSheet();
3301
+ }
3302
+ },
3303
+ `${item.title}-${index}`
3304
+ ))
2295
3305
  }
2296
3306
  )
2297
- }
2298
- )
2299
- ] })
2300
- ] }),
2301
- pagesMode === "thumbnails" ? /* @__PURE__ */ jsx7(
2302
- FlatList2,
2303
- {
2304
- data: pages,
2305
- keyExtractor: (item) => `thumb-${item}`,
2306
- numColumns: 2,
2307
- contentContainerStyle: styles6.thumbGrid,
2308
- columnWrapperStyle: styles6.thumbRow,
2309
- showsVerticalScrollIndicator: false,
2310
- initialNumToRender: 6,
2311
- renderItem: ({ item }) => /* @__PURE__ */ jsx7(
2312
- PageThumbnail,
2313
- {
2314
- engine,
2315
- pageIndex: item,
2316
- isActive: item + 1 === currentPage,
2317
- isDark,
2318
- zoom,
2319
- cardWidth,
2320
- frameWidth,
2321
- frameHeight,
2322
- accentColor,
2323
- useNativePreview,
2324
- onPress: () => {
2325
- engine.goToPage(item + 1);
2326
- setDocumentState({ currentPage: item + 1 });
2327
- triggerScrollToPage(item);
2328
- closeSheet();
3307
+ ] }) : /* @__PURE__ */ jsx7(
3308
+ ScrollView3,
3309
+ {
3310
+ contentContainerStyle: styles6.content,
3311
+ showsVerticalScrollIndicator: false,
3312
+ children: sidebarRightTab === "search" ? /* @__PURE__ */ jsxs7(View6, { children: [
3313
+ /* @__PURE__ */ jsxs7(
3314
+ View6,
3315
+ {
3316
+ style: [styles6.searchBox, isDark && styles6.searchBoxDark],
3317
+ children: [
3318
+ /* @__PURE__ */ jsx7(
3319
+ TextInput,
3320
+ {
3321
+ value: query,
3322
+ onChangeText: setQuery,
3323
+ placeholder: t.searchPlaceholder,
3324
+ placeholderTextColor: isDark ? "#9ca3af" : "#6b7280",
3325
+ style: [
3326
+ styles6.searchInput,
3327
+ isDark && styles6.searchInputDark
3328
+ ],
3329
+ onSubmitEditing: handleSearch,
3330
+ returnKeyType: "search"
3331
+ }
3332
+ ),
3333
+ /* @__PURE__ */ jsx7(
3334
+ Pressable4,
3335
+ {
3336
+ onPress: handleSearch,
3337
+ style: [
3338
+ styles6.searchButton,
3339
+ { backgroundColor: accentColor }
3340
+ ],
3341
+ children: /* @__PURE__ */ jsx7(Text3, { style: styles6.searchButtonText, children: t.searchGo })
3342
+ }
3343
+ )
3344
+ ]
3345
+ }
3346
+ ),
3347
+ /* @__PURE__ */ jsxs7(View6, { style: styles6.searchMeta, children: [
3348
+ /* @__PURE__ */ jsxs7(
3349
+ Text3,
3350
+ {
3351
+ style: [
3352
+ styles6.searchCount,
3353
+ isDark && styles6.searchCountDark,
3354
+ { color: accentColor }
3355
+ ],
3356
+ children: [
3357
+ searchResults.length,
3358
+ " ",
3359
+ t.results
3360
+ ]
3361
+ }
3362
+ ),
3363
+ /* @__PURE__ */ jsxs7(View6, { style: styles6.searchNav, children: [
3364
+ /* @__PURE__ */ jsx7(
3365
+ Pressable4,
3366
+ {
3367
+ onPress: prevSearchResult,
3368
+ disabled: searchResults.length === 0,
3369
+ style: [
3370
+ styles6.searchNavButton,
3371
+ isDark && styles6.searchNavButtonDark,
3372
+ searchResults.length === 0 && styles6.searchNavButtonDisabled
3373
+ ],
3374
+ children: /* @__PURE__ */ jsx7(
3375
+ IconChevronLeft,
3376
+ {
3377
+ size: 14,
3378
+ color: isDark ? "#e5e7eb" : "#111827"
3379
+ }
3380
+ )
3381
+ }
3382
+ ),
3383
+ /* @__PURE__ */ jsx7(
3384
+ Pressable4,
3385
+ {
3386
+ onPress: nextSearchResult,
3387
+ disabled: searchResults.length === 0,
3388
+ style: [
3389
+ styles6.searchNavButton,
3390
+ isDark && styles6.searchNavButtonDark,
3391
+ searchResults.length === 0 && styles6.searchNavButtonDisabled
3392
+ ],
3393
+ children: /* @__PURE__ */ jsx7(
3394
+ IconChevronRight,
3395
+ {
3396
+ size: 14,
3397
+ color: isDark ? "#e5e7eb" : "#111827"
3398
+ }
3399
+ )
3400
+ }
3401
+ )
3402
+ ] })
3403
+ ] }),
3404
+ isSearching && /* @__PURE__ */ jsxs7(View6, { style: styles6.searchStatus, children: [
3405
+ /* @__PURE__ */ jsx7(ActivityIndicator, { size: "small", color: accentColor }),
3406
+ /* @__PURE__ */ jsx7(
3407
+ Text3,
3408
+ {
3409
+ style: [
3410
+ styles6.searchStatusText,
3411
+ isDark && styles6.searchStatusTextDark
3412
+ ],
3413
+ children: t.searching
3414
+ }
3415
+ )
3416
+ ] }),
3417
+ !isSearching && searchResults.length === 0 && /* @__PURE__ */ jsx7(
3418
+ Text3,
3419
+ {
3420
+ style: [styles6.emptyText, isDark && styles6.emptyTextDark],
3421
+ children: t.noResults
3422
+ }
3423
+ ),
3424
+ !isSearching && searchResults.map((res, idx) => {
3425
+ const isActive = idx === activeSearchIndex;
3426
+ return /* @__PURE__ */ jsxs7(
3427
+ Pressable4,
3428
+ {
3429
+ onPress: () => {
3430
+ setDocumentStateTracked(
3431
+ { activeSearchIndex: idx },
3432
+ "searchResult.select"
3433
+ );
3434
+ triggerScrollToPage(res.pageIndex);
3435
+ closeSheet();
3436
+ },
3437
+ style: [
3438
+ styles6.resultCard,
3439
+ isDark && styles6.resultCardDark,
3440
+ isActive && styles6.resultCardActive,
3441
+ isActive && { borderColor: accentColor }
3442
+ ],
3443
+ children: [
3444
+ /* @__PURE__ */ jsxs7(
3445
+ Text3,
3446
+ {
3447
+ style: [
3448
+ styles6.resultPage,
3449
+ isDark && styles6.resultPageDark,
3450
+ { color: accentColor }
3451
+ ],
3452
+ children: [
3453
+ t.page,
3454
+ " ",
3455
+ res.pageIndex + 1
3456
+ ]
3457
+ }
3458
+ ),
3459
+ renderHighlightedSnippet(res.text, isActive)
3460
+ ]
3461
+ },
3462
+ `${res.pageIndex}-${idx}`
3463
+ );
3464
+ })
3465
+ ] }) : /* @__PURE__ */ jsx7(View6, { children: annotations.length === 0 ? /* @__PURE__ */ jsx7(
3466
+ Text3,
3467
+ {
3468
+ style: [styles6.emptyText, isDark && styles6.emptyTextDark],
3469
+ children: t.noAnnotations
3470
+ }
3471
+ ) : annotations.map((ann) => /* @__PURE__ */ jsxs7(
3472
+ Pressable4,
3473
+ {
3474
+ onPress: () => {
3475
+ setSelectedAnnotation(ann.id);
3476
+ triggerScrollToPage(ann.pageIndex);
3477
+ closeSheet();
3478
+ },
3479
+ style: [styles6.noteCard, isDark && styles6.noteCardDark],
3480
+ children: [
3481
+ /* @__PURE__ */ jsxs7(View6, { style: styles6.noteHeader, children: [
3482
+ /* @__PURE__ */ jsx7(
3483
+ View6,
3484
+ {
3485
+ style: [
3486
+ styles6.noteDot,
3487
+ { backgroundColor: ann.color }
3488
+ ]
3489
+ }
3490
+ ),
3491
+ /* @__PURE__ */ jsxs7(
3492
+ Text3,
3493
+ {
3494
+ style: [
3495
+ styles6.noteTitle,
3496
+ isDark && styles6.noteTitleDark
3497
+ ],
3498
+ children: [
3499
+ t.page,
3500
+ " ",
3501
+ ann.pageIndex + 1
3502
+ ]
3503
+ }
3504
+ )
3505
+ ] }),
3506
+ /* @__PURE__ */ jsx7(
3507
+ Text3,
3508
+ {
3509
+ style: [
3510
+ styles6.noteType,
3511
+ isDark && styles6.noteTypeDark,
3512
+ { color: accentColor }
3513
+ ],
3514
+ children: ann.type === "comment" || ann.type === "text" ? t.note.toUpperCase() : ann.type.toUpperCase()
3515
+ }
3516
+ ),
3517
+ ann.content ? /* @__PURE__ */ jsx7(
3518
+ Text3,
3519
+ {
3520
+ style: [
3521
+ styles6.noteContent,
3522
+ isDark && styles6.noteContentDark
3523
+ ],
3524
+ children: ann.content
3525
+ }
3526
+ ) : null
3527
+ ]
3528
+ },
3529
+ ann.id
3530
+ )) })
2329
3531
  }
2330
- }
2331
- )
3532
+ )
3533
+ ]
2332
3534
  }
2333
- ) : /* @__PURE__ */ jsx7(ScrollView3, { contentContainerStyle: styles6.summaryContent, showsVerticalScrollIndicator: false, children: outline.length === 0 ? /* @__PURE__ */ jsx7(Text3, { style: [styles6.emptyText, isDark && styles6.emptyTextDark], children: t.noSummary }) : outline.map((item, index) => /* @__PURE__ */ jsx7(
2334
- OutlineNode,
2335
- {
2336
- item,
2337
- isDark,
2338
- untitledLabel: t.untitled,
2339
- onSelect: (pageIndex) => {
2340
- engine.goToPage(pageIndex + 1);
2341
- setDocumentState({ currentPage: pageIndex + 1 });
2342
- triggerScrollToPage(pageIndex);
2343
- closeSheet();
2344
- }
2345
- },
2346
- `${item.title}-${index}`
2347
- )) })
2348
- ] }) : /* @__PURE__ */ jsx7(ScrollView3, { contentContainerStyle: styles6.content, showsVerticalScrollIndicator: false, children: sidebarRightTab === "search" ? /* @__PURE__ */ jsxs7(View6, { children: [
2349
- /* @__PURE__ */ jsxs7(View6, { style: [styles6.searchBox, isDark && styles6.searchBoxDark], children: [
2350
- /* @__PURE__ */ jsx7(
2351
- TextInput,
2352
- {
2353
- value: query,
2354
- onChangeText: setQuery,
2355
- placeholder: t.searchPlaceholder,
2356
- placeholderTextColor: isDark ? "#9ca3af" : "#6b7280",
2357
- style: [styles6.searchInput, isDark && styles6.searchInputDark],
2358
- onSubmitEditing: handleSearch,
2359
- returnKeyType: "search"
2360
- }
2361
- ),
2362
- /* @__PURE__ */ jsx7(Pressable4, { onPress: handleSearch, style: [styles6.searchButton, { backgroundColor: accentColor }], children: /* @__PURE__ */ jsx7(Text3, { style: styles6.searchButtonText, children: t.searchGo }) })
2363
- ] }),
2364
- /* @__PURE__ */ jsxs7(View6, { style: styles6.searchMeta, children: [
2365
- /* @__PURE__ */ jsxs7(Text3, { style: [styles6.searchCount, isDark && styles6.searchCountDark, { color: accentColor }], children: [
2366
- searchResults.length,
2367
- " ",
2368
- t.results
2369
- ] }),
2370
- /* @__PURE__ */ jsxs7(View6, { style: styles6.searchNav, children: [
2371
- /* @__PURE__ */ jsx7(
2372
- Pressable4,
2373
- {
2374
- onPress: prevSearchResult,
2375
- disabled: searchResults.length === 0,
2376
- style: [
2377
- styles6.searchNavButton,
2378
- isDark && styles6.searchNavButtonDark,
2379
- searchResults.length === 0 && styles6.searchNavButtonDisabled
2380
- ],
2381
- children: /* @__PURE__ */ jsx7(IconChevronLeft, { size: 14, color: isDark ? "#e5e7eb" : "#111827" })
2382
- }
2383
- ),
2384
- /* @__PURE__ */ jsx7(
2385
- Pressable4,
2386
- {
2387
- onPress: nextSearchResult,
2388
- disabled: searchResults.length === 0,
2389
- style: [
2390
- styles6.searchNavButton,
2391
- isDark && styles6.searchNavButtonDark,
2392
- searchResults.length === 0 && styles6.searchNavButtonDisabled
2393
- ],
2394
- children: /* @__PURE__ */ jsx7(IconChevronRight, { size: 14, color: isDark ? "#e5e7eb" : "#111827" })
2395
- }
2396
- )
2397
- ] })
2398
- ] }),
2399
- isSearching && /* @__PURE__ */ jsxs7(View6, { style: styles6.searchStatus, children: [
2400
- /* @__PURE__ */ jsx7(ActivityIndicator, { size: "small", color: accentColor }),
2401
- /* @__PURE__ */ jsx7(Text3, { style: [styles6.searchStatusText, isDark && styles6.searchStatusTextDark], children: t.searching })
2402
- ] }),
2403
- !isSearching && searchResults.length === 0 && /* @__PURE__ */ jsx7(Text3, { style: [styles6.emptyText, isDark && styles6.emptyTextDark], children: t.noResults }),
2404
- !isSearching && searchResults.map((res, idx) => {
2405
- const isActive = idx === activeSearchIndex;
2406
- return /* @__PURE__ */ jsxs7(
2407
- Pressable4,
2408
- {
2409
- onPress: () => {
2410
- setDocumentState({ activeSearchIndex: idx });
2411
- triggerScrollToPage(res.pageIndex);
2412
- closeSheet();
2413
- },
2414
- style: [
2415
- styles6.resultCard,
2416
- isDark && styles6.resultCardDark,
2417
- isActive && styles6.resultCardActive,
2418
- isActive && { borderColor: accentColor }
2419
- ],
2420
- children: [
2421
- /* @__PURE__ */ jsxs7(Text3, { style: [styles6.resultPage, isDark && styles6.resultPageDark, { color: accentColor }], children: [
2422
- t.page,
2423
- " ",
2424
- res.pageIndex + 1
2425
- ] }),
2426
- renderHighlightedSnippet(res.text, isActive)
2427
- ]
2428
- },
2429
- `${res.pageIndex}-${idx}`
2430
- );
2431
- })
2432
- ] }) : /* @__PURE__ */ jsx7(View6, { children: annotations.length === 0 ? /* @__PURE__ */ jsx7(Text3, { style: [styles6.emptyText, isDark && styles6.emptyTextDark], children: t.noAnnotations }) : annotations.map((ann) => /* @__PURE__ */ jsxs7(
2433
- Pressable4,
2434
- {
2435
- onPress: () => {
2436
- setSelectedAnnotation(ann.id);
2437
- triggerScrollToPage(ann.pageIndex);
2438
- closeSheet();
2439
- },
2440
- style: [styles6.noteCard, isDark && styles6.noteCardDark],
2441
- children: [
2442
- /* @__PURE__ */ jsxs7(View6, { style: styles6.noteHeader, children: [
2443
- /* @__PURE__ */ jsx7(View6, { style: [styles6.noteDot, { backgroundColor: ann.color }] }),
2444
- /* @__PURE__ */ jsxs7(Text3, { style: [styles6.noteTitle, isDark && styles6.noteTitleDark], children: [
2445
- t.page,
2446
- " ",
2447
- ann.pageIndex + 1
2448
- ] })
2449
- ] }),
2450
- /* @__PURE__ */ jsx7(Text3, { style: [styles6.noteType, isDark && styles6.noteTypeDark, { color: accentColor }], children: ann.type === "comment" || ann.type === "text" ? t.note.toUpperCase() : ann.type.toUpperCase() }),
2451
- ann.content ? /* @__PURE__ */ jsx7(Text3, { style: [styles6.noteContent, isDark && styles6.noteContentDark], children: ann.content }) : null
2452
- ]
2453
- },
2454
- ann.id
2455
- )) }) })
2456
- ] })
2457
- ] }) });
3535
+ )
3536
+ ] })
3537
+ }
3538
+ );
2458
3539
  };
2459
3540
  var styles6 = StyleSheet6.create({
2460
3541
  modalRoot: {
@@ -2835,7 +3916,7 @@ var styles6 = StyleSheet6.create({
2835
3916
  var RightSheet_default = RightSheet;
2836
3917
 
2837
3918
  // components/AnnotationEditor.tsx
2838
- import { useEffect as useEffect6, useState as useState4 } from "react";
3919
+ import { useEffect as useEffect6, useState as useState5 } from "react";
2839
3920
  import { Modal as Modal2, View as View7, Text as Text4, TextInput as TextInput2, Pressable as Pressable5, StyleSheet as StyleSheet7 } from "react-native";
2840
3921
  import { useViewerStore as useViewerStore7 } from "@papyrus-sdk/core";
2841
3922
  import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
@@ -2843,7 +3924,7 @@ var AnnotationEditor = () => {
2843
3924
  const { annotations, selectedAnnotationId, updateAnnotation, setSelectedAnnotation, uiTheme, locale, accentColor } = useViewerStore7();
2844
3925
  const annotation = annotations.find((ann) => ann.id === selectedAnnotationId);
2845
3926
  const isEditable = annotation && (annotation.type === "text" || annotation.type === "comment");
2846
- const [draft, setDraft] = useState4("");
3927
+ const [draft, setDraft] = useState5("");
2847
3928
  const isDark = uiTheme === "dark";
2848
3929
  const t = getStrings(locale);
2849
3930
  useEffect6(() => {
@@ -3619,7 +4700,7 @@ var styles9 = StyleSheet9.create({
3619
4700
  var SettingsSheet_default = SettingsSheet;
3620
4701
 
3621
4702
  // components/CoverPreview.tsx
3622
- import { useEffect as useEffect7, useMemo as useMemo5, useRef as useRef5, useState as useState5 } from "react";
4703
+ import { useEffect as useEffect7, useMemo as useMemo5, useRef as useRef5, useState as useState6 } from "react";
3623
4704
  import {
3624
4705
  ActivityIndicator as ActivityIndicator2,
3625
4706
  StyleSheet as StyleSheet10,
@@ -3677,11 +4758,11 @@ var CoverPreview = ({
3677
4758
  onLoadEnd,
3678
4759
  onError
3679
4760
  }) => {
3680
- const [engine, setEngine] = useState5(null);
3681
- const [layoutReady, setLayoutReady] = useState5(false);
3682
- const [loaded, setLoaded] = useState5(false);
3683
- const [loading, setLoading] = useState5(false);
3684
- const [error, setError] = useState5(null);
4761
+ const [engine, setEngine] = useState6(null);
4762
+ const [layoutReady, setLayoutReady] = useState6(false);
4763
+ const [loaded, setLoaded] = useState6(false);
4764
+ const [loading, setLoading] = useState6(false);
4765
+ const [error, setError] = useState6(null);
3685
4766
  const viewRef = useRef5(null);
3686
4767
  const resolvedType = useMemo5(() => inferDocumentType(source, type), [source, type]);
3687
4768
  const isPdf = resolvedType === "pdf";