@papyrus-sdk/ui-react-native 0.2.26 → 0.2.27

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
@@ -25279,6 +25279,12 @@ var resolvePageTapChromeVisibility = ({
25279
25279
  return !chromeVisible;
25280
25280
  };
25281
25281
 
25282
+ // components/selectionContentInteraction.ts
25283
+ var shouldDismissSelectionOnContentInteraction = ({
25284
+ selectionActive,
25285
+ interaction
25286
+ }) => selectionActive && (interaction === "tap" || interaction === "scroll");
25287
+
25282
25288
  // components/DedicatedAndroidPdfViewer.tsx
25283
25289
  import { Fragment, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
25284
25290
  var TEXT_MARKUP_TOOLS2 = /* @__PURE__ */ new Set(["highlight", "underline", "squiggly", "strikeout"]);
@@ -25459,6 +25465,13 @@ function DedicatedAndroidPdfViewer({
25459
25465
  setSelectedAnnotation(event.nativeEvent.id);
25460
25466
  },
25461
25467
  onTap: () => {
25468
+ if (shouldDismissSelectionOnContentInteraction({
25469
+ selectionActive: selectionRef.current !== null,
25470
+ interaction: "tap"
25471
+ })) {
25472
+ setSelection(null);
25473
+ selectionRef.current = null;
25474
+ }
25462
25475
  const nextVisible = resolvePageTapChromeVisibility({
25463
25476
  chromeVisible: chromeVisibleRef.current,
25464
25477
  selectionActive: selectionRef.current !== null,
@@ -25485,66 +25498,61 @@ function DedicatedAndroidPdfViewer({
25485
25498
  }
25486
25499
  },
25487
25500
  onScroll: (event) => {
25501
+ if (shouldDismissSelectionOnContentInteraction({
25502
+ selectionActive: selectionRef.current !== null,
25503
+ interaction: "scroll"
25504
+ })) {
25505
+ setSelection(null);
25506
+ selectionRef.current = null;
25507
+ }
25488
25508
  trackMobileChromeByOffset(event.nativeEvent.offsetY, "native.scroll");
25489
25509
  }
25490
25510
  }
25491
25511
  ),
25492
- selection && /* @__PURE__ */ jsxs4(Fragment, { children: [
25512
+ selection && /* @__PURE__ */ jsx5(Fragment, { children: /* @__PURE__ */ jsx5(View3, { style: styles3.selectionToolbar, pointerEvents: "box-none", children: /* @__PURE__ */ jsxs4(View3, { style: styles3.toolbarContent, children: [
25493
25513
  /* @__PURE__ */ jsx5(
25494
25514
  Pressable2,
25495
25515
  {
25496
- style: styles3.overlay,
25497
25516
  onPress: () => {
25498
- setSelection(null);
25499
- selectionRef.current = null;
25500
- }
25517
+ void copySelection();
25518
+ },
25519
+ style: styles3.toolbarButton,
25520
+ accessibilityRole: "button",
25521
+ accessibilityLabel: "Copy selected text",
25522
+ children: /* @__PURE__ */ jsx5(IconCopy, { size: 20, color: "#fff", strokeWidth: 2 })
25501
25523
  }
25502
25524
  ),
25503
- /* @__PURE__ */ jsx5(View3, { style: styles3.selectionToolbar, pointerEvents: "box-none", children: /* @__PURE__ */ jsxs4(View3, { style: styles3.toolbarContent, children: [
25504
- /* @__PURE__ */ jsx5(
25505
- Pressable2,
25506
- {
25507
- onPress: () => {
25508
- void copySelection();
25509
- },
25510
- style: styles3.toolbarButton,
25511
- accessibilityRole: "button",
25512
- accessibilityLabel: "Copy selected text",
25513
- children: /* @__PURE__ */ jsx5(IconCopy, { size: 20, color: "#fff", strokeWidth: 2 })
25514
- }
25515
- ),
25516
- /* @__PURE__ */ jsx5(
25517
- Pressable2,
25518
- {
25519
- onPress: () => {
25520
- applySelection("highlight");
25521
- },
25522
- style: styles3.toolbarButton,
25523
- children: /* @__PURE__ */ jsx5(IconHighlight, { size: 22, color: "#fbbf24" })
25524
- }
25525
- ),
25526
- /* @__PURE__ */ jsx5(
25527
- Pressable2,
25528
- {
25529
- onPress: () => {
25530
- applySelection("underline");
25531
- },
25532
- style: styles3.toolbarButton,
25533
- children: /* @__PURE__ */ jsx5(IconUnderline, { size: 22, color: "#60a5fa" })
25534
- }
25535
- ),
25536
- /* @__PURE__ */ jsx5(
25537
- Pressable2,
25538
- {
25539
- onPress: () => {
25540
- applySelection("comment");
25541
- },
25542
- style: styles3.toolbarButton,
25543
- children: /* @__PURE__ */ jsx5(IconCommentBubble, { size: 22, color: "#fff" })
25544
- }
25545
- )
25546
- ] }) })
25547
- ] })
25525
+ /* @__PURE__ */ jsx5(
25526
+ Pressable2,
25527
+ {
25528
+ onPress: () => {
25529
+ applySelection("highlight");
25530
+ },
25531
+ style: styles3.toolbarButton,
25532
+ children: /* @__PURE__ */ jsx5(IconHighlight, { size: 22, color: "#fbbf24" })
25533
+ }
25534
+ ),
25535
+ /* @__PURE__ */ jsx5(
25536
+ Pressable2,
25537
+ {
25538
+ onPress: () => {
25539
+ applySelection("underline");
25540
+ },
25541
+ style: styles3.toolbarButton,
25542
+ children: /* @__PURE__ */ jsx5(IconUnderline, { size: 22, color: "#60a5fa" })
25543
+ }
25544
+ ),
25545
+ /* @__PURE__ */ jsx5(
25546
+ Pressable2,
25547
+ {
25548
+ onPress: () => {
25549
+ applySelection("comment");
25550
+ },
25551
+ style: styles3.toolbarButton,
25552
+ children: /* @__PURE__ */ jsx5(IconCommentBubble, { size: 22, color: "#fff" })
25553
+ }
25554
+ )
25555
+ ] }) }) })
25548
25556
  ] });
