@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.js CHANGED
@@ -625,6 +625,190 @@ var import_react = require("react");
625
625
  var import_react_native = require("react-native");
626
626
  var import_core = require("@papyrus-sdk/core");
627
627
  var import_engine_native = require("@papyrus-sdk/engine-native");
628
+
629
+ // perf/mobilePerf.ts
630
+ var DEFAULT_PREFIX = "[Papyrus Perf]";
631
+ var FRAME_BUDGET_MS = 1e3 / 60;
632
+ var getPerfGlobal = () => globalThis.__PAPYRUS_MOBILE_PERF__;
633
+ var getPerfConfig = () => {
634
+ const value = getPerfGlobal();
635
+ if (value === true) {
636
+ return {
637
+ enabled: true,
638
+ sampleMemory: true,
639
+ logPrefix: DEFAULT_PREFIX,
640
+ verbose: false
641
+ };
642
+ }
643
+ if (!value || typeof value !== "object") {
644
+ return {
645
+ enabled: false,
646
+ sampleMemory: false,
647
+ logPrefix: DEFAULT_PREFIX,
648
+ verbose: false
649
+ };
650
+ }
651
+ return {
652
+ enabled: value.enabled ?? true,
653
+ sampleMemory: value.sampleMemory ?? true,
654
+ logPrefix: value.logPrefix ?? DEFAULT_PREFIX,
655
+ verbose: value.verbose ?? false
656
+ };
657
+ };
658
+ var round = (value) => Math.round(value * 100) / 100;
659
+ var bytesToMb = (bytes) => round(bytes / (1024 * 1024));
660
+ var getNumericValue = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
661
+ var readHermesHeapBytes = () => {
662
+ const runtimeProperties = globalThis.HermesInternal?.getRuntimeProperties?.();
663
+ if (!runtimeProperties || typeof runtimeProperties !== "object") return null;
664
+ const candidates = [
665
+ "JSHeapSize",
666
+ "js_heap_size",
667
+ "HeapSize",
668
+ "heapSize",
669
+ "TotalAllocatedBytes",
670
+ "totalAllocatedBytes",
671
+ "mallocSize"
672
+ ];
673
+ for (const key of candidates) {
674
+ const value = getNumericValue(runtimeProperties[key]);
675
+ if (value !== null) return value;
676
+ }
677
+ return null;
678
+ };
679
+ var logPerf = (scope, event, payload) => {
680
+ const config = getPerfConfig();
681
+ if (!config.enabled) return;
682
+ const line = `${config.logPrefix}[${scope}] ${event}`;
683
+ if (payload) {
684
+ console.log(line, payload);
685
+ return;
686
+ }
687
+ console.log(line);
688
+ };
689
+ var isMobilePerfEnabled = () => getPerfConfig().enabled;
690
+ var perfNow = () => {
691
+ if (typeof performance !== "undefined" && typeof performance.now === "function") {
692
+ return performance.now();
693
+ }
694
+ return Date.now();
695
+ };
696
+ var sampleMemory = (scope, event, payload) => {
697
+ const config = getPerfConfig();
698
+ if (!config.enabled || !config.sampleMemory) return;
699
+ const performanceMemory = globalThis.performance?.memory;
700
+ const jsHeapUsedBytes = getNumericValue(performanceMemory?.usedJSHeapSize);
701
+ const jsHeapTotalBytes = getNumericValue(performanceMemory?.totalJSHeapSize);
702
+ const hermesHeapBytes = readHermesHeapBytes();
703
+ if (jsHeapUsedBytes === null && jsHeapTotalBytes === null && hermesHeapBytes === null) return;
704
+ logPerf(scope, `memory.${event}`, {
705
+ ...payload,
706
+ jsHeapUsedMb: jsHeapUsedBytes === null ? void 0 : bytesToMb(jsHeapUsedBytes),
707
+ jsHeapTotalMb: jsHeapTotalBytes === null ? void 0 : bytesToMb(jsHeapTotalBytes),
708
+ hermesHeapMb: hermesHeapBytes === null ? void 0 : bytesToMb(hermesHeapBytes)
709
+ });
710
+ };
711
+ var createBurstMonitor = (scope, label, threshold = 12, windowMs = 1e3) => {
712
+ let windowStart = 0;
713
+ let calls = 0;
714
+ return (payload) => {
715
+ const config = getPerfConfig();
716
+ if (!config.enabled) return;
717
+ const now = perfNow();
718
+ if (windowStart === 0 || now - windowStart > windowMs) {
719
+ windowStart = now;
720
+ calls = 0;
721
+ }
722
+ calls += 1;
723
+ if (calls === threshold || config.verbose) {
724
+ logPerf(scope, `${label}.burst`, {
725
+ calls,
726
+ windowMs: round(now - windowStart),
727
+ ...payload
728
+ });
729
+ }
730
+ };
731
+ };
732
+ var createRenderCounter = (scope, label = "render", reportEvery = 30) => {
733
+ let count = 0;
734
+ return (payload) => {
735
+ const config = getPerfConfig();
736
+ if (!config.enabled) return;
737
+ count += 1;
738
+ if (count === 1 || count % reportEvery === 0 || config.verbose) {
739
+ logPerf(scope, label, {
740
+ count,
741
+ ...payload
742
+ });
743
+ }
744
+ };
745
+ };
746
+ var createScrollPerfMonitor = (scope, label = "scroll") => {
747
+ let active = false;
748
+ let startAt = 0;
749
+ let lastEventAt = 0;
750
+ let sampleEvents = 0;
751
+ let droppedFrames = 0;
752
+ let maxFrameGapMs = 0;
753
+ const reset = () => {
754
+ active = false;
755
+ startAt = 0;
756
+ lastEventAt = 0;
757
+ sampleEvents = 0;
758
+ droppedFrames = 0;
759
+ maxFrameGapMs = 0;
760
+ };
761
+ return {
762
+ begin: (reason, payload) => {
763
+ const config = getPerfConfig();
764
+ if (!config.enabled) return;
765
+ if (active) return;
766
+ active = true;
767
+ startAt = perfNow();
768
+ if (reason && config.verbose) {
769
+ logPerf(scope, `${label}.begin`, {
770
+ reason,
771
+ ...payload
772
+ });
773
+ }
774
+ },
775
+ track: (timestampMs) => {
776
+ if (!isMobilePerfEnabled()) return;
777
+ const now = typeof timestampMs === "number" && Number.isFinite(timestampMs) ? timestampMs : perfNow();
778
+ if (!active) {
779
+ active = true;
780
+ startAt = now;
781
+ }
782
+ if (lastEventAt > 0) {
783
+ const frameGap = Math.max(0, now - lastEventAt);
784
+ maxFrameGapMs = Math.max(maxFrameGapMs, frameGap);
785
+ droppedFrames += Math.max(0, Math.round(frameGap / FRAME_BUDGET_MS) - 1);
786
+ }
787
+ sampleEvents += 1;
788
+ lastEventAt = now;
789
+ },
790
+ end: (reason, payload) => {
791
+ if (!isMobilePerfEnabled() || !active) return;
792
+ const stopAt = lastEventAt || perfNow();
793
+ const durationMs = Math.max(0, stopAt - startAt);
794
+ const estimatedFrameTotal = Math.max(sampleEvents + droppedFrames, 1);
795
+ const fpsEstimate = durationMs > 0 ? sampleEvents * 1e3 / durationMs : 0;
796
+ logPerf(scope, `${label}.${reason}`, {
797
+ durationMs: round(durationMs),
798
+ sampleEvents,
799
+ droppedFrames,
800
+ droppedFramesPct: round(droppedFrames / estimatedFrameTotal * 100),
801
+ fpsEstimate: round(fpsEstimate),
802
+ maxFrameGapMs: round(maxFrameGapMs),
803
+ ...payload
804
+ });
805
+ reset();
806
+ }
807
+ };
808
+ };
809
+ var logPerfEvent = logPerf;
810
+
811
+ // components/PageRenderer.tsx
628
812
  var import_jsx_runtime = require("react/jsx-runtime");
