pdfjs-reader-core 0.5.10 → 0.5.12

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.d.cts CHANGED
@@ -2031,6 +2031,22 @@ interface EngineDeps {
2031
2031
  * visually in a narration context. Default: 3500ms.
2032
2032
  */
2033
2033
  minOverlayDurationMs?: number;
2034
+ /**
2035
+ * When true, only ONE annotation overlay is on-screen at any time: every
2036
+ * new overlay-emitting step first clears all prior overlays and their
2037
+ * auto-removal timers.
2038
+ *
2039
+ * Defaults to `isIOSMobile()` — on iOS Safari/WebKit, stacking 2-3 overlays
2040
+ * (e.g. highlight + box + underline + callout) on the same anchor is the
2041
+ * single biggest remaining crash vector we've measured. The GPU
2042
+ * compositor rejects the layer combination long before the canvas cap
2043
+ * kicks in. Dropping to one overlay at a time sacrifices overlay chaining
2044
+ * on iOS for a hard stability guarantee.
2045
+ *
2046
+ * Pass `false` explicitly to force multi-overlay mode even on iOS (e.g.
2047
+ * for tests or a deliberate opt-out in the consumer).
2048
+ */
2049
+ singleActiveOverlay?: boolean;
2034
2050
  }
2035
2051
  declare class StoryboardEngine {
2036
2052
  private deps;
@@ -2049,6 +2065,13 @@ declare class StoryboardEngine {
2049
2065
  */
2050
2066
  private overlayRemovalTimers;
2051
2067
  private currentStoryboardId;
2068
+ /**
2069
+ * Resolved value of `deps.singleActiveOverlay`. Cached at construction so
2070
+ * each step dispatch is a single branch rather than a per-call
2071
+ * userAgent sniff, and so the policy stays stable for the engine's
2072
+ * lifetime (a viewport flip that recreates the engine will re-evaluate).
2073
+ */
2074
+ private readonly singleActiveOverlay;
2052
2075
  constructor(deps: EngineDeps);
2053
2076
  /**
2054
2077
  * Execute a new storyboard. Cancels in-flight steps from the previous storyboard
package/dist/index.d.ts CHANGED
@@ -2031,6 +2031,22 @@ interface EngineDeps {
2031
2031
  * visually in a narration context. Default: 3500ms.
2032
2032
  */
2033
2033
  minOverlayDurationMs?: number;
2034
+ /**
2035
+ * When true, only ONE annotation overlay is on-screen at any time: every
2036
+ * new overlay-emitting step first clears all prior overlays and their
2037
+ * auto-removal timers.
2038
+ *
2039
+ * Defaults to `isIOSMobile()` — on iOS Safari/WebKit, stacking 2-3 overlays
2040
+ * (e.g. highlight + box + underline + callout) on the same anchor is the
2041
+ * single biggest remaining crash vector we've measured. The GPU
2042
+ * compositor rejects the layer combination long before the canvas cap
2043
+ * kicks in. Dropping to one overlay at a time sacrifices overlay chaining
2044
+ * on iOS for a hard stability guarantee.
2045
+ *
2046
+ * Pass `false` explicitly to force multi-overlay mode even on iOS (e.g.
2047
+ * for tests or a deliberate opt-out in the consumer).
2048
+ */
2049
+ singleActiveOverlay?: boolean;
2034
2050
  }
2035
2051
  declare class StoryboardEngine {
2036
2052
  private deps;
@@ -2049,6 +2065,13 @@ declare class StoryboardEngine {
2049
2065
  */
2050
2066
  private overlayRemovalTimers;
2051
2067
  private currentStoryboardId;
2068
+ /**
2069
+ * Resolved value of `deps.singleActiveOverlay`. Cached at construction so
2070
+ * each step dispatch is a single branch rather than a per-call
2071
+ * userAgent sniff, and so the policy stays stable for the engine's
2072
+ * lifetime (a viewport flip that recreates the engine will re-evaluate).
2073
+ */
2074
+ private readonly singleActiveOverlay;
2052
2075
  constructor(deps: EngineDeps);
2053
2076
  /**
2054
2077
  * Execute a new storyboard. Cancels in-flight steps from the previous storyboard
package/dist/index.js CHANGED
@@ -1879,6 +1879,15 @@ function detectDeviceCapabilities() {
1879
1879
  screenSize
1880
1880
  };
1881
1881
  }
1882
+ function isIOSMobile() {
1883
+ if (typeof window === "undefined" || typeof navigator === "undefined") {
1884
+ return false;
1885
+ }
1886
+ const ua = navigator.userAgent || "";
1887
+ if (/iPhone|iPad|iPod/i.test(ua)) return true;
1888
+ const isAppleTouch = /Mac/i.test(ua) && typeof navigator.maxTouchPoints === "number" && navigator.maxTouchPoints > 1;
1889
+ return isAppleTouch;
1890
+ }
1882
1891
  function getRenderConfig(quality = "auto", capabilities) {
1883
1892
  const caps = capabilities ?? detectDeviceCapabilities();
1884
1893
  if (quality === "auto") {
@@ -1893,8 +1902,10 @@ function getRenderConfig(quality = "auto", capabilities) {
1893
1902
  switch (quality) {
1894
1903
  case "low":
1895
1904
  return {
1896
- canvasScaleFactor: Math.min(1, caps.devicePixelRatio * 0.5),
1897
- maxCanvasDimension: 2048,
1905
+ canvasScaleFactor: caps.devicePixelRatio,
1906
+ // 3072² ≈ 9 MP — fits a full-DPR US Letter (DPR=3 → 1836×2376) at
1907
+ // default scale and caps at deep zoom on memory-constrained iPhones.
1908
+ maxCanvasDimension: 3072,
1898
1909
  overscanPages: 1,
1899
1910
  maxPagesInMemory: 3,
1900
1911
  scrollDebounceMs: 100,
@@ -1907,7 +1918,8 @@ function getRenderConfig(quality = "auto", capabilities) {
1907
1918
  };
1908
1919
  case "medium":
1909
1920
  return {
1910
- canvasScaleFactor: Math.min(1.5, caps.devicePixelRatio * 0.75),
1921
+ canvasScaleFactor: caps.devicePixelRatio,
1922
+ // 4096² ≈ 16 MP — within iOS Safari per-canvas budget across versions.
1911
1923
  maxCanvasDimension: 4096,
1912
1924
  overscanPages: 2,
1913
1925
  maxPagesInMemory: 5,
@@ -5181,9 +5193,37 @@ var init_ThumbnailPanel = __esm({
5181
5193
  onClick,
5182
5194
  scale
5183
5195
  }) {
5196
+ const buttonRef = useRef7(null);
5184
5197
  const canvasRef = useRef7(null);
5185
5198
  const [isRendered, setIsRendered] = useState6(false);
5199
+ const [shouldRender, setShouldRender] = useState6(false);
5186
5200
  useEffect9(() => {
5201
+ if (shouldRender) return;
5202
+ const el = buttonRef.current;
5203
+ if (!el) return;
5204
+ if (typeof IntersectionObserver === "undefined") {
5205
+ setShouldRender(true);
5206
+ return;
5207
+ }
5208
+ const observer = new IntersectionObserver(
5209
+ (entries) => {
5210
+ for (const entry of entries) {
5211
+ if (entry.isIntersecting) {
5212
+ setShouldRender(true);
5213
+ observer.disconnect();
5214
+ break;
5215
+ }
5216
+ }
5217
+ },
5218
+ // 200px root margin so the page is fetched just before it scrolls
5219
+ // into view, hiding the render latency.
5220
+ { rootMargin: "200px" }
5221
+ );
5222
+ observer.observe(el);
5223
+ return () => observer.disconnect();
5224
+ }, [shouldRender]);
5225
+ useEffect9(() => {
5226
+ if (!shouldRender) return;
5187
5227
  let cancelled = false;
5188
5228
  const renderThumbnail = async () => {
5189
5229
  const canvas = canvasRef.current;
@@ -5217,10 +5257,11 @@ var init_ThumbnailPanel = __esm({
5217
5257
  return () => {
5218
5258
  cancelled = true;
5219
5259
  };
5220
- }, [document2, pageNumber, scale]);
5260
+ }, [document2, pageNumber, scale, shouldRender]);
5221
5261
  return /* @__PURE__ */ jsxs3(
5222
5262
  "button",
5223
5263
  {
5264
+ ref: buttonRef,
5224
5265
  onClick,
5225
5266
  className: cn(
5226
5267
  "thumbnail-item",
@@ -9443,6 +9484,11 @@ var init_VirtualizedDocumentContainer = __esm({
9443
9484
  selectHighlight,
9444
9485
  activeColor
9445
9486
  } = useHighlights();
9487
+ const [defaultDims, setDefaultDims] = useState19({
9488
+ width: DEFAULT_PAGE_WIDTH2,
9489
+ height: DEFAULT_PAGE_HEIGHT2
9490
+ });
9491
+ const [dimsVersion, setDimsVersion] = useState19(0);
9446
9492
  useEffect20(() => {
9447
9493
  if (document2 !== documentRef.current) {
9448
9494
  documentRef.current = document2;
@@ -9451,39 +9497,47 @@ var init_VirtualizedDocumentContainer = __esm({
9451
9497
  setPageObjects(/* @__PURE__ */ new Map());
9452
9498
  }
9453
9499
  }, [document2]);
9500
+ useEffect20(() => {
9501
+ pageDimensionsCache.current.clear();
9502
+ setDimsVersion((v) => v + 1);
9503
+ }, [rotation]);
9454
9504
  useEffect20(() => {
9455
9505
  if (!document2 || numPages === 0) return;
9456
- const calculatePageInfos = async () => {
9457
- const infos = [];
9458
- let currentTop = 0;
9459
- for (let i = 1; i <= numPages; i++) {
9460
- let dimensions = pageDimensionsCache.current.get(i);
9461
- if (!dimensions) {
9462
- try {
9463
- const page = pageCache.current.get(i) || await document2.getPage(i);
9464
- if (!pageCache.current.has(i)) {
9465
- pageCache.current.set(i, page);
9466
- }
9467
- const viewport = page.getViewport({ scale: 1, rotation });
9468
- dimensions = { width: viewport.width, height: viewport.height };
9469
- pageDimensionsCache.current.set(i, dimensions);
9470
- } catch {
9471
- dimensions = { width: DEFAULT_PAGE_WIDTH2, height: DEFAULT_PAGE_HEIGHT2 };
9472
- }
9473
- }
9474
- const scaledHeight = Math.floor(dimensions.height * scale);
9475
- infos.push({
9476
- pageNumber: i,
9477
- top: currentTop,
9478
- height: scaledHeight
9479
- });
9480
- currentTop += scaledHeight + pageGap;
9506
+ let cancelled = false;
9507
+ (async () => {
9508
+ try {
9509
+ const page = pageCache.current.get(1) ?? await document2.getPage(1);
9510
+ if (cancelled) return;
9511
+ pageCache.current.set(1, page);
9512
+ const viewport = page.getViewport({ scale: 1, rotation });
9513
+ const dims = { width: viewport.width, height: viewport.height };
9514
+ pageDimensionsCache.current.set(1, dims);
9515
+ setDefaultDims(dims);
9516
+ setDimsVersion((v) => v + 1);
9517
+ } catch {
9481
9518
  }
9482
- setPageInfos(infos);
9483
- setTotalHeight(currentTop - pageGap);
9519
+ })();
9520
+ return () => {
9521
+ cancelled = true;
9484
9522
  };
9485
- calculatePageInfos();
9486
- }, [document2, numPages, scale, rotation, pageGap]);
9523
+ }, [document2, numPages, rotation]);
9524
+ useEffect20(() => {
9525
+ if (!document2 || numPages === 0) {
9526
+ setPageInfos([]);
9527
+ setTotalHeight(0);
9528
+ return;
9529
+ }
9530
+ const infos = [];
9531
+ let currentTop = 0;
9532
+ for (let i = 1; i <= numPages; i++) {
9533
+ const dimensions = pageDimensionsCache.current.get(i) ?? defaultDims;
9534
+ const scaledHeight = Math.floor(dimensions.height * scale);
9535
+ infos.push({ pageNumber: i, top: currentTop, height: scaledHeight });
9536
+ currentTop += scaledHeight + pageGap;
9537
+ }
9538
+ setPageInfos(infos);
9539
+ setTotalHeight(Math.max(0, currentTop - pageGap));
9540
+ }, [document2, numPages, scale, rotation, pageGap, defaultDims, dimsVersion]);
9487
9541
  const updateVisiblePages = useCallback30(() => {
9488
9542
  if (!scrollContainerRef.current || pageInfos.length === 0) return;
9489
9543
  const container = scrollContainerRef.current;
@@ -9531,6 +9585,7 @@ var init_VirtualizedDocumentContainer = __esm({
9531
9585
  const loadPages = async () => {
9532
9586
  const newPageObjects = new Map(pageObjects);
9533
9587
  let hasChanges = false;
9588
+ let dimsChanged = false;
9534
9589
  for (const pageNum of visiblePages) {
9535
9590
  if (!newPageObjects.has(pageNum)) {
9536
9591
  try {
@@ -9541,6 +9596,14 @@ var init_VirtualizedDocumentContainer = __esm({
9541
9596
  }
9542
9597
  newPageObjects.set(pageNum, page);
9543
9598
  hasChanges = true;
9599
+ if (!pageDimensionsCache.current.has(pageNum)) {
9600
+ const vp = page.getViewport({ scale: 1, rotation });
9601
+ pageDimensionsCache.current.set(pageNum, {
9602
+ width: vp.width,
9603
+ height: vp.height
9604
+ });
9605
+ dimsChanged = true;
9606
+ }
9544
9607
  } catch (error) {
9545
9608
  const errorMessage = error instanceof Error ? error.message : String(error);
9546
9609
  const isDocumentDestroyed = errorMessage.includes("destroyed") || errorMessage.includes("sendWithStream") || errorMessage.includes("sendWithPromise") || errorMessage.includes("Cannot read properties of null");
@@ -9560,9 +9623,12 @@ var init_VirtualizedDocumentContainer = __esm({
9560
9623
  if (hasChanges) {
9561
9624
  setPageObjects(newPageObjects);
9562
9625
  }
9626
+ if (dimsChanged) {
9627
+ setDimsVersion((v) => v + 1);
9628
+ }
9563
9629
  };
9564
9630
  loadPages();
9565
- }, [document2, visiblePages, pageObjects]);
9631
+ }, [document2, visiblePages, pageObjects, rotation]);
9566
9632
  useEffect20(() => {
9567
9633
  if (!scrollToPageRequest || !scrollContainerRef.current || pageInfos.length === 0) return;
9568
9634
  const { page, requestId, behavior } = scrollToPageRequest;
@@ -10127,10 +10193,10 @@ var init_DualPageContainer = __esm({
10127
10193
  });
10128
10194
 
10129
10195
  // src/components/PDFViewer/BookModeContainer.tsx
10130
- import React, { memo as memo26, useEffect as useEffect22, useState as useState21, useRef as useRef19, useCallback as useCallback32 } from "react";
10196
+ import React, { memo as memo26, useEffect as useEffect22, useMemo as useMemo14, useState as useState21, useRef as useRef19, useCallback as useCallback32 } from "react";
10131
10197
  import HTMLFlipBook from "react-pageflip";
10132
10198
  import { jsx as jsx27, jsxs as jsxs23 } from "react/jsx-runtime";
10133
- var BookPage, BookModeContainer;
10199
+ var BOOK_MODE_OVERSCAN, BookPage, BookModeContainer;
10134
10200
  var init_BookModeContainer = __esm({
10135
10201
  "src/components/PDFViewer/BookModeContainer.tsx"() {
10136
10202
  "use strict";
@@ -10138,6 +10204,7 @@ var init_BookModeContainer = __esm({
10138
10204
  init_PDFLoadingScreen2();
10139
10205
  init_hooks();
10140
10206
  init_utils();
10207
+ BOOK_MODE_OVERSCAN = 4;
10141
10208
  BookPage = React.forwardRef(function BookPage2({ pageNumber, page, scale, rotation, width, height }, ref) {
10142
10209
  return /* @__PURE__ */ jsx27("div", { ref, className: "book-page", "data-page-number": pageNumber, children: /* @__PURE__ */ jsx27("div", { style: { width, height, overflow: "hidden" }, children: /* @__PURE__ */ jsx27(
10143
10210
  PDFPage,
@@ -10170,9 +10237,10 @@ var init_BookModeContainer = __esm({
10170
10237
  } = usePDFViewer();
10171
10238
  const scrollToPageRequest = useViewerStore((s) => s.scrollToPageRequest);
10172
10239
  const { viewerStore } = usePDFViewerStores();
10173
- const [pages, setPages] = useState21([]);
10240
+ const [pages, setPages] = useState21(/* @__PURE__ */ new Map());
10174
10241
  const [rawPageDims, setRawPageDims] = useState21({ width: 612, height: 792 });
10175
- const [isLoadingPages, setIsLoadingPages] = useState21(false);
10242
+ const [hasFirstPage, setHasFirstPage] = useState21(false);
10243
+ const pageRequestsRef = useRef19(/* @__PURE__ */ new Set());
10176
10244
  const containerRef = useRef19(null);
10177
10245
  const [containerSize, setContainerSize] = useState21({ width: 0, height: 0 });
10178
10246
  const flipBookRef = useRef19(null);
@@ -10189,41 +10257,67 @@ var init_BookModeContainer = __esm({
10189
10257
  return () => ro.disconnect();
10190
10258
  }, []);
10191
10259
  useEffect22(() => {
10192
- if (!document2) {
10193
- setPages([]);
10194
- return;
10195
- }
10260
+ setPages(/* @__PURE__ */ new Map());
10261
+ pageRequestsRef.current.clear();
10262
+ setHasFirstPage(false);
10263
+ }, [document2]);
10264
+ useEffect22(() => {
10265
+ if (!document2 || numPages === 0) return;
10266
+ const start = Math.max(1, currentPage - BOOK_MODE_OVERSCAN);
10267
+ const end = Math.min(numPages, currentPage + BOOK_MODE_OVERSCAN);
10268
+ const isFirstLoad = !hasFirstPage;
10196
10269
  let cancelled = false;
10197
- const loadAllPages = async () => {
10198
- setIsLoadingPages(true);
10199
- try {
10200
- const pagePromises = [];
10201
- for (let i = 1; i <= numPages; i++) {
10202
- pagePromises.push(document2.getPage(i));
10270
+ const wanted = [];
10271
+ if (isFirstLoad && !pages.has(1) && !pageRequestsRef.current.has(1)) {
10272
+ wanted.push(1);
10273
+ pageRequestsRef.current.add(1);
10274
+ }
10275
+ for (let i = start; i <= end; i++) {
10276
+ if (pages.has(i) || pageRequestsRef.current.has(i)) continue;
10277
+ wanted.push(i);
10278
+ pageRequestsRef.current.add(i);
10279
+ }
10280
+ if (wanted.length === 0) return;
10281
+ (async () => {
10282
+ const results = await Promise.allSettled(
10283
+ wanted.map((i) => document2.getPage(i).then((p) => [i, p]))
10284
+ );
10285
+ if (cancelled) return;
10286
+ const next = new Map(pages);
10287
+ let firstPageJustLoaded = null;
10288
+ for (const r of results) {
10289
+ if (r.status === "fulfilled") {
10290
+ const [i, page] = r.value;
10291
+ next.set(i, page);
10292
+ if (i === 1) firstPageJustLoaded = page;
10203
10293
  }
10204
- const results = await Promise.allSettled(pagePromises);
10205
- if (!cancelled) {
10206
- const loaded2 = results.map((r) => r.status === "fulfilled" ? r.value : null);
10207
- setPages(loaded2);
10208
- const firstPage = loaded2[0];
10209
- if (firstPage) {
10210
- const vp = firstPage.getViewport({ scale: 1, rotation });
10211
- setRawPageDims({ width: vp.width, height: vp.height });
10212
- }
10294
+ }
10295
+ for (const i of next.keys()) {
10296
+ if (i !== 1 && (i < start || i > end)) {
10297
+ next.delete(i);
10213
10298
  }
10214
- } catch {
10215
- } finally {
10216
- if (!cancelled) setIsLoadingPages(false);
10217
10299
  }
10218
- };
10219
- loadAllPages();
10300
+ setPages(next);
10301
+ if (firstPageJustLoaded) {
10302
+ const vp = firstPageJustLoaded.getViewport({ scale: 1, rotation });
10303
+ setRawPageDims({ width: vp.width, height: vp.height });
10304
+ setHasFirstPage(true);
10305
+ }
10306
+ for (const i of wanted) {
10307
+ pageRequestsRef.current.delete(i);
10308
+ }
10309
+ })();
10220
10310
  return () => {
10221
10311
  cancelled = true;
10312
+ for (const i of wanted) {
10313
+ pageRequestsRef.current.delete(i);
10314
+ }
10222
10315
  };
10223
- }, [document2, numPages, rotation]);
10316
+ }, [document2, numPages, currentPage, rotation, pages, hasFirstPage]);
10224
10317
  useEffect22(() => {
10225
- if (pages[0]) {
10226
- const vp = pages[0].getViewport({ scale: 1, rotation });
10318
+ const firstPage = pages.get(1);
10319
+ if (firstPage) {
10320
+ const vp = firstPage.getViewport({ scale: 1, rotation });
10227
10321
  setRawPageDims({ width: vp.width, height: vp.height });
10228
10322
  }
10229
10323
  }, [pages, rotation]);
@@ -10274,8 +10368,28 @@ var init_BookModeContainer = __esm({
10274
10368
  sepia: "bg-amber-50"
10275
10369
  };
10276
10370
  const themeClass = theme === "dark" ? "dark" : theme === "sepia" ? "sepia" : "";
10277
- const ready = !!document2 && !isLoadingPages && pages.length > 0;
10371
+ const ready = !!document2 && hasFirstPage;
10278
10372
  const hasContainer = containerSize.width > 0 && containerSize.height > 0;
10373
+ const pageChildren = useMemo14(() => {
10374
+ if (numPages === 0) return null;
10375
+ const out = new Array(numPages);
10376
+ for (let i = 0; i < numPages; i++) {
10377
+ const pageNumber = i + 1;
10378
+ out[i] = /* @__PURE__ */ jsx27(
10379
+ BookPage,
10380
+ {
10381
+ pageNumber,
10382
+ page: pages.get(pageNumber) ?? null,
10383
+ scale: renderScale,
10384
+ rotation,
10385
+ width: displayWidth,
10386
+ height: displayHeight
10387
+ },
10388
+ pageNumber
10389
+ );
10390
+ }
10391
+ return out;
10392
+ }, [numPages, pages, renderScale, rotation, displayWidth, displayHeight]);
10279
10393
  return /* @__PURE__ */ jsxs23(
10280
10394
  "div",
10281
10395
  {
@@ -10325,18 +10439,7 @@ var init_BookModeContainer = __esm({
10325
10439
  autoSize: false,
10326
10440
  renderOnlyPageLengthChange: false,
10327
10441
  disableFlipByClick: false,
10328
- children: pages.map((page, index) => /* @__PURE__ */ jsx27(
10329
- BookPage,
10330
- {
10331
- pageNumber: index + 1,
10332
- page,
10333
- scale: renderScale,
10334
- rotation,
10335
- width: displayWidth,
10336
- height: displayHeight
10337
- },
10338
- index
10339
- ))
10442
+ children: pageChildren
10340
10443
  }
10341
10444
  )
10342
10445
  ]
@@ -12512,7 +12615,7 @@ var AskAboutTrigger = memo36(function AskAboutTrigger2({
12512
12615
  // src/components/Minimap/Minimap.tsx
12513
12616
  init_hooks();
12514
12617
  init_utils();
12515
- import { memo as memo37, useMemo as useMemo14, useCallback as useCallback41 } from "react";
12618
+ import { memo as memo37, useMemo as useMemo15, useCallback as useCallback41 } from "react";
12516
12619
  import { Fragment as Fragment3, jsx as jsx38, jsxs as jsxs32 } from "react/jsx-runtime";
12517
12620
  var PageIndicator = memo37(function PageIndicator2({
12518
12621
  pageNumber,
@@ -12563,7 +12666,7 @@ var Minimap = memo37(function Minimap2({
12563
12666
  const currentPage = useViewerStore((s) => s.currentPage);
12564
12667
  const numPages = useViewerStore((s) => s.numPages);
12565
12668
  const goToPage = useViewerStore((s) => s.goToPage);
12566
- const bookmarkedPages = useMemo14(() => {
12669
+ const bookmarkedPages = useMemo15(() => {
12567
12670
  return new Set(bookmarks.map((b) => b.pageNumber));
12568
12671
  }, [bookmarks]);
12569
12672
  const compact = numPages > 50;
@@ -12583,7 +12686,7 @@ var Minimap = memo37(function Minimap2({
12583
12686
  },
12584
12687
  [currentPage, visitedPages, bookmarkedPages]
12585
12688
  );
12586
- const pageIndicators = useMemo14(() => {
12689
+ const pageIndicators = useMemo15(() => {
12587
12690
  const pages = [];
12588
12691
  for (let i = 1; i <= numPages; i++) {
12589
12692
  pages.push(
@@ -13085,7 +13188,7 @@ init_PDFLoadingScreen2();
13085
13188
  // src/components/TutorMode/TutorModeContainer.tsx
13086
13189
  init_PDFPage2();
13087
13190
  init_hooks();
13088
- import { useEffect as useEffect28, useMemo as useMemo15, useRef as useRef27, useState as useState30 } from "react";
13191
+ import { useEffect as useEffect28, useMemo as useMemo16, useRef as useRef27, useState as useState30 } from "react";
13089
13192
  import { useStore as useStore2 } from "zustand";
13090
13193
 
13091
13194
  // src/components/TutorMode/CameraView.tsx
@@ -14811,6 +14914,7 @@ function SubtitleBar({ text }) {
14811
14914
  // src/director/storyboard-engine.ts
14812
14915
  init_narration_store();
14813
14916
  init_camera_math();
14917
+ init_mobile_config();
14814
14918
  var DEFAULT_MIN_OVERLAY_MS = 3500;
14815
14919
  var StoryboardEngine = class {
14816
14920
  constructor(deps) {
@@ -14830,6 +14934,7 @@ var StoryboardEngine = class {
14830
14934
  this.overlayRemovalTimers = /* @__PURE__ */ new Map();
14831
14935
  this.currentStoryboardId = 0;
14832
14936
  this.deps = deps;
14937
+ this.singleActiveOverlay = deps.singleActiveOverlay ?? isIOSMobile();
14833
14938
  }
14834
14939
  /**
14835
14940
  * Execute a new storyboard. Cancels in-flight steps from the previous storyboard
@@ -14999,6 +15104,10 @@ var StoryboardEngine = class {
14999
15104
  createdAt: Date.now(),
15000
15105
  expiresAt: Date.now() + visibleMs
15001
15106
  };
15107
+ if (this.singleActiveOverlay) {
15108
+ this.cancelAllRemovalTimers();
15109
+ narrationStore.getState().clearOverlays();
15110
+ }
15002
15111
  narrationStore.getState().addOverlay(overlay);
15003
15112
  const timer = setTimeout(() => {
15004
15113
  narrationStore.getState().removeOverlay(overlay.id);
@@ -16071,7 +16180,7 @@ function TutorModeContainer({
16071
16180
  className
16072
16181
  }) {
16073
16182
  const containerRef = useRef27(null);
16074
- const index = useMemo15(() => buildBBoxIndex(bboxData), [bboxData]);
16183
+ const index = useMemo16(() => buildBBoxIndex(bboxData), [bboxData]);
16075
16184
  const {
16076
16185
  document: document2,
16077
16186
  currentPage: viewerCurrentPage,