25549
25557
  }
25550
25558
  var styles3 = StyleSheet3.create({
@@ -25584,10 +25592,6 @@ var styles3 = StyleSheet3.create({
25584
25592
  alignItems: "center",
25585
25593
  justifyContent: "center"
25586
25594
  },
25587
- overlay: {
25588
- ...StyleSheet3.absoluteFillObject,
25589
- zIndex: 40
25590
- },
25591
25595
  toolbarButtonText: {
25592
25596
  color: "#fff",
25593
25597
  fontSize: 11,
@@ -25742,6 +25746,87 @@ function createScrollPerfSession({
25742
25746
  return { begin, track, end };
25743
25747
  }
25744
25748
 
25749
+ // components/pageScrubberModel.ts
25750
+ var shouldRenderPageScrubber = ({
25751
+ mobileChromeVisible,
25752
+ mobileProgressPillVisible,
25753
+ isScrubbing
25754
+ }) => mobileProgressPillVisible && (mobileChromeVisible || isScrubbing);
25755
+ var shouldEnableViewerScrollForPageScrub = ({
25756
+ viewerScrollEnabled,
25757
+ isScrubbing
25758
+ }) => viewerScrollEnabled && !isScrubbing;
25759
+ var resolvePageScrubberTouchPolicy = () => ({
25760
+ responderTarget: "pill",
25761
+ claimOnStart: true,
25762
+ captureOnStart: true,
25763
+ captureOnMove: true,
25764
+ childConsumesTouch: false
25765
+ });
25766
+ var resolvePageScrubberReleaseAction = ({
25767
+ hasMoved,
25768
+ pendingPage
25769
+ }) => hasMoved && pendingPage !== null ? { kind: "navigate", page: pendingPage } : { kind: "open" };
25770
+ var createPageScrubberNavigationController = (onRelease) => {
25771
+ let active = false;
25772
+ let pendingPage = null;
25773
+ return {
25774
+ begin: () => {
25775
+ active = true;
25776
+ pendingPage = null;
25777
+ },
25778
+ update: (page) => {
25779
+ if (active) pendingPage = page;
25780
+ },
25781
+ release: () => {
25782
+ active = false;
25783
+ const page = pendingPage;
25784
+ pendingPage = null;
25785
+ if (page !== null) onRelease(page);
25786
+ return page;
25787
+ },
25788
+ cancel: () => {
25789
+ active = false;
25790
+ pendingPage = null;
25791
+ }
25792
+ };
25793
+ };
25794
+ var resolvePageScrubberThumbTopFromPosition = ({
25795
+ position,
25796
+ trackHeight,
25797
+ thumbHeight
25798
+ }) => Math.max(0, Math.min(trackHeight - thumbHeight, position - thumbHeight / 2));
25799
+ var resolvePageScrubberThumbTop = ({
25800
+ currentPage,
25801
+ trackHeight,
25802
+ thumbHeight,
25803
+ pageCount
25804
+ }) => {
25805
+ if (pageCount <= 1) return 0;
25806
+ const travel = Math.max(0, trackHeight - thumbHeight);
25807
+ const ratio = Math.max(
25808
+ 0,
25809
+ Math.min(1, (currentPage - 1) / Math.max(1, pageCount - 1))
25810
+ );
25811
+ return ratio * travel;
25812
+ };
25813
+ var resolvePageScrubberPage = ({
25814
+ position,
25815
+ trackHeight,
25816
+ thumbHeight,
25817
+ pageCount
25818
+ }) => {
25819
+ if (!Number.isFinite(pageCount) || pageCount <= 0) return null;
25820
+ if (pageCount === 1) return 1;
25821
+ const travel = Math.max(0, trackHeight - thumbHeight);
25822
+ const thumbTop = Math.max(0, Math.min(travel, position - thumbHeight / 2));
25823
+ const ratio = travel === 0 ? 0 : thumbTop / travel;
25824
+ return Math.max(
25825
+ 1,
25826
+ Math.min(pageCount, Math.round(ratio * (pageCount - 1)) + 1)
25827
+ );
25828
+ };
25829
+
25745
25830
  // components/viewerNavigation.ts
25746
25831
  var resolveViewerScrollTarget = (pageIndex, isDouble) => ({
25747
25832
  pageIndex,
@@ -25801,7 +25886,8 @@ var Viewer = ({
25801
25886
  maxToRenderPerBatch,
25802
25887
  removeClippedSubviews,
25803
25888
  useDedicatedAndroidPdfViewer,
25804
- viewerMode
25889
+ viewerMode,
25890
+ pageScrubActive = false
25805
25891
  }) => {
25806
25892
  const pageCount = useViewerStore4((state) => state.pageCount);
25807
25893
  const currentPage = useViewerStore4((state) => state.currentPage);
@@ -25876,6 +25962,8 @@ var Viewer = ({
25876
25962
  const selectionDragActiveRef = useRef4(false);
25877
25963
  const [gestureScrollLockActive, setGestureScrollLockActive] = useState4(false);
25878
25964
  const gestureScrollLockActiveRef = useRef4(false);
25965
+ const pageScrubActiveRef = useRef4(pageScrubActive);
25966
+ pageScrubActiveRef.current = pageScrubActive;
25879
25967
  const pinchPreviewScale = useRef4(new Animated.Value(1)).current;
25880
25968
  const pinchPreviewFocalX = useRef4(new Animated.Value(0)).current;
25881
25969
  const pinchPreviewFocalY = useRef4(new Animated.Value(0)).current;
@@ -26390,16 +26478,20 @@ var Viewer = ({
26390
26478
  selectionDragActive,
26391
26479
  gestureScrollLockActive
26392
26480
  });
26481
+ const pageScrubResolvedScrollEnabled = shouldEnableViewerScrollForPageScrub({
26482
+ viewerScrollEnabled: resolvedViewerScrollEnabled,
26483
+ isScrubbing: pageScrubActive
26484
+ });
26393
26485
  const setViewerScrollEnabledNative = useCallback3((enabled) => {
26394
26486
  const scrollNode = listRef.current;
26395
26487
  scrollNode?.setNativeProps?.({ scrollEnabled: enabled });
26396
26488
  }, []);
26397
26489
  const syncViewerScrollEnabled = useCallback3(
26398
- (nextSelectionDragActive = selectionDragActiveRef.current, nextGestureScrollLockActive = gestureScrollLockActiveRef.current) => {
26490
+ (nextSelectionDragActive = selectionDragActiveRef.current, nextGestureScrollLockActive = gestureScrollLockActiveRef.current, nextPageScrubActive = pageScrubActiveRef.current) => {
26399
26491
  setViewerScrollEnabledNative(
26400
26492
  shouldEnableViewerScroll({
26401
26493
  selectionDragActive: nextSelectionDragActive,
26402
- gestureScrollLockActive: nextGestureScrollLockActive
26494
+ gestureScrollLockActive: nextGestureScrollLockActive || nextPageScrubActive
26403
26495
  })
26404
26496
  );
26405
26497
  },
@@ -26672,6 +26764,13 @@ var Viewer = ({
26672
26764
  gestureScrollLockActiveRef.current
26673
26765
  );
26674
26766
  }, [selectionDragActive, syncViewerScrollEnabled]);
26767
+ useEffect4(() => {
26768
+ syncViewerScrollEnabled(
26769
+ selectionDragActiveRef.current,
26770
+ gestureScrollLockActiveRef.current,
26771
+ pageScrubActive
26772
+ );
26773
+ }, [pageScrubActive, syncViewerScrollEnabled]);
26675
26774
  useEffect4(() => {
26676
26775
  const pendingRestore = pendingPinchAnchorRestoreRef.current;
26677
26776
  if (!pendingRestore) return;
@@ -27241,7 +27340,7 @@ var Viewer = ({
27241
27340
  {
27242
27341
  ref: horizontalScrollRef,
27243
27342
  horizontal: true,
27244
- scrollEnabled: !gestureScrollLockActive && documentSurfaceWidth > windowWidth,
27343
+ scrollEnabled: !gestureScrollLockActive && !pageScrubActive && documentSurfaceWidth > windowWidth,
27245
27344
  showsHorizontalScrollIndicator: false,
27246
27345
  onScroll: (event) => {
27247
27346
  horizontalScrollOffsetRef.current = event.nativeEvent.contentOffset?.x ?? 0;
@@ -27257,7 +27356,7 @@ var Viewer = ({
27257
27356
  style: { width: documentSurfaceWidth },
27258
27357
  contentContainerStyle: styles4.singleContent,
27259
27358
  showsVerticalScrollIndicator: false,
27260
- scrollEnabled: resolvedViewerScrollEnabled,
27359
+ scrollEnabled: pageScrubResolvedScrollEnabled,
27261
27360
  onLayout: () => captureViewerFrame(listRef.current),
27262
27361
  onContentSizeChange: (_, height) => {
27263
27362
  viewerContentHeightRef.current = height;
@@ -27330,7 +27429,7 @@ var Viewer = ({
27330
27429
  {
27331
27430
  ref: horizontalScrollRef,
27332
27431
  horizontal: true,
27333
- scrollEnabled: !gestureScrollLockActive && documentSurfaceWidth > windowWidth,
27432
+ scrollEnabled: !gestureScrollLockActive && !pageScrubActive && documentSurfaceWidth > windowWidth,
27334
27433
  showsHorizontalScrollIndicator: false,
27335
27434
  onScroll: (event) => {
27336
27435
  horizontalScrollOffsetRef.current = event.nativeEvent.contentOffset?.x ?? 0;
@@ -27353,7 +27452,7 @@ var Viewer = ({
27353
27452
  renderItem,
27354
27453
  onViewableItemsChanged,
27355
27454
  viewabilityConfig: { itemVisiblePercentThreshold: 60 },
27356
- scrollEnabled: resolvedViewerScrollEnabled,
27455
+ scrollEnabled: pageScrubResolvedScrollEnabled,
27357
27456
  onLayout: () => {
27358
27457
  captureViewerFrame(listRef.current);
27359
27458
  restoreOrientationScrollOffset();
@@ -29281,6 +29380,11 @@ var resolveRightSheetHeight = ({
29281
29380
  }) => showingNotes ? Math.min(440, windowHeight * 0.56) : Math.min(640, windowHeight * 0.72);
29282
29381
  var supportsPageThumbnails = (documentType) => documentType !== "text" && documentType !== "epub";
29283
29382
 
29383
+ // components/thumbnailClipping.ts
29384
+ var shouldRemoveThumbnailClipping = ({
29385
+ useNativePreview
29386
+ }) => !useNativePreview;
29387
+
29284
29388
  // components/RightSheet.tsx
29285
29389
  import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
29286
29390
  var THUMBNAILS_INITIAL_NUM_TO_RENDER = 4;
@@ -29796,7 +29900,9 @@ var RightSheet = ({
29796
29900
  windowSize: THUMBNAILS_WINDOW_SIZE,
29797
29901
  maxToRenderPerBatch: THUMBNAILS_MAX_TO_RENDER_PER_BATCH,
29798
29902
  updateCellsBatchingPeriod: THUMBNAILS_UPDATE_CELLS_BATCHING_PERIOD,
29799
- removeClippedSubviews: true,
29903
+ removeClippedSubviews: shouldRemoveThumbnailClipping({
29904
+ useNativePreview
29905
+ }),
29800
29906
  viewabilityConfig: { itemVisiblePercentThreshold: 20 },
29801
29907
  onViewableItemsChanged: onThumbnailsViewableItemsChanged,
29802
29908
  renderItem: renderThumbnailItem
@@ -31390,7 +31496,7 @@ var styles13 = StyleSheet13.create({
31390
31496
  var CoverPreview_default = CoverPreview;
31391
31497
 
31392
31498
  // components/ReadingShell.tsx
31393
- import React16, { useState as useState12 } from "react";
31499
+ import React16, { useState as useState13 } from "react";
31394
31500
  import { Keyboard as Keyboard2, View as View19, StyleSheet as StyleSheet20 } from "react-native";
31395
31501
  import { useViewerStore as useViewerStore17 } from "@papyrus-sdk/core";
31396
31502
 
@@ -31754,25 +31860,24 @@ var styles16 = StyleSheet16.create({
31754
31860
  });
31755
31861
 
31756
31862
  // components/ProgressPill.tsx
31757
- import { useMemo as useMemo7 } from "react";
31758
- import { Pressable as Pressable12, StyleSheet as StyleSheet17, Text as Text12, View as View17 } from "react-native";
31863
+ import { useEffect as useEffect10, useMemo as useMemo7, useRef as useRef8, useState as useState11 } from "react";
31864
+ import {
31865
+ Animated as Animated2,
31866
+ PanResponder as PanResponder2,
31867
+ StyleSheet as StyleSheet17,
31868
+ Text as Text12,
31869
+ View as View17,
31870
+ useWindowDimensions as useWindowDimensions6
31871
+ } from "react-native";
31759
31872
  import { useViewerStore as useViewerStore14 } from "@papyrus-sdk/core";
31760
-
31761
- // components/progressPillInteraction.ts
31762
- var getProgressPillInteraction = (onPress, onLongPress) => ({
31763
- onPress,
31764
- onLongPress,
31765
- accessibilityLabel: "Open document navigation",
31766
- accessibilityRole: "button"
31767
- });
31768
-
31769
- // components/ProgressPill.tsx
31770
31873
  import { jsx as jsx20, jsxs as jsxs17 } from "react/jsx-runtime";
31771
31874
  var clampPercent = (value) => Math.max(0, Math.min(100, Math.round(value)));
31772
31875
  function ProgressPill({
31773
31876
  documentType,
31774
31877
  onPress,
31775
- onOpenPageJump
31878
+ onOpenPageJump,
31879
+ onNavigateToPage,
31880
+ onScrubbingChange
31776
31881
  }) {
31777
31882
  const {
31778
31883
  currentPage,
@@ -31784,18 +31889,193 @@ function ProgressPill({
31784
31889
  } = useViewerStore14();
31785
31890
  const isDark = uiTheme === "dark";
31786
31891
  const offsets = resolveMobileChromeOffsets(usePapyrusSafeAreaInsets());
31892
+ const { height: windowHeight } = useWindowDimensions6();
31893
+ const trackHeight = Math.max(
31894
+ 180,
31895
+ Math.min(520, windowHeight - offsets.progress - offsets.bottom - 132)
31896
+ );
31897
+ const thumbHeight = 44;
31898
+ const trackYRef = useRef8(0);
31899
+ const scrubberRef = useRef8(null);
31900
+ const currentPageRef = useRef8(currentPage);
31901
+ currentPageRef.current = currentPage;
31902
+ const isScrubbingRef = useRef8(false);
31903
+ const hasMovedRef = useRef8(false);
31904
+ const longPressTriggeredRef = useRef8(false);
31905
+ const longPressTimerRef = useRef8(null);
31906
+ const pendingReleasedScrubPageRef = useRef8(null);
31907
+ const [scrubPreviewPage, setScrubPreviewPage] = useState11(null);
31908
+ const onNavigateToPageRef = useRef8(onNavigateToPage);
31909
+ const onPressRef = useRef8(onPress);
31910
+ const onOpenPageJumpRef = useRef8(onOpenPageJump);
31911
+ const onScrubbingChangeRef = useRef8(onScrubbingChange);
31912
+ onScrubbingChangeRef.current = onScrubbingChange;
31913
+ const scrubToPositionRef = useRef8(() => {
31914
+ });
31915
+ const scrubNavigationControllerRef = useRef8(
31916
+ createPageScrubberNavigationController((page) => {
31917
+ onNavigateToPageRef.current?.(page);
31918
+ })
31919
+ );
31920
+ const scrubberResponderRef = useRef8(null);
31921
+ const touchPolicy = resolvePageScrubberTouchPolicy();
31922
+ const displayedPage = scrubPreviewPage ?? currentPage;
31787
31923
  const label = useMemo7(() => {
31788
31924
  const total = Math.max(pageCount, 1);
31789
- const percent = clampPercent(currentPage / total * 100);
31925
+ const percent = clampPercent(displayedPage / total * 100);
31790
31926
  if (documentType === "pdf") {
31791
- return `${currentPage}/${pageCount || 0}`;
31927
+ return `${displayedPage}/${pageCount || 0}`;
31792
31928
  }
31793
31929
  if (documentType === "epub") {
31794
- return `Cap. ${currentPage} \xB7 ${percent}%`;
31930
+ return `Cap. ${displayedPage} \xB7 ${percent}%`;
31795
31931
  }
31796
31932
  return `${percent}%`;
31797
- }, [currentPage, documentType, pageCount]);
31798
- if (!mobileChromeVisible || !mobileProgressPillVisible) return null;
31933
+ }, [displayedPage, documentType, pageCount]);
31934
+ const thumbTop = resolvePageScrubberThumbTop({
31935
+ currentPage,
31936
+ trackHeight,
31937
+ thumbHeight,
31938
+ pageCount
31939
+ });
31940
+ const thumbTopRef = useRef8(new Animated2.Value(thumbTop));
31941
+ const thumbTopSnapshotRef = useRef8(thumbTop);
31942
+ thumbTopSnapshotRef.current = thumbTop;
31943
+ const scrubberMetricsRef = useRef8({ trackHeight, thumbHeight, pageCount });
31944
+ scrubberMetricsRef.current = { trackHeight, thumbHeight, pageCount };
31945
+ useEffect10(() => {
31946
+ onNavigateToPageRef.current = onNavigateToPage;
31947
+ }, [onNavigateToPage]);
31948
+ useEffect10(() => {
31949
+ onPressRef.current = onPress;
31950
+ onOpenPageJumpRef.current = onOpenPageJump;
31951
+ }, [onOpenPageJump, onPress]);
31952
+ useEffect10(() => {
31953
+ const pendingPage = pendingReleasedScrubPageRef.current;
31954
+ if (pendingPage !== null && currentPage === pendingPage) {
31955
+ pendingReleasedScrubPageRef.current = null;
31956
+ setScrubPreviewPage(null);
31957
+ }
31958
+ }, [currentPage]);
31959
+ useEffect10(() => {
31960
+ if (!isScrubbingRef.current) {
31961
+ thumbTopRef.current.setValue(thumbTop);
31962
+ }
31963
+ }, [thumbTop, thumbTopRef]);
31964
+ useEffect10(
31965
+ () => () => {
31966
+ if (longPressTimerRef.current !== null) {
31967
+ clearTimeout(longPressTimerRef.current);
31968
+ }
31969
+ },
31970
+ []
31971
+ );
31972
+ if (!shouldRenderPageScrubber({
31973
+ mobileChromeVisible,
31974
+ mobileProgressPillVisible,
31975
+ isScrubbing: isScrubbingRef.current
31976
+ })) {
31977
+ return null;
31978
+ }
31979
+ const scrubToPosition = (position) => {
31980
+ const { trackHeight: trackHeight2, thumbHeight: thumbHeight2, pageCount: pageCount2 } = scrubberMetricsRef.current;
31981
+ const visualPosition = resolvePageScrubberThumbTopFromPosition({
31982
+ position,
31983
+ trackHeight: trackHeight2,
31984
+ thumbHeight: thumbHeight2
31985
+ });
31986
+ thumbTopRef.current.setValue(visualPosition);
31987
+ const nextPage = resolvePageScrubberPage({
31988
+ position,
31989
+ trackHeight: trackHeight2,
31990
+ thumbHeight: thumbHeight2,
31991
+ pageCount: pageCount2
31992
+ });
31993
+ if (nextPage === null) return;
31994
+ scrubNavigationControllerRef.current.update(nextPage);
31995
+ setScrubPreviewPage(nextPage);
31996
+ };
31997
+ scrubToPositionRef.current = scrubToPosition;
31998
+ if (scrubberResponderRef.current === null) {
31999
+ scrubberResponderRef.current = PanResponder2.create({
32000
+ onStartShouldSetPanResponder: () => touchPolicy.claimOnStart,
32001
+ onStartShouldSetPanResponderCapture: () => touchPolicy.captureOnStart,
32002
+ onMoveShouldSetPanResponder: () => true,
32003
+ onMoveShouldSetPanResponderCapture: () => touchPolicy.captureOnMove,
32004
+ onPanResponderTerminationRequest: () => false,
32005
+ onPanResponderGrant: () => {
32006
+ isScrubbingRef.current = true;
32007
+ onScrubbingChangeRef.current?.(true);
32008
+ hasMovedRef.current = false;
32009
+ longPressTriggeredRef.current = false;
32010
+ scrubNavigationControllerRef.current.begin();
32011
+ pendingReleasedScrubPageRef.current = null;
32012
+ setScrubPreviewPage(currentPageRef.current);
32013
+ if (longPressTimerRef.current !== null) {
32014
+ clearTimeout(longPressTimerRef.current);
32015
+ }
32016
+ longPressTimerRef.current = onOpenPageJumpRef.current ? setTimeout(() => {
32017
+ longPressTimerRef.current = null;
32018
+ if (!hasMovedRef.current) {
32019
+ longPressTriggeredRef.current = true;
32020
+ onOpenPageJumpRef.current?.();
32021
+ }
32022
+ }, 500) : null;
32023
+ },
32024
+ onPanResponderMove: (_, gestureState) => {
32025
+ if (Math.abs(gestureState.dy) <= 4) return;
32026
+ hasMovedRef.current = true;
32027
+ if (longPressTimerRef.current !== null) {
32028
+ clearTimeout(longPressTimerRef.current);
32029
+ longPressTimerRef.current = null;
32030
+ }
32031
+ scrubToPositionRef.current(gestureState.moveY - trackYRef.current);
32032
+ },
32033
+ onPanResponderRelease: () => {
32034
+ if (longPressTimerRef.current !== null) {
32035
+ clearTimeout(longPressTimerRef.current);
32036
+ longPressTimerRef.current = null;
32037
+ }
32038
+ isScrubbingRef.current = false;
32039
+ onScrubbingChangeRef.current?.(false);
32040
+ const releasedPage = scrubNavigationControllerRef.current.release();
32041
+ const releaseAction = resolvePageScrubberReleaseAction({
32042
+ hasMoved: hasMovedRef.current,
32043
+ pendingPage: releasedPage
32044
+ });
32045
+ if (releaseAction.kind === "navigate") {
32046
+ pendingReleasedScrubPageRef.current = releaseAction.page;
32047
+ if (!onNavigateToPageRef.current) {
32048
+ setScrubPreviewPage(null);
32049
+ }
32050
+ } else {
32051
+ scrubNavigationControllerRef.current.cancel();
32052
+ pendingReleasedScrubPageRef.current = null;
32053
+ setScrubPreviewPage(null);
32054
+ if (!longPressTriggeredRef.current) onPressRef.current();
32055
+ }
32056
+ hasMovedRef.current = false;
32057
+ longPressTriggeredRef.current = false;
32058
+ },
32059
+ onPanResponderTerminate: () => {
32060
+ if (longPressTimerRef.current !== null) {
32061
+ clearTimeout(longPressTimerRef.current);
32062
+ longPressTimerRef.current = null;
32063
+ }
32064
+ isScrubbingRef.current = false;
32065
+ onScrubbingChangeRef.current?.(false);
32066
+ scrubNavigationControllerRef.current.cancel();
32067
+ pendingReleasedScrubPageRef.current = null;
32068
+ setScrubPreviewPage(null);
32069
+ hasMovedRef.current = false;
32070
+ longPressTriggeredRef.current = false;
32071
+ thumbTopRef.current.setValue(thumbTopSnapshotRef.current);
32072
+ }
32073
+ });
32074
+ }
32075
+ const responderPanHandlers = scrubberResponderRef.current?.panHandlers ?? {};
32076
+ const responderTarget = touchPolicy.responderTarget;
32077
+ const scrubberPanHandlers = responderTarget === "track" ? responderPanHandlers : {};
32078
+ const pillPanHandlers = responderTarget === "pill" ? responderPanHandlers : {};
31799
32079
  return /* @__PURE__ */ jsx20(
31800
32080
  View17,
31801
32081
  {
@@ -31804,32 +32084,45 @@ function ProgressPill({
31804
32084
  styles17.frame,
31805
32085
  {
31806
32086
  top: offsets.progress,
31807
- left: 0,
31808
- right: 0,
31809
- paddingLeft: offsets.left + MOBILE_CHROME_METRICS.progressHorizontalOffset,
31810
- paddingRight: offsets.right
32087
+ right: offsets.right
31811
32088
  }
31812
32089
  ],
31813
32090
  children: /* @__PURE__ */ jsxs17(
31814
- Pressable12,
32091
+ View17,
31815
32092
  {
31816
- ...getProgressPillInteraction(onPress, onOpenPageJump),
31817
- style: [
31818
- styles17.pill,
31819
- isDark && styles17.pillDark,
31820
- { borderColor: `${accentColor}33` }
31821
- ],
31822
- testID: "papyrus-progress-pill",
32093
+ ref: scrubberRef,
32094
+ style: [styles17.scrubber, { height: trackHeight }],
32095
+ ...scrubberPanHandlers,
32096
+ onLayout: () => {
32097
+ scrubberRef.current?.measureInWindow((_x, y) => {
32098
+ trackYRef.current = y;
32099
+ });
32100
+ },
31823
32101
  children: [
31824
- /* @__PURE__ */ jsx20(View17, { style: styles17.iconHit, children: /* @__PURE__ */ jsx20(
31825
- IconPageNav,
32102
+ /* @__PURE__ */ jsx20(View17, { style: [styles17.track, isDark && styles17.trackDark] }),
32103
+ /* @__PURE__ */ jsx20(Animated2.View, { style: [styles17.thumb, { top: thumbTopRef.current }], children: /* @__PURE__ */ jsxs17(
32104
+ View17,
31826
32105
  {
31827
- size: 20,
31828
- color: isDark ? "#f8fafc" : "#111827",
31829
- strokeWidth: 1.8
32106
+ style: [
32107
+ styles17.pill,
32108
+ isDark && styles17.pillDark,
32109
+ { borderColor: `${accentColor}33` }
32110
+ ],
32111
+ ...pillPanHandlers,
32112
+ testID: "papyrus-progress-pill",
32113
+ children: [
32114
+ /* @__PURE__ */ jsx20(View17, { style: styles17.labelHit, children: /* @__PURE__ */ jsx20(Text12, { style: [styles17.label, isDark && styles17.labelDark], children: label }) }),
32115
+ /* @__PURE__ */ jsx20(View17, { style: styles17.iconHit, children: /* @__PURE__ */ jsx20(
32116
+ IconPageNav,
32117
+ {
32118
+ size: 20,
32119
+ color: isDark ? "#f8fafc" : "#111827",
32120
+ strokeWidth: 1.8
32121
+ }
32122
+ ) })
32123
+ ]
31830
32124
  }
31831
- ) }),
31832
- /* @__PURE__ */ jsx20(View17, { style: styles17.labelHit, children: /* @__PURE__ */ jsx20(Text12, { style: [styles17.label, isDark && styles17.labelDark], children: label }) })
32125
+ ) })
31833
32126
  ]
31834
32127
  }
31835
32128
  )
@@ -31843,9 +32136,31 @@ var styles17 = StyleSheet17.create({
31843
32136
  left: 0,
31844
32137
  right: 0,
31845
32138
  bottom: "auto",
31846
- alignItems: "flex-start",
32139
+ alignItems: "flex-end",
31847
32140
  zIndex: 18
31848
32141
  },
32142
+ scrubber: {
32143
+ width: 132,
32144
+ position: "relative",
32145
+ marginRight: 4
32146
+ },
32147
+ track: {
32148
+ position: "absolute",
32149
+ top: 0,
32150
+ right: 30,
32151
+ bottom: 0,
32152
+ width: 3,
32153
+ borderRadius: 2,
32154
+ backgroundColor: "rgba(15,23,42,0.16)"
32155
+ },
32156
+ trackDark: {
32157
+ backgroundColor: "rgba(248,250,252,0.22)"
32158
+ },
32159
+ thumb: {
32160
+ position: "absolute",
32161
+ right: 0,
32162
+ height: 44
32163
+ },
31849
32164
  pill: {
31850
32165
  minWidth: 92,
31851
32166
  borderRadius: 999,
@@ -31884,10 +32199,10 @@ var styles17 = StyleSheet17.create({
31884
32199
  });
31885
32200
 
31886
32201
  // components/SearchOverlay.tsx
31887
- import React15, { useEffect as useEffect10, useMemo as useMemo8, useState as useState11 } from "react";
32202
+ import React15, { useEffect as useEffect11, useMemo as useMemo8, useState as useState12 } from "react";
31888
32203
  import {
31889
32204
  Keyboard,
31890
- Pressable as Pressable13,
32205
+ Pressable as Pressable12,
31891
32206
  StyleSheet as StyleSheet18,
31892
32207
  Text as Text13,
31893
32208
  TextInput as TextInput3,
@@ -31915,19 +32230,19 @@ function SearchOverlay({
31915
32230
  uiTheme,
31916
32231
  accentColor
31917
32232
  } = useViewerStore15();
31918
- const [draft, setDraft] = useState11(searchQuery);
31919
- const [isSearching, setIsSearching] = useState11(false);
32233
+ const [draft, setDraft] = useState12(searchQuery);
32234
+ const [isSearching, setIsSearching] = useState12(false);
31920
32235
  const searchRequestIdRef = React15.useRef(0);
31921
32236
  const isDark = uiTheme === "dark";
31922
32237
  const offsets = resolveMobileChromeOffsets(usePapyrusSafeAreaInsets());
31923
32238
  const t = getStrings(locale);
31924
32239
  const searchService = useMemo8(() => new SearchService(engine), [engine]);
31925
- useEffect10(() => {
32240
+ useEffect11(() => {
31926
32241
  searchService.clearCache();
31927
32242
  }, [isLoaded, pageCount, searchService]);
31928
32243
  const currentCount = searchResults.length > 0 && activeSearchIndex >= 0 ? activeSearchIndex + 1 : 0;
31929
32244
  const targetLabel = documentType === "pdf" ? t.page : documentType === "epub" ? t.contents : t.progress;
31930
- useEffect10(() => {
32245
+ useEffect11(() => {
31931
32246
  if (!visible) return;
31932
32247
  setDraft(searchQuery);
31933
32248
  }, [searchQuery, visible]);
@@ -31980,7 +32295,7 @@ function SearchOverlay({
31980
32295
  )
31981
32296
  ] }),
31982
32297
  /* @__PURE__ */ jsx21(
31983
- Pressable13,
32298
+ Pressable12,
31984
32299
  {
31985
32300
  onPress: handleClose,
31986
32301
  style: [styles18.closeButton, isDark && styles18.closeButtonDark],
@@ -31991,7 +32306,7 @@ function SearchOverlay({
31991
32306
  ] }),
31992
32307
  /* @__PURE__ */ jsxs18(View18, { style: styles18.metaRow, children: [
31993
32308
  /* @__PURE__ */ jsx21(
31994
- Pressable13,
32309
+ Pressable12,
31995
32310
  {
31996
32311
  onPress: prevSearchResult,
31997
32312
  disabled: searchResults.length === 0,
@@ -32006,7 +32321,7 @@ function SearchOverlay({
32006
32321
  ),
32007
32322
  /* @__PURE__ */ jsx21(Text13, { style: [styles18.metaText, isDark && styles18.metaTextDark], children: isSearching ? t.searching : `${currentCount}/${searchResults.length} ${targetLabel}` }),
32008
32323
  /* @__PURE__ */ jsx21(
32009
- Pressable13,
32324
+ Pressable12,
32010
32325
  {
32011
32326
  onPress: nextSearchResult,
32012
32327
  disabled: searchResults.length === 0,
@@ -32022,7 +32337,7 @@ function SearchOverlay({
32022
32337
  ] }),
32023
32338
  /* @__PURE__ */ jsxs18(View18, { style: styles18.actionsRow, children: [
32024
32339
  /* @__PURE__ */ jsx21(
32025
- Pressable13,
32340
+ Pressable12,
32026
32341
  {
32027
32342
  onPress: handleSubmit,
32028
32343
  style: [styles18.primaryAction, { backgroundColor: accentColor }],
@@ -32032,7 +32347,7 @@ function SearchOverlay({
32032
32347
  }
32033
32348
  ),
32034
32349
  /* @__PURE__ */ jsx21(
32035
- Pressable13,
32350
+ Pressable12,
32036
32351
  {
32037
32352
  onPress: onOpenResults,
32038
32353
  disabled: searchResults.length === 0,
@@ -32200,7 +32515,7 @@ var styles18 = StyleSheet18.create({
32200
32515
  });
32201
32516
 
32202
32517
  // components/SearchResultsSheet.tsx
32203
- import { Pressable as Pressable14, StyleSheet as StyleSheet19, Text as Text14 } from "react-native";
32518
+ import { Pressable as Pressable13, StyleSheet as StyleSheet19, Text as Text14 } from "react-native";
32204
32519
  import { useViewerStore as useViewerStore16 } from "@papyrus-sdk/core";
32205
32520
  import { jsx as jsx22, jsxs as jsxs19 } from "react/jsx-runtime";
32206
32521
  function SearchResultsSheet({
@@ -32251,7 +32566,7 @@ function SearchResultsSheet({
32251
32566
  /* @__PURE__ */ jsx22(NativeSheetScrollView, { contentContainerStyle: styles19.content, children: searchResults.length === 0 ? /* @__PURE__ */ jsx22(Text14, { style: [styles19.emptyText, { color: palette.mutedText }], children: t.noResults }) : searchResults.map((result, index) => {
32252
32567
  const isActive = index === activeSearchIndex;
32253
32568
  return /* @__PURE__ */ jsxs19(
32254
- Pressable14,
32569
+ Pressable13,
32255
32570
  {
32256
32571
  onPress: () => {
32257
32572
  setDocumentState({
@@ -32321,12 +32636,13 @@ function ReadingShell({
32321
32636
  uiTheme,
32322
32637
  accentColor
32323
32638
  } = useViewerStore17();
32324
- const [overflowOpen, setOverflowOpen] = useState12(false);
32325
- const [infoOpen, setInfoOpen] = useState12(false);
32326
- const [actionsOpen, setActionsOpen] = useState12(false);
32327
- const [settingsOpen, setSettingsOpen] = useState12(false);
32328
- const [searchResultsOpen, setSearchResultsOpen] = useState12(false);
32329
- const [pageJumpOpen, setPageJumpOpen] = useState12(false);
32639
+ const [overflowOpen, setOverflowOpen] = useState13(false);
32640
+ const [infoOpen, setInfoOpen] = useState13(false);
32641
+ const [actionsOpen, setActionsOpen] = useState13(false);
32642
+ const [settingsOpen, setSettingsOpen] = useState13(false);
32643
+ const [searchResultsOpen, setSearchResultsOpen] = useState13(false);
32644
+ const [pageJumpOpen, setPageJumpOpen] = useState13(false);
32645
+ const [pageScrubActive, setPageScrubActive] = useState13(false);
32330
32646
  const isDark = uiTheme === "dark";
32331
32647
  React16.useEffect(() => {
32332
32648
  const showSubscription = Keyboard2.addListener("keyboardDidShow", () => {
@@ -32403,15 +32719,24 @@ function ReadingShell({
32403
32719
  showPageNavigationControls: false
32404
32720
  }
32405
32721
  ),
32406
- /* @__PURE__ */ jsx23(View19, { style: styles20.viewerStage, children: /* @__PURE__ */ jsx23(Viewer_default, { engine, ...viewerProps }) }),
32722
+ /* @__PURE__ */ jsx23(View19, { style: styles20.viewerStage, children: /* @__PURE__ */ jsx23(
32723
+ Viewer_default,
32724
+ {
32725
+ engine,
32726
+ ...viewerProps,
32727
+ pageScrubActive
32728
+ }
32729
+ ) }),
32407
32730
  /* @__PURE__ */ jsx23(
32408
32731
  ProgressPill,
32409
32732
  {
32410
32733
  documentType,
32734
+ onNavigateToPage: navigateToPage,
32411
32735
  onPress: () => openDestination(
32412
32736
  documentType === "pdf" ? "pages" : documentType === "epub" ? "contents" : "progress"
32413
32737
  ),
32414
- onOpenPageJump: () => setPageJumpOpen(true)
32738
+ onOpenPageJump: () => setPageJumpOpen(true),
32739
+ onScrubbingChange: setPageScrubActive
32415
32740
  }
32416
32741
  ),
32417
32742
  /* @__PURE__ */ jsx23(