629
813
  var PageRenderer = ({
630
814
  engine,
@@ -641,6 +825,11 @@ var PageRenderer = ({
641
825
  const { width: windowWidth } = (0, import_react_native.useWindowDimensions)();
642
826
  const isAndroid = import_react_native.Platform.OS === "android";
643
827
  const isNative = import_react_native.Platform.OS === "android" || import_react_native.Platform.OS === "ios";
828
+ const perfEnabled = isMobilePerfEnabled();
829
+ const renderCountRef = (0, import_react.useRef)(0);
830
+ const setStateBurstRef = (0, import_react.useRef)(
831
+ createBurstMonitor("PageRenderer", "setDocumentState", 18, 700)
832
+ );
644
833
  const {
645
834
  zoom,
646
835
  rotation,
@@ -657,6 +846,19 @@ var PageRenderer = ({
657
846
  activeSearchIndex,
658
847
  setSelectionActive
659
848
  } = (0, import_core.useViewerStore)();
849
+ const setDocumentStateTracked = (0, import_react.useCallback)(
850
+ (state, reason) => {
851
+ if (perfEnabled) {
852
+ setStateBurstRef.current({
853
+ reason,
854
+ page: pageIndex + 1,
855
+ keys: Object.keys(state).join(",")
856
+ });
857
+ }
858
+ setDocumentState(state);
859
+ },
860
+ [pageIndex, perfEnabled, setDocumentState]
861
+ );
660
862
  const pageAnnotations = (0, import_react.useMemo)(
661
863
  () => annotations.filter((ann) => ann.pageIndex === pageIndex),
662
864
  [annotations, pageIndex]
@@ -665,6 +867,17 @@ var PageRenderer = ({
665
867
  () => searchResults.map((result, index) => ({ result, index })).filter(({ result }) => result.pageIndex === pageIndex),
666
868
  [searchResults, pageIndex]
667
869
  );
870
+ renderCountRef.current += 1;
871
+ if (perfEnabled && (renderCountRef.current === 1 || renderCountRef.current % 20 === 0)) {
872
+ logPerfEvent("PageRenderer", "render", {
873
+ page: pageIndex + 1,
874
+ renderCount: renderCountRef.current,
875
+ zoom,
876
+ rotation,
877
+ annotationCount: pageAnnotations.length,
878
+ searchHits: pageSearchHits.length
879
+ });
880
+ }
668
881
  const [selectionRect, setSelectionRect] = (0, import_react.useState)(null);
669
882
  const [selectionRects, setSelectionRects] = (0, import_react.useState)([]);
670
883
  const [selectionBounds, setSelectionBounds] = (0, import_react.useState)(null);
@@ -674,30 +887,72 @@ var PageRenderer = ({
674
887
  const selectionRectRef = (0, import_react.useRef)(null);
675
888
  const selectionBoundsRef = (0, import_react.useRef)(null);
676
889
  const selectionBoundsStart = (0, import_react.useRef)(null);
677
- const lastTapRef = (0, import_react.useRef)(null);
890
+ const lastTapRef = (0, import_react.useRef)(
891
+ null
892
+ );
678
893
  const pinchRef = (0, import_react.useRef)(null);
679
894
  (0, import_react.useEffect)(() => {
680
895
  if (!layout.width || !layout.height) return;
681
896
  const viewTag = (0, import_react_native.findNodeHandle)(viewRef.current);
682
897
  if (viewTag) {
683
898
  const renderScale = isNative ? scale / Math.max(zoom, 0.5) : scale;
684
- void engine.renderPage(pageIndex, viewTag, renderScale);
899
+ const startedAt = perfEnabled ? perfNow() : 0;
900
+ void Promise.resolve(engine.renderPage(pageIndex, viewTag, renderScale)).then(() => {
901
+ if (!perfEnabled) return;
902
+ const renderDurationMs = perfNow() - startedAt;
903
+ if (renderDurationMs >= 40) {
904
+ logPerfEvent("PageRenderer", "renderPage.slow", {
905
+ page: pageIndex + 1,
906
+ renderDurationMs: Math.round(renderDurationMs * 100) / 100,
907
+ layoutWidth: layout.width,
908
+ layoutHeight: layout.height,
909
+ renderScale: Math.round(renderScale * 100) / 100
910
+ });
911
+ }
912
+ }).catch((error) => {
913
+ logPerfEvent("PageRenderer", "renderPage.error", {
914
+ page: pageIndex + 1,
915
+ message: error instanceof Error ? error.message : String(error)
916
+ });
917
+ });
685
918
  }
686
- }, [engine, pageIndex, scale, zoom, rotation, layout.width, layout.height, isNative]);
919
+ }, [
920
+ engine,
921
+ pageIndex,
922
+ scale,
923
+ zoom,
924
+ rotation,
925
+ layout.width,
926
+ layout.height,
927
+ isNative,
928
+ perfEnabled
929
+ ]);
687
930
  (0, import_react.useEffect)(() => {
688
931
  let active = true;
689
932
  const loadDimensions = async () => {
933
+ const startedAt = perfEnabled ? perfNow() : 0;
690
934
  const dims = await engine.getPageDimensions(pageIndex);
691
935
  if (!active) return;
692
936
  if (dims.width > 0 && dims.height > 0) {
693
937
  setPageSize({ width: dims.width, height: dims.height });
694
938
  }
939
+ if (perfEnabled) {
940
+ const durationMs = perfNow() - startedAt;
941
+ if (durationMs >= 20 || pageIndex === 0) {
942
+ logPerfEvent("PageRenderer", "pageDimensions", {
943
+ page: pageIndex + 1,
944
+ durationMs: Math.round(durationMs * 100) / 100,
945
+ width: dims.width,
946
+ height: dims.height
947
+ });
948
+ }
949
+ }
695
950
  };
696
951
  void loadDimensions();
697
952
  return () => {
698
953
  active = false;
699
954
  };
700
- }, [engine, pageIndex]);
955
+ }, [engine, pageIndex, perfEnabled]);
701
956
  const handleLayout = (event) => {
702
957
  const { width, height } = event.nativeEvent.layout;
703
958
  if (width !== layout.width || height !== layout.height) {
@@ -797,7 +1052,7 @@ var PageRenderer = ({
797
1052
  if (!distance) return;
798
1053
  const scale2 = distance / pinchRef.current.distance;
799
1054
  const nextZoom = clamp(pinchRef.current.zoom * scale2, 0.5, 4);
800
- setDocumentState({ zoom: nextZoom });
1055
+ setDocumentStateTracked({ zoom: nextZoom }, "pinchMove");
801
1056
  engine.setZoom(nextZoom);
802
1057
  };
803
1058
  const handlePinchEnd = () => {
@@ -845,7 +1100,12 @@ var PageRenderer = ({
845
1100
  const top = Math.max(0, Math.min(start.y, currentY));
846
1101
  const right = Math.min(layout.width, Math.max(start.x, currentX));
847
1102
  const bottom = Math.min(layout.height, Math.max(start.y, currentY));
848
- const rect = { x: left, y: top, width: right - left, height: bottom - top };
1103
+ const rect = {
1104
+ x: left,
1105
+ y: top,
1106
+ width: right - left,
1107
+ height: bottom - top
1108
+ };
849
1109
  selectionRectRef.current = rect;
850
1110
  setSelectionRect(rect);
851
1111
  },
@@ -941,7 +1201,13 @@ var PageRenderer = ({
941
1201
  if (selectionRects.length === 0) return;
942
1202
  if (type === "comment") {
943
1203
  const first = selectionRects[0];
944
- addAnnotationAt(first.x, first.y, Math.max(0.08, first.width), Math.max(0.06, first.height), "comment");
1204
+ addAnnotationAt(
1205
+ first.x,
1206
+ first.y,
1207
+ Math.max(0.08, first.width),
1208
+ Math.max(0.06, first.height),
1209
+ "comment"
1210
+ );
945
1211
  clearSelection();
946
1212
  return;
947
1213
  }
@@ -975,12 +1241,18 @@ var PageRenderer = ({
975
1241
  horizontal: true,
976
1242
  scrollEnabled,
977
1243
  showsHorizontalScrollIndicator: false,
978
- contentContainerStyle: [styles.scrollContent, { paddingHorizontal: horizontalPadding }],
1244
+ contentContainerStyle: [
1245
+ styles.scrollContent,
1246
+ { paddingHorizontal: horizontalPadding }
1247
+ ],
979
1248
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
980
1249
  import_react_native.Pressable,
981
1250
  {
982
1251
  ...panResponder.panHandlers,
983
- style: [styles.container, { width: pageWidth, height: pageHeight, marginBottom: spacing }],
1252
+ style: [
1253
+ styles.container,
1254
+ { width: pageWidth, height: pageHeight, marginBottom: spacing }
1255
+ ],
984
1256
  onLayout: handleLayout,
985
1257
  onPress: handlePress,
986
1258
  onStartShouldSetResponder: (event) => shouldHandlePinch(event.nativeEvent.touches),
@@ -991,7 +1263,13 @@ var PageRenderer = ({
991
1263
  onResponderTerminate: handlePinchEnd,
992
1264
  children: [
993
1265
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PageViewComponent, { ref: viewRef, style: styles.page }),
994
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.View, { pointerEvents: "none", style: [styles.themeOverlay, themeOverlayStyle] }),
1266
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1267
+ import_react_native.View,
1268
+ {
1269
+ pointerEvents: "none",
1270
+ style: [styles.themeOverlay, themeOverlayStyle]
1271
+ }
1272
+ ),
995
1273
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native.View, { pointerEvents: "box-none", style: styles.selectionLayer, children: [
996
1274
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.View, { pointerEvents: "none", children: selectionRects.map((rect, index) => {
997
1275
  const style = {
@@ -1000,7 +1278,13 @@ var PageRenderer = ({
1000
1278
  width: `${rect.width * 100}%`,
1001
1279
  height: `${rect.height * 100}%`
1002
1280
  };
1003
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.View, { style: [styles.selectionHighlight, style] }, `sel-${index}`);
1281
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1282
+ import_react_native.View,
1283
+ {
1284
+ style: [styles.selectionHighlight, style]
1285
+ },
1286
+ `sel-${index}`
1287
+ );
1004
1288
  }) }),
1005
1289
  selectionBoundsPx ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1006
1290
  import_react_native.View,
@@ -1021,14 +1305,20 @@ var PageRenderer = ({
1021
1305
  import_react_native.View,
1022
1306
  {
1023
1307
  ...startHandleResponder.panHandlers,
1024
- style: [styles.selectionHandle, { left: -8, top: -8, borderColor: accentColor }]
1308
+ style: [
1309
+ styles.selectionHandle,
1310
+ { left: -8, top: -8, borderColor: accentColor }
1311
+ ]
1025
1312
  }
1026
1313
  ),
1027
1314
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1028
1315
  import_react_native.View,
1029
1316
  {
1030
1317
  ...endHandleResponder.panHandlers,
1031
- style: [styles.selectionHandle, { right: -8, bottom: -8, borderColor: accentColor }]
1318
+ style: [
1319
+ styles.selectionHandle,
1320
+ { right: -8, bottom: -8, borderColor: accentColor }
1321
+ ]
1032
1322
  }
1033
1323
  )
1034
1324
  ]
@@ -1067,9 +1357,15 @@ var PageRenderer = ({
1067
1357
  {
1068
1358
  style: [
1069
1359
  styles.searchHighlight,
1070
- { borderColor: accentColor, backgroundColor: `${accentColor}26` },
1360
+ {
1361
+ borderColor: accentColor,
1362
+ backgroundColor: `${accentColor}26`
1363
+ },
1071
1364
  isActive && styles.searchHighlightActive,
1072
- isActive && { borderColor: accentColor, backgroundColor: `${accentColor}40` },
1365
+ isActive && {
1366
+ borderColor: accentColor,
1367
+ backgroundColor: `${accentColor}40`
1368
+ },
1073
1369
  highlightStyle
1074
1370
  ]
1075
1371
  },
@@ -1099,8 +1395,29 @@ var PageRenderer = ({
1099
1395
  isSelected && { borderColor: accentColor }
1100
1396
  ],
1101
1397
  children: [
1102
- (ann.type === "comment" || ann.type === "text") && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.View, { style: [styles.annotationBadge, { borderColor: ann.color }], children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.View, { style: [styles.annotationDot, { backgroundColor: ann.color }] }) }),
1103
- isSelected && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.Pressable, { onPress: () => removeAnnotation(ann.id), style: styles.deleteButton, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.View, { style: styles.deleteDot }) })
1398
+ (ann.type === "comment" || ann.type === "text") && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1399
+ import_react_native.View,
1400
+ {
1401
+ style: [styles.annotationBadge, { borderColor: ann.color }],
1402
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1403
+ import_react_native.View,
1404
+ {
1405
+ style: [
1406
+ styles.annotationDot,
1407
+ { backgroundColor: ann.color }
1408
+ ]
1409
+ }
1410
+ )
1411
+ }
1412
+ ),
1413
+ isSelected && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1414
+ import_react_native.Pressable,
1415
+ {
1416
+ onPress: () => removeAnnotation(ann.id),
1417
+ style: styles.deleteButton,
1418
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.View, { style: styles.deleteDot })
1419
+ }
1420
+ )
1104
1421
  ]
1105
1422
  },
1106
1423
  ann.id
@@ -1118,9 +1435,46 @@ var PageRenderer = ({
1118
1435
  }
1119
1436
  ],
1120
1437
  children: [
1121
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.Pressable, { onPress: () => applySelection("comment"), style: styles.selectionAction, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.View, { style: styles.selectionActionDot }) }),
1122
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.Pressable, { onPress: () => applySelection("highlight"), style: styles.selectionAction, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.View, { style: [styles.selectionSwatch, { backgroundColor: annotationColor }] }) }),
1123
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.Pressable, { onPress: () => applySelection("strikeout"), style: [styles.selectionAction, styles.selectionActionLast], children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.View, { style: [styles.selectionStrike, { backgroundColor: annotationColor }] }) })
1438
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1439
+ import_react_native.Pressable,
1440
+ {
1441
+ onPress: () => applySelection("comment"),
1442
+ style: styles.selectionAction,
1443
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.View, { style: styles.selectionActionDot })
1444
+ }
1445
+ ),
1446
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1447
+ import_react_native.Pressable,
1448
+ {
1449
+ onPress: () => applySelection("highlight"),
1450
+ style: styles.selectionAction,
1451
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1452
+ import_react_native.View,
1453
+ {
1454
+ style: [
1455
+ styles.selectionSwatch,
1456
+ { backgroundColor: annotationColor }
1457
+ ]
1458
+ }
1459
+ )
1460
+ }
1461
+ ),
1462
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1463
+ import_react_native.Pressable,
1464
+ {
1465
+ onPress: () => applySelection("strikeout"),
1466
+ style: [styles.selectionAction, styles.selectionActionLast],
1467
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1468
+ import_react_native.View,
1469
+ {
1470
+ style: [
1471
+ styles.selectionStrike,
1472
+ { backgroundColor: annotationColor }
1473
+ ]
1474
+ }
1475
+ )
1476
+ }
1477
+ )
1124
1478
  ]
1125
1479
  }
1126
1480
  ) : null
@@ -1418,8 +1772,27 @@ var WebViewViewer_default = WebViewViewer;
1418
1772
 
1419
1773
  // components/Viewer.tsx
1420
1774
  var import_jsx_runtime3 = require("react/jsx-runtime");
1775
+ var LIST_TOP_PADDING = 18;
1776
+ var LIST_BOTTOM_PADDING = 120;
1777
+ var CONTINUOUS_PAGE_SPACING = 28;
1778
+ var DOUBLE_PAGE_SPACING = 20;
1779
+ var DEFAULT_PAGE_ASPECT_RATIO = 0.77;
1780
+ var FLATLIST_WINDOW_SIZE = 8;
1781
+ var FLATLIST_MAX_TO_RENDER_PER_BATCH = 6;
1782
+ var FLATLIST_UPDATE_CELLS_BATCHING_PERIOD = 40;
1783
+ var FLATLIST_INITIAL_NUM_TO_RENDER = 6;
1784
+ var SCROLL_RETRY_DELAY_MS = 120;
1785
+ var SCROLL_MAX_RETRIES = 10;
1421
1786
  var Viewer = ({ engine }) => {
1422
- const { pageCount, currentPage, scrollToPageSignal, setDocumentState, uiTheme, viewMode } = (0, import_core3.useViewerStore)();
1787
+ const {
1788
+ pageCount,
1789
+ currentPage,
1790
+ scrollToPageSignal,
1791
+ setDocumentState,
1792
+ uiTheme,
1793
+ viewMode,
1794
+ zoom
1795
+ } = (0, import_core3.useViewerStore)();
1423
1796
  const listRef = (0, import_react3.useRef)(null);
1424
1797
  const isDark = uiTheme === "dark";
1425
1798
  const { width: windowWidth } = (0, import_react_native3.useWindowDimensions)();
@@ -1427,7 +1800,38 @@ var Viewer = ({ engine }) => {
1427
1800
  const isSingle = viewMode === "single";
1428
1801
  const renderTargetType = engine.getRenderTargetType?.() ?? "canvas";
1429
1802
  const isWebView = renderTargetType === "webview";
1430
- const pages = (0, import_react3.useMemo)(() => Array.from({ length: pageCount }).map((_, i) => i), [pageCount]);
1803
+ const perfEnabled = isMobilePerfEnabled();
1804
+ const mountedAtRef = (0, import_react3.useRef)(perfNow());
1805
+ const readyLoggedRef = (0, import_react3.useRef)(false);
1806
+ const renderCounterRef = (0, import_react3.useRef)(createRenderCounter("Viewer", "render", 40));
1807
+ const setStateBurstRef = (0, import_react3.useRef)(
1808
+ createBurstMonitor("Viewer", "setDocumentState", 12, 800)
1809
+ );
1810
+ const viewableBurstRef = (0, import_react3.useRef)(
1811
+ createBurstMonitor("Viewer", "onViewableItemsChanged", 12, 800)
1812
+ );
1813
+ const scrollMonitorRef = (0, import_react3.useRef)(createScrollPerfMonitor("Viewer"));
1814
+ const dimensionsCacheRef = (0, import_react3.useRef)(/* @__PURE__ */ new Map());
1815
+ const dimensionsPendingRef = (0, import_react3.useRef)(/* @__PURE__ */ new Set());
1816
+ const layoutRefreshTimeoutRef = (0, import_react3.useRef)(
1817
+ null
1818
+ );
1819
+ const pendingScrollIndexRef = (0, import_react3.useRef)(null);
1820
+ const pendingScrollAttemptsRef = (0, import_react3.useRef)(0);
1821
+ const pendingScrollTimeoutRef = (0, import_react3.useRef)(
1822
+ null
1823
+ );
1824
+ const [layoutRevision, setLayoutRevision] = (0, import_react3.useState)(0);
1825
+ renderCounterRef.current({
1826
+ pageCount,
1827
+ currentPage,
1828
+ viewMode,
1829
+ renderTargetType
1830
+ });
1831
+ const pages = (0, import_react3.useMemo)(
1832
+ () => Array.from({ length: pageCount }).map((_, i) => i),
1833
+ [pageCount]
1834
+ );
1431
1835
  const rows = (0, import_react3.useMemo)(() => {
1432
1836
  if (!isDouble) return [];
1433
1837
  const result = [];
@@ -1436,48 +1840,354 @@ var Viewer = ({ engine }) => {
1436
1840
  }
1437
1841
  return result;
1438
1842
  }, [isDouble, pageCount]);
1843
+ const scheduleLayoutRefresh = (0, import_react3.useCallback)(() => {
1844
+ if (layoutRefreshTimeoutRef.current) return;
1845
+ layoutRefreshTimeoutRef.current = setTimeout(() => {
1846
+ layoutRefreshTimeoutRef.current = null;
1847
+ setLayoutRevision((value) => value + 1);
1848
+ }, 120);
1849
+ }, []);
1850
+ const clearPendingScrollRetry = (0, import_react3.useCallback)(() => {
1851
+ if (pendingScrollTimeoutRef.current) {
1852
+ clearTimeout(pendingScrollTimeoutRef.current);
1853
+ pendingScrollTimeoutRef.current = null;
1854
+ }
1855
+ }, []);
1856
+ const clearPendingScrollTarget = (0, import_react3.useCallback)(() => {
1857
+ pendingScrollIndexRef.current = null;
1858
+ pendingScrollAttemptsRef.current = 0;
1859
+ clearPendingScrollRetry();
1860
+ }, [clearPendingScrollRetry]);
1861
+ const scheduleScrollRetry = (0, import_react3.useCallback)(
1862
+ (reason) => {
1863
+ const pendingIndex = pendingScrollIndexRef.current;
1864
+ if (pendingIndex === null) return;
1865
+ if (pendingScrollAttemptsRef.current >= SCROLL_MAX_RETRIES) {
1866
+ if (perfEnabled) {
1867
+ logPerfEvent("Viewer", "scroll.retry.giveup", {
1868
+ reason,
1869
+ targetIndex: pendingIndex,
1870
+ attempts: pendingScrollAttemptsRef.current
1871
+ });
1872
+ }
1873
+ clearPendingScrollTarget();
1874
+ return;
1875
+ }
1876
+ clearPendingScrollRetry();
1877
+ pendingScrollTimeoutRef.current = setTimeout(() => {
1878
+ pendingScrollTimeoutRef.current = null;
1879
+ const targetIndex = pendingScrollIndexRef.current;
1880
+ if (targetIndex === null) return;
1881
+ pendingScrollAttemptsRef.current += 1;
1882
+ listRef.current?.scrollToIndex({
1883
+ index: targetIndex,
1884
+ animated: false,
1885
+ viewPosition: 0
1886
+ });
1887
+ if (perfEnabled) {
1888
+ logPerfEvent("Viewer", "scroll.retry", {
1889
+ reason,
1890
+ targetIndex,
1891
+ attempt: pendingScrollAttemptsRef.current
1892
+ });
1893
+ }
1894
+ }, SCROLL_RETRY_DELAY_MS);
1895
+ },
1896
+ [clearPendingScrollRetry, clearPendingScrollTarget, perfEnabled]
1897
+ );
1898
+ (0, import_react3.useEffect)(
1899
+ () => () => {
1900
+ if (layoutRefreshTimeoutRef.current) {
1901
+ clearTimeout(layoutRefreshTimeoutRef.current);
1902
+ }
1903
+ clearPendingScrollRetry();
1904
+ },
1905
+ [clearPendingScrollRetry]
1906
+ );
1907
+ const ensurePageDimensions = (0, import_react3.useCallback)(
1908
+ (pageIndex) => {
1909
+ if (pageIndex < 0 || pageIndex >= pageCount) return;
1910
+ if (dimensionsCacheRef.current.has(pageIndex)) return;
1911
+ if (dimensionsPendingRef.current.has(pageIndex)) return;
1912
+ dimensionsPendingRef.current.add(pageIndex);
1913
+ void engine.getPageDimensions(pageIndex).then((dims) => {
1914
+ if (dims.width <= 0 || dims.height <= 0) return;
1915
+ const previous = dimensionsCacheRef.current.get(pageIndex);
1916
+ if (previous && previous.width === dims.width && previous.height === dims.height) {
1917
+ return;
1918
+ }
1919
+ dimensionsCacheRef.current.set(pageIndex, {
1920
+ width: dims.width,
1921
+ height: dims.height
1922
+ });
1923
+ scheduleLayoutRefresh();
1924
+ }).catch((error) => {
1925
+ if (!perfEnabled) return;
1926
+ logPerfEvent("Viewer", "pageDimensions.error", {
1927
+ page: pageIndex + 1,
1928
+ message: error instanceof Error ? error.message : String(error)
1929
+ });
1930
+ }).finally(() => {
1931
+ dimensionsPendingRef.current.delete(pageIndex);
1932
+ });
1933
+ },
1934
+ [engine, pageCount, perfEnabled, scheduleLayoutRefresh]
1935
+ );
1936
+ const getPageAspectRatio = (0, import_react3.useCallback)((pageIndex) => {
1937
+ const dims = dimensionsCacheRef.current.get(pageIndex);
1938
+ if (!dims || dims.width <= 0 || dims.height <= 0) {
1939
+ return DEFAULT_PAGE_ASPECT_RATIO;
1940
+ }
1941
+ return dims.width / dims.height;
1942
+ }, []);
1943
+ (0, import_react3.useEffect)(() => {
1944
+ if (!perfEnabled) return;
1945
+ logPerfEvent("Viewer", "mount", { viewMode, renderTargetType });
1946
+ sampleMemory("Viewer", "mount", { pageCount });
1947
+ return () => {
1948
+ logPerfEvent("Viewer", "unmount");
1949
+ };
1950
+ }, [perfEnabled]);
1951
+ (0, import_react3.useEffect)(() => {
1952
+ if (!perfEnabled || readyLoggedRef.current || pageCount <= 0) return;
1953
+ readyLoggedRef.current = true;
1954
+ logPerfEvent("Viewer", "document.ready", {
1955
+ pageCount,
1956
+ initialLoadMs: Math.round((perfNow() - mountedAtRef.current) * 100) / 100
1957
+ });
1958
+ sampleMemory("Viewer", "document.ready", { pageCount });
1959
+ }, [pageCount, perfEnabled]);
1960
+ (0, import_react3.useEffect)(() => {
1961
+ if (isWebView || isSingle || pageCount <= 0) return;
1962
+ const warmupCount = Math.min(pageCount, 12);
1963
+ for (let i = 0; i < warmupCount; i += 1) {
1964
+ ensurePageDimensions(i);
1965
+ }
1966
+ }, [ensurePageDimensions, isSingle, isWebView, pageCount]);
1967
+ const setDocumentStateTracked = (0, import_react3.useCallback)(
1968
+ (state, reason) => {
1969
+ if (perfEnabled) {
1970
+ setStateBurstRef.current({
1971
+ reason,
1972
+ keys: Object.keys(state).join(",")
1973
+ });
1974
+ }
1975
+ setDocumentState(state);
1976
+ },
1977
+ [perfEnabled, setDocumentState]
1978
+ );
1979
+ const columnGap = 12;
1980
+ const horizontalPadding = 16;
1981
+ const columnWidth = isDouble ? (windowWidth - horizontalPadding * 2 - columnGap) / 2 : windowWidth;
1982
+ const listLayoutMetrics = (0, import_react3.useMemo)(() => {
1983
+ const offsets = [];
1984
+ const lengths = [];
1985
+ let offset = LIST_TOP_PADDING;
1986
+ if (isDouble) {
1987
+ const safeZoom2 = Math.max(zoom, 0.25);
1988
+ const pageWidth2 = columnWidth * 0.92 * safeZoom2;
1989
+ const estimatedLength2 = pageWidth2 / DEFAULT_PAGE_ASPECT_RATIO + DOUBLE_PAGE_SPACING;
1990
+ for (let i = 0; i < rows.length; i += 1) {
1991
+ const row = rows[i];
1992
+ const leftRatio = getPageAspectRatio(row.left);
1993
+ const rightRatio = row.right === null ? leftRatio : getPageAspectRatio(row.right);
1994
+ const leftLength = pageWidth2 / leftRatio + DOUBLE_PAGE_SPACING;
1995
+ const rightLength = pageWidth2 / rightRatio + DOUBLE_PAGE_SPACING;
1996
+ const rowLength = Math.max(leftLength, rightLength);
1997
+ offsets.push(offset);
1998
+ lengths.push(rowLength);
1999
+ offset += rowLength;
2000
+ }
2001
+ return { offsets, lengths, estimatedLength: estimatedLength2 };
2002
+ }
2003
+ const safeZoom = Math.max(zoom, 0.25);
2004
+ const pageWidth = windowWidth * 0.92 * safeZoom;
2005
+ const estimatedLength = pageWidth / DEFAULT_PAGE_ASPECT_RATIO + CONTINUOUS_PAGE_SPACING;
2006
+ for (let i = 0; i < pageCount; i += 1) {
2007
+ const ratio = getPageAspectRatio(i);
2008
+ const length = pageWidth / ratio + CONTINUOUS_PAGE_SPACING;
2009
+ offsets.push(offset);
2010
+ lengths.push(length);
2011
+ offset += length;
2012
+ }
2013
+ return { offsets, lengths, estimatedLength };
2014
+ }, [
2015
+ columnWidth,
2016
+ getPageAspectRatio,
2017
+ isDouble,
2018
+ layoutRevision,
2019
+ pageCount,
2020
+ rows,
2021
+ windowWidth,
2022
+ zoom
2023
+ ]);
2024
+ const getFallbackOffsetForIndex = (0, import_react3.useCallback)(
2025
+ (index) => {
2026
+ if (listLayoutMetrics.lengths.length === 0) {
2027
+ return LIST_TOP_PADDING;
2028
+ }
2029
+ const safeIndex = Math.max(
2030
+ 0,
2031
+ Math.min(index, listLayoutMetrics.lengths.length - 1)
2032
+ );
2033
+ const cachedOffset = listLayoutMetrics.offsets[safeIndex];
2034
+ if (typeof cachedOffset === "number") return cachedOffset;
2035
+ return LIST_TOP_PADDING + listLayoutMetrics.estimatedLength * safeIndex;
2036
+ },
2037
+ [listLayoutMetrics]
2038
+ );
2039
+ const getItemLayout = (0, import_react3.useCallback)(
2040
+ (_, index) => {
2041
+ if (listLayoutMetrics.lengths.length === 0) {
2042
+ return {
2043
+ index,
2044
+ length: listLayoutMetrics.estimatedLength,
2045
+ offset: LIST_TOP_PADDING
2046
+ };
2047
+ }
2048
+ const safeIndex = Math.max(
2049
+ 0,
2050
+ Math.min(index, listLayoutMetrics.lengths.length - 1)
2051
+ );
2052
+ if (isDouble) {
2053
+ const row = rows[safeIndex];
2054
+ if (row) {
2055
+ ensurePageDimensions(row.left);
2056
+ if (row.right !== null) {
2057
+ ensurePageDimensions(row.right);
2058
+ }
2059
+ }
2060
+ } else {
2061
+ ensurePageDimensions(safeIndex);
2062
+ }
2063
+ const cachedLength = listLayoutMetrics.lengths[safeIndex];
2064
+ const cachedOffset = listLayoutMetrics.offsets[safeIndex];
2065
+ return {
2066
+ index,
2067
+ length: typeof cachedLength === "number" ? cachedLength : listLayoutMetrics.estimatedLength,
2068
+ offset: typeof cachedOffset === "number" ? cachedOffset : LIST_TOP_PADDING + listLayoutMetrics.estimatedLength * safeIndex
2069
+ };
2070
+ },
2071
+ [ensurePageDimensions, isDouble, listLayoutMetrics, rows]
2072
+ );
1439
2073
  (0, import_react3.useEffect)(() => {
1440
2074
  if (isWebView) {
2075
+ clearPendingScrollTarget();
1441
2076
  if (scrollToPageSignal === null) return;
1442
2077
  if (pageCount === 0) return;
1443
2078
  if (scrollToPageSignal < 0 || scrollToPageSignal >= pageCount) return;
1444
2079
  const nextPage = scrollToPageSignal + 1;
1445
2080
  engine.goToPage(nextPage);
1446
- setDocumentState({ currentPage: nextPage, scrollToPageSignal: null });
2081
+ setDocumentStateTracked(
2082
+ { currentPage: nextPage, scrollToPageSignal: null },
2083
+ "scrollToPageSignal.webview"
2084
+ );
1447
2085
  return;
1448
2086
  }
1449
2087
  if (scrollToPageSignal === null) return;
1450
2088
  if (pageCount === 0) return;
1451
2089
  if (scrollToPageSignal < 0 || scrollToPageSignal >= pageCount) return;
1452
2090
  if (isSingle) {
1453
- setDocumentState({ currentPage: scrollToPageSignal + 1, scrollToPageSignal: null });
2091
+ clearPendingScrollTarget();
2092
+ setDocumentStateTracked(
2093
+ { currentPage: scrollToPageSignal + 1, scrollToPageSignal: null },
2094
+ "scrollToPageSignal.single"
2095
+ );
1454
2096
  return;
1455
2097
  }
2098
+ ensurePageDimensions(scrollToPageSignal);
2099
+ if (isDouble) {
2100
+ ensurePageDimensions(scrollToPageSignal - 1);
2101
+ ensurePageDimensions(scrollToPageSignal + 1);
2102
+ }
1456
2103
  const targetIndex = isDouble ? Math.floor(scrollToPageSignal / 2) : scrollToPageSignal;
1457
- listRef.current?.scrollToIndex({ index: targetIndex, animated: true });
1458
- setDocumentState({ scrollToPageSignal: null });
1459
- }, [scrollToPageSignal, pageCount, setDocumentState, isDouble, isSingle, isWebView, engine]);
2104
+ pendingScrollIndexRef.current = targetIndex;
2105
+ pendingScrollAttemptsRef.current = 0;
2106
+ clearPendingScrollRetry();
2107
+ setDocumentStateTracked(
2108
+ {
2109
+ currentPage: scrollToPageSignal + 1,
2110
+ scrollToPageSignal: null
2111
+ },
2112
+ "scrollToPageSignal.flatList"
2113
+ );
2114
+ listRef.current?.scrollToIndex({
2115
+ index: targetIndex,
2116
+ animated: true,
2117
+ viewPosition: 0
2118
+ });
2119
+ }, [
2120
+ clearPendingScrollRetry,
2121
+ clearPendingScrollTarget,
2122
+ ensurePageDimensions,
2123
+ scrollToPageSignal,
2124
+ pageCount,
2125
+ setDocumentStateTracked,
2126
+ isDouble,
2127
+ isSingle,
2128
+ isWebView,
2129
+ engine
2130
+ ]);
1460
2131
  const onViewableItemsChanged = (0, import_react3.useCallback)(
1461
2132
  ({ viewableItems }) => {
2133
+ if (perfEnabled) {
2134
+ viewableBurstRef.current({
2135
+ viewableCount: viewableItems.length,
2136
+ mode: isDouble ? "double" : "continuous"
2137
+ });
2138
+ }
2139
+ const pendingIndex = pendingScrollIndexRef.current;
2140
+ if (pendingIndex !== null) {
2141
+ const reachedTarget = viewableItems.some((token) => {
2142
+ if (isDouble) {
2143
+ const row = token.item;
2144
+ if (!row) return false;
2145
+ if (row.left === pendingIndex) return true;
2146
+ return row.right === pendingIndex;
2147
+ }
2148
+ return token.index === pendingIndex;
2149
+ });
2150
+ if (reachedTarget) {
2151
+ if (perfEnabled) {
2152
+ logPerfEvent("Viewer", "scroll.retry.resolved", {
2153
+ targetIndex: pendingIndex,
2154
+ attempts: pendingScrollAttemptsRef.current
2155
+ });
2156
+ }
2157
+ clearPendingScrollTarget();
2158
+ }
2159
+ }
1462
2160
  const first = viewableItems[0];
1463
2161
  if (!first) return;
1464
2162
  if (isDouble) {
1465
2163
  const item = first.item;
1466
2164
  if (!item) return;
2165
+ ensurePageDimensions(item.left);
2166
+ if (item.right !== null) {
2167
+ ensurePageDimensions(item.right);
2168
+ }
1467
2169
  const page = item.left + 1;
1468
2170
  if (page !== currentPage) {
1469
- setDocumentState({ currentPage: page });
2171
+ setDocumentStateTracked({ currentPage: page }, "viewable.double");
1470
2172
  }
1471
2173
  return;
1472
2174
  }
1473
2175
  if (first.index !== null && first.index !== void 0) {
2176
+ ensurePageDimensions(first.index);
1474
2177
  const page = first.index + 1;
1475
2178
  if (page !== currentPage) {
1476
- setDocumentState({ currentPage: page });
2179
+ setDocumentStateTracked({ currentPage: page }, "viewable.continuous");
1477
2180
  }
1478
2181
  }
1479
2182
  },
1480
- [currentPage, isDouble, setDocumentState]
2183
+ [
2184
+ clearPendingScrollTarget,
2185
+ currentPage,
2186
+ ensurePageDimensions,
2187
+ isDouble,
2188
+ perfEnabled,
2189
+ setDocumentStateTracked
2190
+ ]
1481
2191
  );
1482
2192
  if (isWebView) {
1483
2193
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native3.View, { style: [styles3.container, isDark && styles3.containerDark], children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(WebViewViewer_default, { engine }) });
@@ -1489,50 +2199,137 @@ var Viewer = ({ engine }) => {
1489
2199
  contentContainerStyle: styles3.singleContent,
1490
2200
  showsVerticalScrollIndicator: false,
1491
2201
  scrollEnabled: true,
1492
- children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PageRenderer_default, { engine, pageIndex: Math.max(0, currentPage - 1), spacing: 32 })
2202
+ onScroll: perfEnabled ? (event) => {
2203
+ const timestampValue = event.nativeEvent.timestamp;
2204
+ const timestamp = typeof timestampValue === "number" ? timestampValue : void 0;
2205
+ scrollMonitorRef.current.track(timestamp);
2206
+ } : void 0,
2207
+ onScrollBeginDrag: perfEnabled ? () => {
2208
+ scrollMonitorRef.current.begin("single.beginDrag");
2209
+ } : void 0,
2210
+ onMomentumScrollBegin: perfEnabled ? () => {
2211
+ scrollMonitorRef.current.begin("single.momentumBegin");
2212
+ } : void 0,
2213
+ onScrollEndDrag: perfEnabled ? () => {
2214
+ scrollMonitorRef.current.end("single.endDrag");
2215
+ sampleMemory("Viewer", "single.endDrag", { pageCount });
2216
+ } : void 0,
2217
+ onMomentumScrollEnd: perfEnabled ? () => {
2218
+ scrollMonitorRef.current.end("single.momentumEnd");
2219
+ sampleMemory("Viewer", "single.momentumEnd", { pageCount });
2220
+ } : void 0,
2221
+ scrollEventThrottle: perfEnabled ? 16 : void 0,
2222
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2223
+ PageRenderer_default,
2224
+ {
2225
+ engine,
2226
+ pageIndex: Math.max(0, currentPage - 1),
2227
+ spacing: 32
2228
+ }
2229
+ )
1493
2230
  }
1494
2231
  ) });
1495
2232
  }
1496
- const columnGap = 12;
1497
- const horizontalPadding = 16;
1498
- const columnWidth = isDouble ? (windowWidth - horizontalPadding * 2 - columnGap) / 2 : windowWidth;
1499
2233
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native3.View, { style: [styles3.container, isDark && styles3.containerDark], children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1500
2234
  import_react_native3.FlatList,
1501
2235
  {
1502
2236
  ref: listRef,
1503
2237
  data: isDouble ? rows : pages,
2238
+ initialNumToRender: FLATLIST_INITIAL_NUM_TO_RENDER,
2239
+ windowSize: FLATLIST_WINDOW_SIZE,
2240
+ maxToRenderPerBatch: FLATLIST_MAX_TO_RENDER_PER_BATCH,
2241
+ updateCellsBatchingPeriod: FLATLIST_UPDATE_CELLS_BATCHING_PERIOD,
2242
+ removeClippedSubviews: true,
2243
+ getItemLayout,
1504
2244
  keyExtractor: (item) => isDouble ? `row-${item.left}` : `page-${item}`,
1505
2245
  contentContainerStyle: styles3.listContent,
1506
- renderItem: ({ item }) => isDouble ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_react_native3.View, { style: [styles3.row, { paddingHorizontal: horizontalPadding }], children: [
1507
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native3.View, { style: { width: columnWidth }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1508
- PageRenderer_default,
1509
- {
1510
- engine,
1511
- pageIndex: item.left,
1512
- availableWidth: columnWidth,
1513
- horizontalPadding: 8,
1514
- spacing: 20
1515
- }
1516
- ) }),
1517
- item.right !== null ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native3.View, { style: { width: columnWidth }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1518
- PageRenderer_default,
1519
- {
1520
- engine,
1521
- pageIndex: item.right,
1522
- availableWidth: columnWidth,
1523
- horizontalPadding: 8,
1524
- spacing: 20
1525
- }
1526
- ) }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native3.View, { style: { width: columnWidth } })
1527
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PageRenderer_default, { engine, pageIndex: item, spacing: 28 }),
2246
+ renderItem: ({ item }) => isDouble ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
2247
+ import_react_native3.View,
2248
+ {
2249
+ style: [styles3.row, { paddingHorizontal: horizontalPadding }],
2250
+ children: [
2251
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native3.View, { style: { width: columnWidth }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2252
+ PageRenderer_default,
2253
+ {
2254
+ engine,
2255
+ pageIndex: item.left,
2256
+ availableWidth: columnWidth,
2257
+ horizontalPadding: 8,
2258
+ spacing: DOUBLE_PAGE_SPACING
2259
+ }
2260
+ ) }),
2261
+ item.right !== null ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native3.View, { style: { width: columnWidth }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2262
+ PageRenderer_default,
2263
+ {
2264
+ engine,
2265
+ pageIndex: item.right,
2266
+ availableWidth: columnWidth,
2267
+ horizontalPadding: 8,
2268
+ spacing: DOUBLE_PAGE_SPACING
2269
+ }
2270
+ ) }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native3.View, { style: { width: columnWidth } })
2271
+ ]
2272
+ }
2273
+ ) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2274
+ PageRenderer_default,
2275
+ {
2276
+ engine,
2277
+ pageIndex: item,
2278
+ spacing: CONTINUOUS_PAGE_SPACING
2279
+ }
2280
+ ),
1528
2281
  onViewableItemsChanged,
1529
2282
  viewabilityConfig: { itemVisiblePercentThreshold: 60 },
1530
2283
  scrollEnabled: true,
1531
2284
  onScrollToIndexFailed: ({ index, averageItemLength }) => {
1532
- if (index < 0 || index >= pageCount) return;
1533
- const offset = Math.max(0, averageItemLength * index);
1534
- listRef.current?.scrollToOffset({ offset, animated: true });
2285
+ const dataLength = isDouble ? rows.length : pages.length;
2286
+ if (index < 0 || index >= dataLength) return;
2287
+ pendingScrollIndexRef.current = index;
2288
+ const offset = Math.max(0, getFallbackOffsetForIndex(index));
2289
+ listRef.current?.scrollToOffset({ offset, animated: false });
2290
+ if (!isDouble) {
2291
+ ensurePageDimensions(index);
2292
+ } else {
2293
+ const row = rows[index];
2294
+ if (row) {
2295
+ ensurePageDimensions(row.left);
2296
+ if (row.right !== null) {
2297
+ ensurePageDimensions(row.right);
2298
+ }
2299
+ }
2300
+ }
2301
+ scheduleScrollRetry("onScrollToIndexFailed");
2302
+ if (perfEnabled) {
2303
+ logPerfEvent("Viewer", "scrollToIndexFailed", {
2304
+ index,
2305
+ averageItemLength,
2306
+ fallbackOffset: offset,
2307
+ fallbackSource: "cached-item-layout",
2308
+ itemCount: dataLength,
2309
+ retryAttempt: pendingScrollAttemptsRef.current
2310
+ });
2311
+ }
1535
2312
  },
2313
+ onScroll: perfEnabled ? (event) => {
2314
+ const timestampValue = event.nativeEvent.timestamp;
2315
+ const timestamp = typeof timestampValue === "number" ? timestampValue : void 0;
2316
+ scrollMonitorRef.current.track(timestamp);
2317
+ } : void 0,
2318
+ onScrollBeginDrag: perfEnabled ? () => {
2319
+ scrollMonitorRef.current.begin("continuous.beginDrag");
2320
+ } : void 0,
2321
+ onMomentumScrollBegin: perfEnabled ? () => {
2322
+ scrollMonitorRef.current.begin("continuous.momentumBegin");
2323
+ } : void 0,
2324
+ onScrollEndDrag: perfEnabled ? () => {
2325
+ scrollMonitorRef.current.end("continuous.endDrag");
2326
+ sampleMemory("Viewer", "continuous.endDrag", { pageCount });
2327
+ } : void 0,
2328
+ onMomentumScrollEnd: perfEnabled ? () => {
2329
+ scrollMonitorRef.current.end("continuous.momentumEnd");
2330
+ sampleMemory("Viewer", "continuous.momentumEnd", { pageCount });
2331
+ } : void 0,
2332
+ scrollEventThrottle: perfEnabled ? 16 : void 0,
1536
2333
  showsVerticalScrollIndicator: false
1537
2334
  }
1538
2335
  ) });
@@ -1546,8 +2343,8 @@ var styles3 = import_react_native3.StyleSheet.create({
1546
2343
  backgroundColor: "#0f1115"
1547
2344
  },
1548
2345
  listContent: {
1549
- paddingTop: 18,
1550
- paddingBottom: 120
2346
+ paddingTop: LIST_TOP_PADDING,
2347
+ paddingBottom: LIST_BOTTOM_PADDING
1551
2348
  },
1552
2349
  singleContent: {
1553
2350
  paddingTop: 18,
@@ -2069,7 +2866,29 @@ var PageThumbnail = ({
2069
2866
  isActive && { borderColor: accentColor }
2070
2867
  ],
2071
2868
  children: [
2072
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native6.View, { onLayout: handleLayout, style: [styles6.thumbFrame, { width: frameWidth, height: frameHeight }], children: useNativePreview ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_engine_native2.PapyrusPageView, { ref: viewRef, style: styles6.thumbView }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native6.View, { style: [styles6.thumbFallback, isDark && styles6.thumbFallbackDark], children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native6.Text, { style: [styles6.thumbFallbackText, isDark && styles6.thumbFallbackTextDark], children: pageIndex + 1 }) }) }),
2869
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2870
+ import_react_native6.View,
2871
+ {
2872
+ onLayout: handleLayout,
2873
+ style: [styles6.thumbFrame, { width: frameWidth, height: frameHeight }],
2874
+ children: useNativePreview ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_engine_native2.PapyrusPageView, { ref: viewRef, style: styles6.thumbView }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2875
+ import_react_native6.View,
2876
+ {
2877
+ style: [styles6.thumbFallback, isDark && styles6.thumbFallbackDark],
2878
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2879
+ import_react_native6.Text,
2880
+ {
2881
+ style: [
2882
+ styles6.thumbFallbackText,
2883
+ isDark && styles6.thumbFallbackTextDark
2884
+ ],
2885
+ children: pageIndex + 1
2886
+ }
2887
+ )
2888
+ }
2889
+ )
2890
+ }
2891
+ ),
2073
2892
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native6.Text, { style: [styles6.thumbLabel, isDark && styles6.thumbLabelDark], children: pageIndex + 1 })
2074
2893
  ]
2075
2894
  }
@@ -2136,7 +2955,9 @@ var RightSheet = ({ engine }) => {
2136
2955
  locale,
2137
2956
  accentColor
2138
2957
  } = (0, import_core6.useViewerStore)();
2139
- const [pagesMode, setPagesMode] = (0, import_react5.useState)("thumbnails");
2958
+ const [pagesMode, setPagesMode] = (0, import_react5.useState)(
2959
+ "thumbnails"
2960
+ );
2140
2961
  const [query, setQuery] = (0, import_react5.useState)("");
2141
2962
  const [isSearching, setIsSearching] = (0, import_react5.useState)(false);
2142
2963
  const searchService = (0, import_react5.useMemo)(() => new import_core6.SearchService(engine), [engine]);
@@ -2144,6 +2965,10 @@ var RightSheet = ({ engine }) => {
2144
2965
  const accentSoft = withAlpha(accentColor, 0.2);
2145
2966
  const accentStrong = withAlpha(accentColor, 0.35);
2146
2967
  const t = getStrings(locale);
2968
+ const perfEnabled = isMobilePerfEnabled();
2969
+ const setStateBurstRef = (0, import_react5.useRef)(
2970
+ createBurstMonitor("RightSheet", "setDocumentState", 10, 800)
2971
+ );
2147
2972
  const sheetHeight = Math.min(640, import_react_native6.Dimensions.get("window").height * 0.72);
2148
2973
  const windowWidth = import_react_native6.Dimensions.get("window").width;
2149
2974
  const gridGutter = 12;
@@ -2152,24 +2977,76 @@ var RightSheet = ({ engine }) => {
2152
2977
  const frameWidth = cardWidth - 16;
2153
2978
  const frameHeight = frameWidth * 1.28;
2154
2979
  const renderTarget = engine.getRenderTargetType?.();
2155
- const hasNativePageView = Boolean(import_react_native6.UIManager.getViewManagerConfig?.("PapyrusPageView"));
2980
+ const hasNativePageView = Boolean(
2981
+ import_react_native6.UIManager.getViewManagerConfig?.("PapyrusPageView")
2982
+ );
2156
2983
  const useNativePreview = renderTarget !== "webview" && hasNativePageView;
2157
2984
  const closeSheet = () => toggleSidebarRight();
2985
+ const setDocumentStateTracked = (0, import_react5.useCallback)(
2986
+ (state, reason) => {
2987
+ if (perfEnabled) {
2988
+ setStateBurstRef.current({
2989
+ reason,
2990
+ keys: Object.keys(state).join(",")
2991
+ });
2992
+ }
2993
+ setDocumentState(state);
2994
+ },
2995
+ [perfEnabled, setDocumentState]
2996
+ );
2997
+ (0, import_react5.useEffect)(() => {
2998
+ if (!perfEnabled || !sidebarRightOpen) return;
2999
+ logPerfEvent("RightSheet", "open", {
3000
+ tab: sidebarRightTab,
3001
+ pageCount,
3002
+ currentPage
3003
+ });
3004
+ sampleMemory("RightSheet", "open", {
3005
+ tab: sidebarRightTab,
3006
+ pageCount
3007
+ });
3008
+ }, [perfEnabled, sidebarRightOpen]);
3009
+ (0, import_react5.useEffect)(() => {
3010
+ if (!perfEnabled || !sidebarRightOpen) return;
3011
+ return () => {
3012
+ logPerfEvent("RightSheet", "close");
3013
+ };
3014
+ }, [perfEnabled, sidebarRightOpen]);
3015
+ (0, import_react5.useEffect)(() => {
3016
+ if (!perfEnabled || !sidebarRightOpen || sidebarRightTab !== "pages" || pagesMode !== "thumbnails")
3017
+ return;
3018
+ sampleMemory("RightSheet", "thumbnails.open.start", { pageCount });
3019
+ const timeout = setTimeout(() => {
3020
+ sampleMemory("RightSheet", "thumbnails.open.steady", { pageCount });
3021
+ }, 1200);
3022
+ return () => clearTimeout(timeout);
3023
+ }, [pageCount, pagesMode, perfEnabled, sidebarRightOpen, sidebarRightTab]);
2158
3024
  const handleSearch = async () => {
2159
3025
  const trimmed = query.trim();
2160
3026
  if (!trimmed) {
2161
3027
  setSearch("", []);
2162
3028
  return;
2163
3029
  }
3030
+ const startedAt = perfEnabled ? perfNow() : 0;
2164
3031
  setIsSearching(true);
2165
3032
  try {
2166
3033
  const results = await searchService.search(trimmed);
2167
3034
  setSearch(trimmed, results);
3035
+ if (perfEnabled) {
3036
+ logPerfEvent("RightSheet", "search.completed", {
3037
+ queryLength: trimmed.length,
3038
+ results: results.length,
3039
+ durationMs: Math.round((perfNow() - startedAt) * 100) / 100
3040
+ });
3041
+ }
2168
3042
  } finally {
2169
3043
  setIsSearching(false);
2170
3044
  }
2171
3045
  };
2172
- const pages = (0, import_react5.useMemo)(() => Array.from({ length: pageCount }, (_, i) => i), [pageCount]);
3046
+ const pages = (0, import_react5.useMemo)(
3047
+ () => Array.from({ length: pageCount }, (_, i) => i),
3048
+ [pageCount]
3049
+ );
2173
3050
  const renderHighlightedSnippet = (text, isActive) => {
2174
3051
  const trimmedQuery = searchQuery.trim();
2175
3052
  if (trimmedQuery.length < 2) {
@@ -2203,7 +3080,10 @@ var RightSheet = ({ engine }) => {
2203
3080
  if (index > cursor) {
2204
3081
  parts.push({ text: text.slice(cursor, index), match: false });
2205
3082
  }
2206
- parts.push({ text: text.slice(index, index + trimmedQuery.length), match: true });
3083
+ parts.push({
3084
+ text: text.slice(index, index + trimmedQuery.length),
3085
+ match: true
3086
+ });
2207
3087
  cursor = index + trimmedQuery.length;
2208
3088
  }
2209
3089
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
@@ -2222,7 +3102,10 @@ var RightSheet = ({ engine }) => {
2222
3102
  part.match && styles6.matchText,
2223
3103
  part.match && isDark && styles6.matchTextDark,
2224
3104
  part.match && isActive && styles6.matchTextActive,
2225
- part.match && isActive && { backgroundColor: accentStrong, color: accentColor }
3105
+ part.match && isActive && {
3106
+ backgroundColor: accentStrong,
3107
+ color: accentColor
3108
+ }
2226
3109
  ],
2227
3110
  children: part.text
2228
3111
  },
@@ -2232,247 +3115,419 @@ var RightSheet = ({ engine }) => {
2232
3115
  );
2233
3116
  };
2234
3117
  if (!sidebarRightOpen) return null;
2235
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native6.Modal, { visible: true, transparent: true, animationType: "slide", onRequestClose: closeSheet, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native6.View, { style: styles6.modalRoot, children: [
2236
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native6.Pressable, { style: styles6.backdrop, onPress: closeSheet }),
2237
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native6.View, { style: [styles6.sheet, { height: sheetHeight }, isDark && styles6.sheetDark], children: [
2238
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native6.View, { style: [styles6.handle, isDark && styles6.handleDark] }),
2239
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native6.View, { style: styles6.tabs, children: ["pages", "search", "annotations"].map((tab) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2240
- import_react_native6.Pressable,
2241
- {
2242
- onPress: () => toggleSidebarRight(tab),
2243
- style: [
2244
- styles6.tabButton,
2245
- isDark && styles6.tabButtonDark,
2246
- sidebarRightTab === tab && styles6.tabButtonActive,
2247
- sidebarRightTab === tab && { backgroundColor: accentColor }
2248
- ],
2249
- children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2250
- import_react_native6.Text,
2251
- {
2252
- style: [
2253
- styles6.tabText,
2254
- isDark && styles6.tabTextDark,
2255
- sidebarRightTab === tab && styles6.tabTextActive
2256
- ],
2257
- children: tab === "pages" ? t.pages : tab === "search" ? t.search : t.notes
2258
- }
2259
- )
2260
- },
2261
- tab
2262
- )) }),
2263
- sidebarRightTab === "pages" ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native6.View, { style: styles6.pagesContent, children: [
2264
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native6.View, { style: styles6.pageHeader, children: [
2265
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native6.Text, { style: [styles6.pageStatus, isDark && styles6.pageStatusDark], children: [
2266
- t.page,
2267
- " ",
2268
- currentPage,
2269
- " / ",
2270
- pageCount
2271
- ] }),
2272
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native6.View, { style: [styles6.segmented, isDark && styles6.segmentedDark], children: [
2273
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2274
- import_react_native6.Pressable,
2275
- {
2276
- onPress: () => setPagesMode("thumbnails"),
2277
- style: [
2278
- styles6.segmentButton,
2279
- pagesMode === "thumbnails" && styles6.segmentButtonActive,
2280
- pagesMode === "thumbnails" && { backgroundColor: accentColor }
2281
- ],
2282
- children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2283
- import_react_native6.Text,
3118
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3119
+ import_react_native6.Modal,
3120
+ {
3121
+ visible: true,
3122
+ transparent: true,
3123
+ animationType: "slide",
3124
+ onRequestClose: closeSheet,
3125
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native6.View, { style: styles6.modalRoot, children: [
3126
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native6.Pressable, { style: styles6.backdrop, onPress: closeSheet }),
3127
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
3128
+ import_react_native6.View,
3129
+ {
3130
+ style: [
3131
+ styles6.sheet,
3132
+ { height: sheetHeight },
3133
+ isDark && styles6.sheetDark
3134
+ ],
3135
+ children: [
3136
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native6.View, { style: [styles6.handle, isDark && styles6.handleDark] }),
3137
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native6.View, { style: styles6.tabs, children: ["pages", "search", "annotations"].map((tab) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3138
+ import_react_native6.Pressable,
3139
+ {
3140
+ onPress: () => toggleSidebarRight(tab),
3141
+ style: [
3142
+ styles6.tabButton,
3143
+ isDark && styles6.tabButtonDark,
3144
+ sidebarRightTab === tab && styles6.tabButtonActive,
3145
+ sidebarRightTab === tab && { backgroundColor: accentColor }
3146
+ ],
3147
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3148
+ import_react_native6.Text,
3149
+ {
3150
+ style: [
3151
+ styles6.tabText,
3152
+ isDark && styles6.tabTextDark,
3153
+ sidebarRightTab === tab && styles6.tabTextActive
3154
+ ],
3155
+ children: tab === "pages" ? t.pages : tab === "search" ? t.search : t.notes
3156
+ }
3157
+ )
3158
+ },
3159
+ tab
3160
+ )) }),
3161
+ sidebarRightTab === "pages" ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native6.View, { style: styles6.pagesContent, children: [
3162
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native6.View, { style: styles6.pageHeader, children: [
3163
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
3164
+ import_react_native6.Text,
3165
+ {
3166
+ style: [styles6.pageStatus, isDark && styles6.pageStatusDark],
3167
+ children: [
3168
+ t.page,
3169
+ " ",
3170
+ currentPage,
3171
+ " / ",
3172
+ pageCount
3173
+ ]
3174
+ }
3175
+ ),
3176
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
3177
+ import_react_native6.View,
3178
+ {
3179
+ style: [styles6.segmented, isDark && styles6.segmentedDark],
3180
+ children: [
3181
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3182
+ import_react_native6.Pressable,
3183
+ {
3184
+ onPress: () => setPagesMode("thumbnails"),
3185
+ style: [
3186
+ styles6.segmentButton,
3187
+ pagesMode === "thumbnails" && styles6.segmentButtonActive,
3188
+ pagesMode === "thumbnails" && {
3189
+ backgroundColor: accentColor
3190
+ }
3191
+ ],
3192
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3193
+ import_react_native6.Text,
3194
+ {
3195
+ style: [
3196
+ styles6.segmentText,
3197
+ isDark && styles6.segmentTextDark,
3198
+ pagesMode === "thumbnails" && styles6.segmentTextActive
3199
+ ],
3200
+ children: t.pagesTab
3201
+ }
3202
+ )
3203
+ }
3204
+ ),
3205
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3206
+ import_react_native6.Pressable,
3207
+ {
3208
+ onPress: () => setPagesMode("summary"),
3209
+ style: [
3210
+ styles6.segmentButton,
3211
+ pagesMode === "summary" && styles6.segmentButtonActive,
3212
+ pagesMode === "summary" && {
3213
+ backgroundColor: accentColor
3214
+ }
3215
+ ],
3216
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3217
+ import_react_native6.Text,
3218
+ {
3219
+ style: [
3220
+ styles6.segmentText,
3221
+ isDark && styles6.segmentTextDark,
3222
+ pagesMode === "summary" && styles6.segmentTextActive
3223
+ ],
3224
+ children: t.summaryTab
3225
+ }
3226
+ )
3227
+ }
3228
+ )
3229
+ ]
3230
+ }
3231
+ )
3232
+ ] }),
3233
+ pagesMode === "thumbnails" ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3234
+ import_react_native6.FlatList,
2284
3235
  {
2285
- style: [
2286
- styles6.segmentText,
2287
- isDark && styles6.segmentTextDark,
2288
- pagesMode === "thumbnails" && styles6.segmentTextActive
2289
- ],
2290
- children: t.pagesTab
3236
+ data: pages,
3237
+ keyExtractor: (item) => `thumb-${item}`,
3238
+ numColumns: 2,
3239
+ contentContainerStyle: styles6.thumbGrid,
3240
+ columnWrapperStyle: styles6.thumbRow,
3241
+ showsVerticalScrollIndicator: false,
3242
+ initialNumToRender: 6,
3243
+ renderItem: ({ item }) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3244
+ PageThumbnail,
3245
+ {
3246
+ engine,
3247
+ pageIndex: item,
3248
+ isActive: item + 1 === currentPage,
3249
+ isDark,
3250
+ zoom,
3251
+ cardWidth,
3252
+ frameWidth,
3253
+ frameHeight,
3254
+ accentColor,
3255
+ useNativePreview,
3256
+ onPress: () => {
3257
+ engine.goToPage(item + 1);
3258
+ setDocumentStateTracked(
3259
+ { currentPage: item + 1 },
3260
+ "thumbnail.press"
3261
+ );
3262
+ triggerScrollToPage(item);
3263
+ closeSheet();
3264
+ }
3265
+ }
3266
+ )
2291
3267
  }
2292
- )
2293
- }
2294
- ),
2295
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2296
- import_react_native6.Pressable,
2297
- {
2298
- onPress: () => setPagesMode("summary"),
2299
- style: [
2300
- styles6.segmentButton,
2301
- pagesMode === "summary" && styles6.segmentButtonActive,
2302
- pagesMode === "summary" && { backgroundColor: accentColor }
2303
- ],
2304
- children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2305
- import_react_native6.Text,
3268
+ ) : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3269
+ import_react_native6.ScrollView,
2306
3270
  {
2307
- style: [
2308
- styles6.segmentText,
2309
- isDark && styles6.segmentTextDark,
2310
- pagesMode === "summary" && styles6.segmentTextActive
2311
- ],
2312
- children: t.summaryTab
3271
+ contentContainerStyle: styles6.summaryContent,
3272
+ showsVerticalScrollIndicator: false,
3273
+ children: outline.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3274
+ import_react_native6.Text,
3275
+ {
3276
+ style: [styles6.emptyText, isDark && styles6.emptyTextDark],
3277
+ children: t.noSummary
3278
+ }
3279
+ ) : outline.map((item, index) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3280
+ OutlineNode,
3281
+ {
3282
+ item,
3283
+ isDark,
3284
+ untitledLabel: t.untitled,
3285
+ onSelect: (pageIndex) => {
3286
+ engine.goToPage(pageIndex + 1);
3287
+ setDocumentStateTracked(
3288
+ { currentPage: pageIndex + 1 },
3289
+ "outline.select"
3290
+ );
3291
+ triggerScrollToPage(pageIndex);
3292
+ closeSheet();
3293
+ }
3294
+ },
3295
+ `${item.title}-${index}`
3296
+ ))
2313
3297
  }
2314
3298
  )
2315
- }
2316
- )
2317
- ] })
2318
- ] }),
2319
- pagesMode === "thumbnails" ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2320
- import_react_native6.FlatList,
2321
- {
2322
- data: pages,
2323
- keyExtractor: (item) => `thumb-${item}`,
2324
- numColumns: 2,
2325
- contentContainerStyle: styles6.thumbGrid,
2326
- columnWrapperStyle: styles6.thumbRow,
2327
- showsVerticalScrollIndicator: false,
2328
- initialNumToRender: 6,
2329
- renderItem: ({ item }) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2330
- PageThumbnail,
2331
- {
2332
- engine,
2333
- pageIndex: item,
2334
- isActive: item + 1 === currentPage,
2335
- isDark,
2336
- zoom,
2337
- cardWidth,
2338
- frameWidth,
2339
- frameHeight,
2340
- accentColor,
2341
- useNativePreview,
2342
- onPress: () => {
2343
- engine.goToPage(item + 1);
2344
- setDocumentState({ currentPage: item + 1 });
2345
- triggerScrollToPage(item);
2346
- closeSheet();
3299
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3300
+ import_react_native6.ScrollView,
3301
+ {
3302
+ contentContainerStyle: styles6.content,
3303
+ showsVerticalScrollIndicator: false,
3304
+ children: sidebarRightTab === "search" ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native6.View, { children: [
3305
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
3306
+ import_react_native6.View,
3307
+ {
3308
+ style: [styles6.searchBox, isDark && styles6.searchBoxDark],
3309
+ children: [
3310
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3311
+ import_react_native6.TextInput,
3312
+ {
3313
+ value: query,
3314
+ onChangeText: setQuery,
3315
+ placeholder: t.searchPlaceholder,
3316
+ placeholderTextColor: isDark ? "#9ca3af" : "#6b7280",
3317
+ style: [
3318
+ styles6.searchInput,
3319
+ isDark && styles6.searchInputDark
3320
+ ],
3321
+ onSubmitEditing: handleSearch,
3322
+ returnKeyType: "search"
3323
+ }
3324
+ ),
3325
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3326
+ import_react_native6.Pressable,
3327
+ {
3328
+ onPress: handleSearch,
3329
+ style: [
3330
+ styles6.searchButton,
3331
+ { backgroundColor: accentColor }
3332
+ ],
3333
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native6.Text, { style: styles6.searchButtonText, children: t.searchGo })
3334
+ }
3335
+ )
3336
+ ]
3337
+ }
3338
+ ),
3339
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native6.View, { style: styles6.searchMeta, children: [
3340
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
3341
+ import_react_native6.Text,
3342
+ {
3343
+ style: [
3344
+ styles6.searchCount,
3345
+ isDark && styles6.searchCountDark,
3346
+ { color: accentColor }
3347
+ ],
3348
+ children: [
3349
+ searchResults.length,
3350
+ " ",
3351
+ t.results
3352
+ ]
3353
+ }
3354
+ ),
3355
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native6.View, { style: styles6.searchNav, children: [
3356
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3357
+ import_react_native6.Pressable,
3358
+ {
3359
+ onPress: prevSearchResult,
3360
+ disabled: searchResults.length === 0,
3361
+ style: [
3362
+ styles6.searchNavButton,
3363
+ isDark && styles6.searchNavButtonDark,
3364
+ searchResults.length === 0 && styles6.searchNavButtonDisabled
3365
+ ],
3366
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3367
+ IconChevronLeft,
3368
+ {
3369
+ size: 14,
3370
+ color: isDark ? "#e5e7eb" : "#111827"
3371
+ }
3372
+ )
3373
+ }
3374
+ ),
3375
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3376
+ import_react_native6.Pressable,
3377
+ {
3378
+ onPress: nextSearchResult,
3379
+ disabled: searchResults.length === 0,
3380
+ style: [
3381
+ styles6.searchNavButton,
3382
+ isDark && styles6.searchNavButtonDark,
3383
+ searchResults.length === 0 && styles6.searchNavButtonDisabled
3384
+ ],
3385
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3386
+ IconChevronRight,
3387
+ {
3388
+ size: 14,
3389
+ color: isDark ? "#e5e7eb" : "#111827"
3390
+ }
3391
+ )
3392
+ }
3393
+ )
3394
+ ] })
3395
+ ] }),
3396
+ isSearching && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native6.View, { style: styles6.searchStatus, children: [
3397
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native6.ActivityIndicator, { size: "small", color: accentColor }),
3398
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3399
+ import_react_native6.Text,
3400
+ {
3401
+ style: [
3402
+ styles6.searchStatusText,
3403
+ isDark && styles6.searchStatusTextDark
3404
+ ],
3405
+ children: t.searching
3406
+ }
3407
+ )
3408
+ ] }),
3409
+ !isSearching && searchResults.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3410
+ import_react_native6.Text,
3411
+ {
3412
+ style: [styles6.emptyText, isDark && styles6.emptyTextDark],
3413
+ children: t.noResults
3414
+ }
3415
+ ),
3416
+ !isSearching && searchResults.map((res, idx) => {
3417
+ const isActive = idx === activeSearchIndex;
3418
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
3419
+ import_react_native6.Pressable,
3420
+ {
3421
+ onPress: () => {
3422
+ setDocumentStateTracked(
3423
+ { activeSearchIndex: idx },
3424
+ "searchResult.select"
3425
+ );
3426
+ triggerScrollToPage(res.pageIndex);
3427
+ closeSheet();
3428
+ },
3429
+ style: [
3430
+ styles6.resultCard,
3431
+ isDark && styles6.resultCardDark,
3432
+ isActive && styles6.resultCardActive,
3433
+ isActive && { borderColor: accentColor }
3434
+ ],
3435
+ children: [
3436
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
3437
+ import_react_native6.Text,
3438
+ {
3439
+ style: [
3440
+ styles6.resultPage,
3441
+ isDark && styles6.resultPageDark,
3442
+ { color: accentColor }
3443
+ ],
3444
+ children: [
3445
+ t.page,
3446
+ " ",
3447
+ res.pageIndex + 1
3448
+ ]
3449
+ }
3450
+ ),
3451
+ renderHighlightedSnippet(res.text, isActive)
3452
+ ]
3453
+ },
3454
+ `${res.pageIndex}-${idx}`
3455
+ );
3456
+ })
3457
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native6.View, { children: annotations.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3458
+ import_react_native6.Text,
3459
+ {
3460
+ style: [styles6.emptyText, isDark && styles6.emptyTextDark],
3461
+ children: t.noAnnotations
3462
+ }
3463
+ ) : annotations.map((ann) => /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
3464
+ import_react_native6.Pressable,
3465
+ {
3466
+ onPress: () => {
3467
+ setSelectedAnnotation(ann.id);
3468
+ triggerScrollToPage(ann.pageIndex);
3469
+ closeSheet();
3470
+ },
3471
+ style: [styles6.noteCard, isDark && styles6.noteCardDark],
3472
+ children: [
3473
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native6.View, { style: styles6.noteHeader, children: [
3474
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3475
+ import_react_native6.View,
3476
+ {
3477
+ style: [
3478
+ styles6.noteDot,
3479
+ { backgroundColor: ann.color }
3480
+ ]
3481
+ }
3482
+ ),
3483
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
3484
+ import_react_native6.Text,
3485
+ {
3486
+ style: [
3487
+ styles6.noteTitle,
3488
+ isDark && styles6.noteTitleDark
3489
+ ],
3490
+ children: [
3491
+ t.page,
3492
+ " ",
3493
+ ann.pageIndex + 1
3494
+ ]
3495
+ }
3496
+ )
3497
+ ] }),
3498
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3499
+ import_react_native6.Text,
3500
+ {
3501
+ style: [
3502
+ styles6.noteType,
3503
+ isDark && styles6.noteTypeDark,
3504
+ { color: accentColor }
3505
+ ],
3506
+ children: ann.type === "comment" || ann.type === "text" ? t.note.toUpperCase() : ann.type.toUpperCase()
3507
+ }
3508
+ ),
3509
+ ann.content ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3510
+ import_react_native6.Text,
3511
+ {
3512
+ style: [
3513
+ styles6.noteContent,
3514
+ isDark && styles6.noteContentDark
3515
+ ],
3516
+ children: ann.content
3517
+ }
3518
+ ) : null
3519
+ ]
3520
+ },
3521
+ ann.id
3522
+ )) })
2347
3523
  }
2348
- }
2349
- )
3524
+ )
3525
+ ]
2350
3526
  }
2351
- ) : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native6.ScrollView, { contentContainerStyle: styles6.summaryContent, showsVerticalScrollIndicator: false, children: outline.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native6.Text, { style: [styles6.emptyText, isDark && styles6.emptyTextDark], children: t.noSummary }) : outline.map((item, index) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2352
- OutlineNode,
2353
- {
2354
- item,
2355
- isDark,
2356
- untitledLabel: t.untitled,
2357
- onSelect: (pageIndex) => {
2358
- engine.goToPage(pageIndex + 1);
2359
- setDocumentState({ currentPage: pageIndex + 1 });
2360
- triggerScrollToPage(pageIndex);
2361
- closeSheet();
2362
- }
2363
- },
2364
- `${item.title}-${index}`
2365
- )) })
2366
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native6.ScrollView, { contentContainerStyle: styles6.content, showsVerticalScrollIndicator: false, children: sidebarRightTab === "search" ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native6.View, { children: [
2367
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native6.View, { style: [styles6.searchBox, isDark && styles6.searchBoxDark], children: [
2368
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2369
- import_react_native6.TextInput,
2370
- {
2371
- value: query,
2372
- onChangeText: setQuery,
2373
- placeholder: t.searchPlaceholder,
2374
- placeholderTextColor: isDark ? "#9ca3af" : "#6b7280",
2375
- style: [styles6.searchInput, isDark && styles6.searchInputDark],
2376
- onSubmitEditing: handleSearch,
2377
- returnKeyType: "search"
2378
- }
2379
- ),
2380
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native6.Pressable, { onPress: handleSearch, style: [styles6.searchButton, { backgroundColor: accentColor }], children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native6.Text, { style: styles6.searchButtonText, children: t.searchGo }) })
2381
- ] }),
2382
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native6.View, { style: styles6.searchMeta, children: [
2383
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native6.Text, { style: [styles6.searchCount, isDark && styles6.searchCountDark, { color: accentColor }], children: [
2384
- searchResults.length,
2385
- " ",
2386
- t.results
2387
- ] }),
2388
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native6.View, { style: styles6.searchNav, children: [
2389
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2390
- import_react_native6.Pressable,
2391
- {
2392
- onPress: prevSearchResult,
2393
- disabled: searchResults.length === 0,
2394
- style: [
2395
- styles6.searchNavButton,
2396
- isDark && styles6.searchNavButtonDark,
2397
- searchResults.length === 0 && styles6.searchNavButtonDisabled
2398
- ],
2399
- children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(IconChevronLeft, { size: 14, color: isDark ? "#e5e7eb" : "#111827" })
2400
- }
2401
- ),
2402
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2403
- import_react_native6.Pressable,
2404
- {
2405
- onPress: nextSearchResult,
2406
- disabled: searchResults.length === 0,
2407
- style: [
2408
- styles6.searchNavButton,
2409
- isDark && styles6.searchNavButtonDark,
2410
- searchResults.length === 0 && styles6.searchNavButtonDisabled
2411
- ],
2412
- children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(IconChevronRight, { size: 14, color: isDark ? "#e5e7eb" : "#111827" })
2413
- }
2414
- )
2415
- ] })
2416
- ] }),
2417
- isSearching && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native6.View, { style: styles6.searchStatus, children: [
2418
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native6.ActivityIndicator, { size: "small", color: accentColor }),
2419
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native6.Text, { style: [styles6.searchStatusText, isDark && styles6.searchStatusTextDark], children: t.searching })
2420
- ] }),
2421
- !isSearching && searchResults.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native6.Text, { style: [styles6.emptyText, isDark && styles6.emptyTextDark], children: t.noResults }),
2422
- !isSearching && searchResults.map((res, idx) => {
2423
- const isActive = idx === activeSearchIndex;
2424
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
2425
- import_react_native6.Pressable,
2426
- {
2427
- onPress: () => {
2428
- setDocumentState({ activeSearchIndex: idx });
2429
- triggerScrollToPage(res.pageIndex);
2430
- closeSheet();
2431
- },
2432
- style: [
2433
- styles6.resultCard,
2434
- isDark && styles6.resultCardDark,
2435
- isActive && styles6.resultCardActive,
2436
- isActive && { borderColor: accentColor }
2437
- ],
2438
- children: [
2439
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native6.Text, { style: [styles6.resultPage, isDark && styles6.resultPageDark, { color: accentColor }], children: [
2440
- t.page,
2441
- " ",
2442
- res.pageIndex + 1
2443
- ] }),
2444
- renderHighlightedSnippet(res.text, isActive)
2445
- ]
2446
- },
2447
- `${res.pageIndex}-${idx}`
2448
- );
2449
- })
2450
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native6.View, { children: annotations.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native6.Text, { style: [styles6.emptyText, isDark && styles6.emptyTextDark], children: t.noAnnotations }) : annotations.map((ann) => /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
2451
- import_react_native6.Pressable,
2452
- {
2453
- onPress: () => {
2454
- setSelectedAnnotation(ann.id);
2455
- triggerScrollToPage(ann.pageIndex);
2456
- closeSheet();
2457
- },
2458
- style: [styles6.noteCard, isDark && styles6.noteCardDark],
2459
- children: [
2460
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native6.View, { style: styles6.noteHeader, children: [
2461
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native6.View, { style: [styles6.noteDot, { backgroundColor: ann.color }] }),
2462
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native6.Text, { style: [styles6.noteTitle, isDark && styles6.noteTitleDark], children: [
2463
- t.page,
2464
- " ",
2465
- ann.pageIndex + 1
2466
- ] })
2467
- ] }),
2468
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native6.Text, { style: [styles6.noteType, isDark && styles6.noteTypeDark, { color: accentColor }], children: ann.type === "comment" || ann.type === "text" ? t.note.toUpperCase() : ann.type.toUpperCase() }),
2469
- ann.content ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native6.Text, { style: [styles6.noteContent, isDark && styles6.noteContentDark], children: ann.content }) : null
2470
- ]
2471
- },
2472
- ann.id
2473
- )) }) })
2474
- ] })
2475
- ] }) });
3527
+ )
3528
+ ] })
3529
+ }
3530
+ );
2476
3531
  };
2477
3532
  var styles6 = import_react_native6.StyleSheet.create({
2478
3533
  modalRoot: {