pdfjs-reader-core 0.5.7 → 0.5.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.d.cts CHANGED
@@ -1858,40 +1858,19 @@ interface TutorModeContainerProps {
1858
1858
  * Added in v0.4.2.
1859
1859
  */
1860
1860
  onPageChange?: (page: number) => void;
1861
- /**
1862
- * PDF-points → CSS-pixels scale factor. Direct override: when set, the
1863
- * tutor renders the PDF at exactly this scale regardless of source DPI.
1864
- * Takes precedence over `scale` when both are passed.
1865
- *
1866
- * If neither `renderScale` nor `scale` is passed:
1867
- * - Desktop (`pointer: fine`): defaults to `page.dpi / 72` — identical
1868
- * to pre-v0.6 behavior.
1869
- * - Touch (`pointer: coarse`): defaults to fit-to-viewport — much
1870
- * smaller canvas on mobile, sidesteps the iOS Safari crash root
1871
- * cause even when bbox data ships at very high source DPI.
1872
- *
1873
- * Added in v0.6.0.
1874
- */
1875
- renderScale?: number;
1876
1861
  className?: string;
1877
1862
  }
1878
1863
  /** Build a cross-page/block index from the raw bbox list. */
1879
1864
  declare function buildBBoxIndex(bboxData: PageBBoxData[]): BBoxIndex;
1880
- declare function TutorModeContainer({ pageNumber, bboxData, narrationStore, scale, rotation, currentChunk, llm, idleTimeoutMs, llmTimeoutMs, embeddingProvider, showSubtitles, showExitButton, onExitTutorMode, minOverlayDurationMs, backgroundColor, loadingComponent, onPageChange, storyboardProvider, renderScale, className, }: TutorModeContainerProps): react_jsx_runtime.JSX.Element;
1865
+ declare function TutorModeContainer({ pageNumber, bboxData, narrationStore, scale, rotation, currentChunk, llm, idleTimeoutMs, llmTimeoutMs, embeddingProvider, showSubtitles, showExitButton, onExitTutorMode, minOverlayDurationMs, backgroundColor, loadingComponent, onPageChange, storyboardProvider, className, }: TutorModeContainerProps): react_jsx_runtime.JSX.Element;
1881
1866
 
1882
1867
  interface CinemaLayerProps {
1883
1868
  page: PageBBoxData;
1884
1869
  index: BBoxIndex;
1885
1870
  overlays: ActiveOverlay[];
1886
- /**
1887
- * Factor converting source-DPI pixel coords (the bbox space every
1888
- * overlay primitive uses internally) into CSS-pixel coords. Applied
1889
- * as `transform: scale(coordScale)` on the outer element so individual
1890
- * overlay components don't need to know about CSS vs DPI space.
1891
- */
1892
- coordScale: number;
1871
+ scale: number;
1893
1872
  }
1894
- declare function CinemaLayer({ page, index, overlays, coordScale, }: CinemaLayerProps): react_jsx_runtime.JSX.Element | null;
1873
+ declare function CinemaLayer({ page, index, overlays, scale, }: CinemaLayerProps): react_jsx_runtime.JSX.Element;
1895
1874
 
1896
1875
  interface CameraViewProps {
1897
1876
  camera: CameraState;
@@ -2086,14 +2065,18 @@ declare class StoryboardEngine {
2086
2065
  /** Cancel every removal timer (used by resetVisuals and destroy). */
2087
2066
  private cancelAllRemovalTimers;
2088
2067
  /**
2089
- * Full destructor — cancels both pending step timers AND overlay removal
2090
- * timers. Use this in component unmount/cleanup. The per-storyboard
2091
- * `cancelPending()` deliberately leaves overlay removal timers alone so a
2092
- * mid-flight overlay doesn't get stranded (see `overlayRemovalTimers` doc),
2093
- * but at destruction time we must release every timer or their closures
2068
+ * Full destructor — cancels BOTH pending step timers AND overlay
2069
+ * removal timers. Use this in component unmount / cleanup.
2070
+ *
2071
+ * The per-storyboard `cancelPending()` deliberately leaves overlay
2072
+ * removal timers alone so a mid-flight overlay doesn't get stranded
2073
+ * when a new storyboard arrives (see `overlayRemovalTimers` doc).
2074
+ * That invariant is correct inside a session, but at teardown we
2075
+ * must release every timer — otherwise their setTimeout closures
2094
2076
  * keep `deps` (narrationStore, the full bboxIndex) alive beyond the
2095
- * lifetime of this engine. On iOS Safari with many engine recreations
2096
- * this causes cumulative memory pressure and eventual tab reload.
2077
+ * lifetime of this engine. Over many component recreations on iOS
2078
+ * Safari (viewport state churns during address-bar scroll
2079
+ * animation), that cumulative retention contributes to tab reloads.
2097
2080
  */
2098
2081
  destroy(): void;
2099
2082
  /** Test-only — exposes internal queue sizes for regression tests. */
package/dist/index.d.ts CHANGED
@@ -1858,40 +1858,19 @@ interface TutorModeContainerProps {
1858
1858
  * Added in v0.4.2.
1859
1859
  */
1860
1860
  onPageChange?: (page: number) => void;
1861
- /**
1862
- * PDF-points → CSS-pixels scale factor. Direct override: when set, the
1863
- * tutor renders the PDF at exactly this scale regardless of source DPI.
1864
- * Takes precedence over `scale` when both are passed.
1865
- *
1866
- * If neither `renderScale` nor `scale` is passed:
1867
- * - Desktop (`pointer: fine`): defaults to `page.dpi / 72` — identical
1868
- * to pre-v0.6 behavior.
1869
- * - Touch (`pointer: coarse`): defaults to fit-to-viewport — much
1870
- * smaller canvas on mobile, sidesteps the iOS Safari crash root
1871
- * cause even when bbox data ships at very high source DPI.
1872
- *
1873
- * Added in v0.6.0.
1874
- */
1875
- renderScale?: number;
1876
1861
  className?: string;
1877
1862
  }
1878
1863
  /** Build a cross-page/block index from the raw bbox list. */
1879
1864
  declare function buildBBoxIndex(bboxData: PageBBoxData[]): BBoxIndex;
1880
- declare function TutorModeContainer({ pageNumber, bboxData, narrationStore, scale, rotation, currentChunk, llm, idleTimeoutMs, llmTimeoutMs, embeddingProvider, showSubtitles, showExitButton, onExitTutorMode, minOverlayDurationMs, backgroundColor, loadingComponent, onPageChange, storyboardProvider, renderScale, className, }: TutorModeContainerProps): react_jsx_runtime.JSX.Element;
1865
+ declare function TutorModeContainer({ pageNumber, bboxData, narrationStore, scale, rotation, currentChunk, llm, idleTimeoutMs, llmTimeoutMs, embeddingProvider, showSubtitles, showExitButton, onExitTutorMode, minOverlayDurationMs, backgroundColor, loadingComponent, onPageChange, storyboardProvider, className, }: TutorModeContainerProps): react_jsx_runtime.JSX.Element;
1881
1866
 
1882
1867
  interface CinemaLayerProps {
1883
1868
  page: PageBBoxData;
1884
1869
  index: BBoxIndex;
1885
1870
  overlays: ActiveOverlay[];
1886
- /**
1887
- * Factor converting source-DPI pixel coords (the bbox space every
1888
- * overlay primitive uses internally) into CSS-pixel coords. Applied
1889
- * as `transform: scale(coordScale)` on the outer element so individual
1890
- * overlay components don't need to know about CSS vs DPI space.
1891
- */
1892
- coordScale: number;
1871
+ scale: number;
1893
1872
  }
1894
- declare function CinemaLayer({ page, index, overlays, coordScale, }: CinemaLayerProps): react_jsx_runtime.JSX.Element | null;
1873
+ declare function CinemaLayer({ page, index, overlays, scale, }: CinemaLayerProps): react_jsx_runtime.JSX.Element;
1895
1874
 
1896
1875
  interface CameraViewProps {
1897
1876
  camera: CameraState;
@@ -2086,14 +2065,18 @@ declare class StoryboardEngine {
2086
2065
  /** Cancel every removal timer (used by resetVisuals and destroy). */
2087
2066
  private cancelAllRemovalTimers;
2088
2067
  /**
2089
- * Full destructor — cancels both pending step timers AND overlay removal
2090
- * timers. Use this in component unmount/cleanup. The per-storyboard
2091
- * `cancelPending()` deliberately leaves overlay removal timers alone so a
2092
- * mid-flight overlay doesn't get stranded (see `overlayRemovalTimers` doc),
2093
- * but at destruction time we must release every timer or their closures
2068
+ * Full destructor — cancels BOTH pending step timers AND overlay
2069
+ * removal timers. Use this in component unmount / cleanup.
2070
+ *
2071
+ * The per-storyboard `cancelPending()` deliberately leaves overlay
2072
+ * removal timers alone so a mid-flight overlay doesn't get stranded
2073
+ * when a new storyboard arrives (see `overlayRemovalTimers` doc).
2074
+ * That invariant is correct inside a session, but at teardown we
2075
+ * must release every timer — otherwise their setTimeout closures
2094
2076
  * keep `deps` (narrationStore, the full bboxIndex) alive beyond the
2095
- * lifetime of this engine. On iOS Safari with many engine recreations
2096
- * this causes cumulative memory pressure and eventual tab reload.
2077
+ * lifetime of this engine. Over many component recreations on iOS
2078
+ * Safari (viewport state churns during address-bar scroll
2079
+ * animation), that cumulative retention contributes to tab reloads.
2097
2080
  */
2098
2081
  destroy(): void;
2099
2082
  /** Test-only — exposes internal queue sizes for regression tests. */
package/dist/index.js CHANGED
@@ -9154,7 +9154,7 @@ var init_DocumentContainer = __esm({
9154
9154
  const containerRef = useRef16(null);
9155
9155
  const documentRef = useRef16(null);
9156
9156
  const baseScaleRef = useRef16(scale);
9157
- const isTouchDevice2 = useIsTouchDevice();
9157
+ const isTouchDevice = useIsTouchDevice();
9158
9158
  const documentLoadingState = useViewerStore((s) => s.documentLoadingState);
9159
9159
  const { selection, clearSelection, copySelection } = useTextSelection();
9160
9160
  const handlePinchZoom = useCallback29(
@@ -9185,7 +9185,7 @@ var init_DocumentContainer = __esm({
9185
9185
  onSwipeLeft: handleSwipeLeft,
9186
9186
  onSwipeRight: handleSwipeRight,
9187
9187
  onDoubleTap: handleDoubleTap,
9188
- enabled: enableTouchGestures && isTouchDevice2,
9188
+ enabled: enableTouchGestures && isTouchDevice,
9189
9189
  swipeThreshold: 50,
9190
9190
  doubleTapInterval: 300
9191
9191
  });
@@ -9429,7 +9429,7 @@ var init_VirtualizedDocumentContainer = __esm({
9429
9429
  const pageCache = useRef17(/* @__PURE__ */ new Map());
9430
9430
  const pageDimensionsCache = useRef17(/* @__PURE__ */ new Map());
9431
9431
  const baseScaleRef = useRef17(scale);
9432
- const isTouchDevice2 = useIsTouchDevice();
9432
+ const isTouchDevice = useIsTouchDevice();
9433
9433
  const [visiblePages, setVisiblePages] = useState19([1]);
9434
9434
  const [pageObjects, setPageObjects] = useState19(/* @__PURE__ */ new Map());
9435
9435
  const [totalHeight, setTotalHeight] = useState19(0);
@@ -9632,7 +9632,7 @@ var init_VirtualizedDocumentContainer = __esm({
9632
9632
  onPinchZoom: handlePinchZoom,
9633
9633
  onSwipeLeft: nextPage,
9634
9634
  onSwipeRight: previousPage,
9635
- enabled: enableTouchGestures && isTouchDevice2
9635
+ enabled: enableTouchGestures && isTouchDevice
9636
9636
  });
9637
9637
  const setContainerRef = useCallback30(
9638
9638
  (element) => {
@@ -9840,7 +9840,7 @@ var init_DualPageContainer = __esm({
9840
9840
  const containerRef = useRef18(null);
9841
9841
  const documentRef = useRef18(null);
9842
9842
  const baseScaleRef = useRef18(scale);
9843
- const isTouchDevice2 = useIsTouchDevice();
9843
+ const isTouchDevice = useIsTouchDevice();
9844
9844
  const [leftPage, setLeftPage] = useState20(null);
9845
9845
  const [rightPage, setRightPage] = useState20(null);
9846
9846
  const [isLoading, setIsLoading] = useState20(false);
@@ -9983,7 +9983,7 @@ var init_DualPageContainer = __esm({
9983
9983
  onPinchZoom: handlePinchZoom,
9984
9984
  onSwipeLeft: goToNextSpread,
9985
9985
  onSwipeRight: goToPreviousSpread,
9986
- enabled: enableTouchGestures && isTouchDevice2
9986
+ enabled: enableTouchGestures && isTouchDevice
9987
9987
  });
9988
9988
  const setContainerRef = useCallback31(
9989
9989
  (element) => {
@@ -13374,13 +13374,13 @@ function AnimatedUnderline({ bbox, action }) {
13374
13374
  const blotX = x2 + 4;
13375
13375
  const blotY = y;
13376
13376
  const strokeWeight = action.style === "wavy" ? 3 : 4;
13377
- const xPad = 8;
13378
- const yAbove = 8;
13379
- const yBelow = 24;
13380
- const svgX = x1 - xPad;
13381
- const svgY = y - yAbove;
13382
- const svgW = x2 - x1 + 2 * xPad;
13383
- const svgH = yAbove + yBelow;
13377
+ const uxPad = 8;
13378
+ const uAbove = 8;
13379
+ const uBelow = 24;
13380
+ const svgX = x1 - uxPad;
13381
+ const svgY = y - uAbove;
13382
+ const svgW = x2 - x1 + 2 * uxPad;
13383
+ const svgH = uAbove + uBelow;
13384
13384
  return /* @__PURE__ */ jsxs36(
13385
13385
  "svg",
13386
13386
  {
@@ -13455,7 +13455,12 @@ function AnimatedUnderline({ bbox, action }) {
13455
13455
  // src/components/TutorMode/AnimatedHighlight.tsx
13456
13456
  import { motion as motion4 } from "framer-motion";
13457
13457
  import { jsx as jsx44 } from "react/jsx-runtime";
13458
- var WASH = "rgba(195, 145, 10, 0.32)";
13458
+ var DEFAULT_HUE = "rgb(230, 180, 34)";
13459
+ var WASH_OPACITY = 0.28;
13460
+ function stripAlpha(color) {
13461
+ const m = color.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i);
13462
+ return m ? `rgb(${m[1]}, ${m[2]}, ${m[3]})` : color;
13463
+ }
13459
13464
  function AnimatedHighlight({ bbox, action }) {
13460
13465
  const [x1, y1, x2, y2] = bbox;
13461
13466
  const h = Math.max(1, y2 - y1);
@@ -13464,7 +13469,7 @@ function AnimatedHighlight({ bbox, action }) {
13464
13469
  const yBot = y2 + bleed;
13465
13470
  const duration = action.draw_duration_ms / 1e3;
13466
13471
  const isDefaultColour = !action.color || action.color === "rgba(250, 204, 21, 0.35)" || action.color === "rgba(250,204,21,0.35)";
13467
- const fill = isDefaultColour ? WASH : action.color;
13472
+ const fill = stripAlpha(isDefaultColour ? DEFAULT_HUE : action.color);
13468
13473
  const taper = Math.min(6, h * 0.2);
13469
13474
  const pathD = `
13470
13475
  M ${x1 - 2} ${yTop + taper}
@@ -13477,11 +13482,11 @@ function AnimatedHighlight({ bbox, action }) {
13477
13482
  L ${x1 - 2} ${yBot - taper}
13478
13483
  Z
13479
13484
  `;
13480
- const pad = 8;
13481
- const svgX = x1 - pad;
13482
- const svgY = yTop - pad;
13483
- const svgW = x2 - x1 + 2 * pad;
13484
- const svgH = yBot - yTop + 2 * pad;
13485
+ const svgPad = 8;
13486
+ const svgX = x1 - svgPad;
13487
+ const svgY = yTop - svgPad;
13488
+ const svgW = x2 - x1 + 2 * svgPad;
13489
+ const svgH = yBot - yTop + 2 * svgPad;
13485
13490
  return /* @__PURE__ */ jsx44(
13486
13491
  "svg",
13487
13492
  {
@@ -13501,6 +13506,7 @@ function AnimatedHighlight({ bbox, action }) {
13501
13506
  {
13502
13507
  d: pathD,
13503
13508
  fill,
13509
+ fillOpacity: WASH_OPACITY,
13504
13510
  initial: { clipPath: `inset(0 100% 0 0)` },
13505
13511
  animate: { clipPath: `inset(0 0% 0 0)` },
13506
13512
  exit: { opacity: 0 },
@@ -13512,7 +13518,6 @@ function AnimatedHighlight({ bbox, action }) {
13512
13518
  }
13513
13519
 
13514
13520
  // src/components/TutorMode/PulseOverlay.tsx
13515
- import { useId as useId2 } from "react";
13516
13521
  import { motion as motion5 } from "framer-motion";
13517
13522
  import { jsx as jsx45, jsxs as jsxs37 } from "react/jsx-runtime";
13518
13523
  var INTENSITY = {
@@ -13521,7 +13526,6 @@ var INTENSITY = {
13521
13526
  strong: { bracketLen: 26, strokeWeight: 3, coreOpacity: 1, ringScale: 1.22 }
13522
13527
  };
13523
13528
  function PulseOverlay({ bbox, action }) {
13524
- const pulseId = useId2();
13525
13529
  const [x1, y1, x2, y2] = bbox;
13526
13530
  const w = Math.max(1, x2 - x1);
13527
13531
  const h = Math.max(1, y2 - y1);
@@ -13530,13 +13534,13 @@ function PulseOverlay({ bbox, action }) {
13530
13534
  const spec = INTENSITY[action.intensity] ?? INTENSITY.normal;
13531
13535
  const L = Math.min(spec.bracketLen, Math.min(w, h) / 2.5);
13532
13536
  const PAD = 6;
13537
+ const ringExtentX = (w / 2 + PAD) * spec.ringScale - w / 2;
13538
+ const ringExtentY = (h / 2 + PAD) * spec.ringScale - h / 2;
13539
+ const glowExtentX = (w / 2 + 10) * spec.ringScale - w / 2 + 24;
13540
+ const glowExtentY = (h / 2 + 10) * spec.ringScale - h / 2 + 24;
13533
13541
  const bracketExtent = PAD + L + 8;
13534
- const glowExtent = (w / 2 + 24) * spec.ringScale - w / 2;
13535
- const ringExtent = (w / 2 + PAD) * spec.ringScale - w / 2;
13536
- const glowExtentV = (h / 2 + 24) * spec.ringScale - h / 2;
13537
- const ringExtentV = (h / 2 + PAD) * spec.ringScale - h / 2;
13538
- const svgPadX = Math.ceil(Math.max(bracketExtent, glowExtent, ringExtent) + 4);
13539
- const svgPadY = Math.ceil(Math.max(bracketExtent, glowExtentV, ringExtentV) + 4);
13542
+ const svgPadX = Math.ceil(Math.max(40, ringExtentX, glowExtentX, bracketExtent));
13543
+ const svgPadY = Math.ceil(Math.max(40, ringExtentY, glowExtentY, bracketExtent));
13540
13544
  const svgX = x1 - svgPadX;
13541
13545
  const svgY = y1 - svgPadY;
13542
13546
  const svgW = w + 2 * svgPadX;
@@ -13556,22 +13560,18 @@ function PulseOverlay({ bbox, action }) {
13556
13560
  },
13557
13561
  "data-role": "pulse",
13558
13562
  children: [
13559
- /* @__PURE__ */ jsx45("defs", { children: /* @__PURE__ */ jsxs37("radialGradient", { id: `${pulseId}-glow`, cx: "50%", cy: "50%", r: "50%", children: [
13560
- /* @__PURE__ */ jsx45("stop", { offset: "0%", stopColor: ACCENT_GLOW, stopOpacity: 1 }),
13561
- /* @__PURE__ */ jsx45("stop", { offset: "60%", stopColor: ACCENT_GLOW, stopOpacity: 0.45 }),
13562
- /* @__PURE__ */ jsx45("stop", { offset: "100%", stopColor: ACCENT_GLOW, stopOpacity: 0 })
13563
- ] }) }),
13564
13563
  /* @__PURE__ */ jsx45(
13565
13564
  motion5.ellipse,
13566
13565
  {
13567
13566
  cx,
13568
13567
  cy,
13569
- rx: w / 2 + 24,
13570
- ry: h / 2 + 24,
13571
- fill: `url(#${pulseId}-glow)`,
13568
+ rx: w / 2 + 10,
13569
+ ry: h / 2 + 10,
13570
+ fill: ACCENT_GLOW,
13572
13571
  style: {
13573
13572
  transformOrigin: `${cx}px ${cy}px`,
13574
- transformBox: "fill-box"
13573
+ transformBox: "fill-box",
13574
+ filter: "blur(16px)"
13575
13575
  },
13576
13576
  initial: { opacity: 0, scale: 0.95 },
13577
13577
  animate: {
@@ -13702,7 +13702,7 @@ function Bracket({
13702
13702
  }
13703
13703
 
13704
13704
  // src/components/TutorMode/CalloutArrow.tsx
13705
- import { useId as useId3 } from "react";
13705
+ import { useId as useId2 } from "react";
13706
13706
  import { motion as motion6 } from "framer-motion";
13707
13707
  import { jsx as jsx46, jsxs as jsxs38 } from "react/jsx-runtime";
13708
13708
  function centerOf(b) {
@@ -13740,7 +13740,7 @@ function arrowPath(from, to, curve) {
13740
13740
  return `M ${from.x} ${from.y} Q ${cx} ${cy} ${to.x} ${to.y}`;
13741
13741
  }
13742
13742
  function CalloutArrow({ fromBbox, toBbox, action }) {
13743
- const markerId = useId3();
13743
+ const markerId = useId2();
13744
13744
  const glowId = `${markerId}-glow`;
13745
13745
  const { from, to } = edgePoints(fromBbox, toBbox);
13746
13746
  const d = arrowPath(from, to, action.curve);
@@ -13913,9 +13913,8 @@ function CinemaLayer({
13913
13913
  page,
13914
13914
  index,
13915
13915
  overlays,
13916
- coordScale
13916
+ scale
13917
13917
  }) {
13918
- if (overlays.length === 0) return null;
13919
13918
  return /* @__PURE__ */ jsx48(
13920
13919
  "div",
13921
13920
  {
@@ -13924,7 +13923,7 @@ function CinemaLayer({
13924
13923
  position: "absolute",
13925
13924
  inset: 0,
13926
13925
  transformOrigin: "0 0",
13927
- transform: `scale(${coordScale})`,
13926
+ transform: `scale(${scale})`,
13928
13927
  width: page.page_dimensions.width,
13929
13928
  height: page.page_dimensions.height,
13930
13929
  pointerEvents: "none",
@@ -14514,8 +14513,7 @@ function LabelOverlay({
14514
14513
  index,
14515
14514
  currentPage,
14516
14515
  camera,
14517
- viewport,
14518
- coordScale
14516
+ viewport
14519
14517
  }) {
14520
14518
  const labels = overlays.filter((o) => o.kind === "label");
14521
14519
  const page = index.byPage.get(currentPage);
@@ -14542,8 +14540,7 @@ function LabelOverlay({
14542
14540
  a.position,
14543
14541
  page,
14544
14542
  camera,
14545
- viewport,
14546
- coordScale
14543
+ viewport
14547
14544
  );
14548
14545
  return /* @__PURE__ */ jsx52(
14549
14546
  StickyLabel,
@@ -14557,7 +14554,7 @@ function LabelOverlay({
14557
14554
  }
14558
14555
  );
14559
14556
  }
14560
- function computeScreenAnchor(bbox, where, page, camera, viewport, coordScale) {
14557
+ function computeScreenAnchor(bbox, where, page, camera, viewport) {
14561
14558
  const [x1, y1, x2, y2] = bbox;
14562
14559
  const pageCX = page.page_dimensions.width / 2;
14563
14560
  const pageCY = page.page_dimensions.height / 2;
@@ -14583,8 +14580,8 @@ function computeScreenAnchor(bbox, where, page, camera, viewport, coordScale) {
14583
14580
  px = (x1 + x2) / 2;
14584
14581
  py = y1;
14585
14582
  }
14586
- const screenX = viewport.width / 2 + camera.x + (px - pageCX) * coordScale * camera.scale;
14587
- const screenY = viewport.height / 2 + camera.y + (py - pageCY) * coordScale * camera.scale;
14583
+ const screenX = viewport.width / 2 + camera.x + (px - pageCX) * camera.scale;
14584
+ const screenY = viewport.height / 2 + camera.y + (py - pageCY) * camera.scale;
14588
14585
  return { x: screenX, y: screenY };
14589
14586
  }
14590
14587
 
@@ -14596,8 +14593,7 @@ function CalloutLabelOverlay({
14596
14593
  index,
14597
14594
  currentPage,
14598
14595
  camera,
14599
- viewport,
14600
- coordScale
14596
+ viewport
14601
14597
  }) {
14602
14598
  const callouts = overlays.filter(
14603
14599
  (o) => o.kind === "callout" && o.action.label
@@ -14626,8 +14622,7 @@ function CalloutLabelOverlay({
14626
14622
  toHit.block.bbox,
14627
14623
  page,
14628
14624
  camera,
14629
- viewport,
14630
- coordScale
14625
+ viewport
14631
14626
  );
14632
14627
  return /* @__PURE__ */ jsx53(
14633
14628
  CalloutLabelPill,
@@ -14642,7 +14637,7 @@ function CalloutLabelOverlay({
14642
14637
  }
14643
14638
  );
14644
14639
  }
14645
- function computePillAnchor(fromBbox, toBbox, page, camera, viewport, coordScale) {
14640
+ function computePillAnchor(fromBbox, toBbox, page, camera, viewport) {
14646
14641
  const aCX = (fromBbox[0] + fromBbox[2]) / 2;
14647
14642
  const aCY = (fromBbox[1] + fromBbox[3]) / 2;
14648
14643
  const bCX = (toBbox[0] + toBbox[2]) / 2;
@@ -14659,8 +14654,8 @@ function computePillAnchor(fromBbox, toBbox, page, camera, viewport, coordScale)
14659
14654
  const toY = bCY - uy * bOff;
14660
14655
  const pageCX = page.page_dimensions.width / 2;
14661
14656
  const pageCY = page.page_dimensions.height / 2;
14662
- const tipScreenX = viewport.width / 2 + camera.x + (toX - pageCX) * coordScale * camera.scale;
14663
- const tipScreenY = viewport.height / 2 + camera.y + (toY - pageCY) * coordScale * camera.scale;
14657
+ const tipScreenX = viewport.width / 2 + camera.x + (toX - pageCX) * camera.scale;
14658
+ const tipScreenY = viewport.height / 2 + camera.y + (toY - pageCY) * camera.scale;
14664
14659
  const isVertical = Math.abs(dy) >= Math.abs(dx);
14665
14660
  const OFFSET = resolvePillOffset(viewport.width);
14666
14661
  const MAX_PILL_W = resolveMaxPillW(viewport.width);
@@ -14803,41 +14798,6 @@ function SubtitleBar({ text }) {
14803
14798
  ) : null });
14804
14799
  }
14805
14800
 
14806
- // src/utils/render-scale.ts
14807
- function computeFitScale(input) {
14808
- const { pageWidthPt, pageHeightPt, viewport, paddingFactor = 0.95 } = input;
14809
- if (!Number.isFinite(pageWidthPt) || !Number.isFinite(pageHeightPt) || pageWidthPt <= 0 || pageHeightPt <= 0) {
14810
- return 1;
14811
- }
14812
- const w = Math.max(1, viewport.width);
14813
- const h = Math.max(1, viewport.height);
14814
- const fit = Math.min(w / pageWidthPt, h / pageHeightPt) * paddingFactor;
14815
- return Number.isFinite(fit) && fit > 0 ? fit : 1;
14816
- }
14817
- function computeCoordScale(input) {
14818
- const { renderScale, dpi } = input;
14819
- if (!Number.isFinite(renderScale) || renderScale <= 0) return 0;
14820
- if (!Number.isFinite(dpi) || dpi <= 0) return 1;
14821
- return renderScale * 72 / dpi;
14822
- }
14823
- function isTouchDevice() {
14824
- if (typeof window === "undefined") return false;
14825
- if (typeof window.matchMedia !== "function") return false;
14826
- try {
14827
- return window.matchMedia("(pointer: coarse)").matches;
14828
- } catch {
14829
- return false;
14830
- }
14831
- }
14832
- function resolveRenderScale(input) {
14833
- const { renderScaleProp, scaleProp, defaultRenderScale } = input;
14834
- if (Number.isFinite(renderScaleProp) && renderScaleProp > 0) {
14835
- return renderScaleProp;
14836
- }
14837
- const multiplier = Number.isFinite(scaleProp) && scaleProp > 0 ? scaleProp : 1;
14838
- return defaultRenderScale * multiplier;
14839
- }
14840
-
14841
14801
  // src/director/storyboard-engine.ts
14842
14802
  init_narration_store();
14843
14803
  init_camera_math();
@@ -14934,14 +14894,18 @@ var StoryboardEngine = class {
14934
14894
  this.overlayRemovalTimers.clear();
14935
14895
  }
14936
14896
  /**
14937
- * Full destructor — cancels both pending step timers AND overlay removal
14938
- * timers. Use this in component unmount/cleanup. The per-storyboard
14939
- * `cancelPending()` deliberately leaves overlay removal timers alone so a
14940
- * mid-flight overlay doesn't get stranded (see `overlayRemovalTimers` doc),
14941
- * but at destruction time we must release every timer or their closures
14897
+ * Full destructor — cancels BOTH pending step timers AND overlay
14898
+ * removal timers. Use this in component unmount / cleanup.
14899
+ *
14900
+ * The per-storyboard `cancelPending()` deliberately leaves overlay
14901
+ * removal timers alone so a mid-flight overlay doesn't get stranded
14902
+ * when a new storyboard arrives (see `overlayRemovalTimers` doc).
14903
+ * That invariant is correct inside a session, but at teardown we
14904
+ * must release every timer — otherwise their setTimeout closures
14942
14905
  * keep `deps` (narrationStore, the full bboxIndex) alive beyond the
14943
- * lifetime of this engine. On iOS Safari with many engine recreations
14944
- * this causes cumulative memory pressure and eventual tab reload.
14906
+ * lifetime of this engine. Over many component recreations on iOS
14907
+ * Safari (viewport state churns during address-bar scroll
14908
+ * animation), that cumulative retention contributes to tab reloads.
14945
14909
  */
14946
14910
  destroy() {
14947
14911
  this.cancelPending();
@@ -16094,7 +16058,6 @@ function TutorModeContainer({
16094
16058
  loadingComponent,
16095
16059
  onPageChange,
16096
16060
  storyboardProvider,
16097
- renderScale,
16098
16061
  className
16099
16062
  }) {
16100
16063
  const containerRef = useRef27(null);
@@ -16109,27 +16072,6 @@ function TutorModeContainer({
16109
16072
  const [viewport, setViewport] = useState30({ width: 800, height: 1e3 });
16110
16073
  const camera = useStore2(narrationStore, (s) => s.camera);
16111
16074
  const activeOverlays = useStore2(narrationStore, (s) => s.activeOverlays);
16112
- const page = index.byPage.get(pageNumber);
16113
- const pagePointsW = page ? page.page_dimensions.width * 72 / page.page_dimensions.dpi : 0;
16114
- const pagePointsH = page ? page.page_dimensions.height * 72 / page.page_dimensions.dpi : 0;
16115
- const touch = isTouchDevice();
16116
- const defaultRenderScale = page ? touch ? computeFitScale({
16117
- pageWidthPt: pagePointsW,
16118
- pageHeightPt: pagePointsH,
16119
- viewport
16120
- }) : page.page_dimensions.dpi / 72 : 1;
16121
- const effectiveRenderScale = resolveRenderScale({
16122
- renderScaleProp: renderScale,
16123
- scaleProp: scale,
16124
- defaultRenderScale
16125
- });
16126
- const coordScale = page ? computeCoordScale({
16127
- renderScale: effectiveRenderScale,
16128
- dpi: page.page_dimensions.dpi
16129
- }) : 1;
16130
- const rasterScale = effectiveRenderScale;
16131
- const baseW = pagePointsW * effectiveRenderScale;
16132
- const baseH = pagePointsH * effectiveRenderScale;
16133
16075
  useEffect28(() => {
16134
16076
  if (numPages <= 0) return;
16135
16077
  if (pageNumber < 1 || pageNumber > numPages) return;
@@ -16145,13 +16087,7 @@ function TutorModeContainer({
16145
16087
  useEffect28(() => {
16146
16088
  if (!containerRef.current) return;
16147
16089
  const el = containerRef.current;
16148
- const update = () => {
16149
- const w = el.clientWidth;
16150
- const h = el.clientHeight;
16151
- setViewport(
16152
- (prev) => prev.width === w && prev.height === h ? prev : { width: w, height: h }
16153
- );
16154
- };
16090
+ const update = () => setViewport({ width: el.clientWidth, height: el.clientHeight });
16155
16091
  update();
16156
16092
  const ro = new ResizeObserver(update);
16157
16093
  ro.observe(el);
@@ -16179,29 +16115,23 @@ function TutorModeContainer({
16179
16115
  const page2 = index.byPage.get(pageNumber);
16180
16116
  if (!page2) return;
16181
16117
  if (viewport.width === 0 || viewport.height === 0) return;
16182
- if (baseW === 0 || baseH === 0) return;
16183
16118
  if (narrationStore.getState().activeOverlays.length > 0) return;
16184
- const fit = Math.min(viewport.width / baseW, viewport.height / baseH) * 0.95;
16119
+ const fit = Math.min(
16120
+ viewport.width / page2.page_dimensions.width,
16121
+ viewport.height / page2.page_dimensions.height
16122
+ ) * 0.95;
16185
16123
  narrationStore.getState().setCamera({ scale: fit, x: 0, y: 0 });
16186
- }, [pageNumber, viewport, index, narrationStore, baseW, baseH]);
16187
- const viewportRef = useRef27(viewport);
16188
- useEffect28(() => {
16189
- viewportRef.current = viewport;
16190
- }, [viewport]);
16124
+ }, [pageNumber, viewport, index, narrationStore]);
16191
16125
  const engineRef = useRef27(null);
16192
16126
  useEffect28(() => {
16193
- const engine = new StoryboardEngine({
16127
+ engineRef.current = new StoryboardEngine({
16194
16128
  narrationStore,
16195
16129
  bboxIndex: index,
16196
- getViewport: () => viewportRef.current,
16130
+ getViewport: () => viewport,
16197
16131
  minOverlayDurationMs
16198
16132
  });
16199
- engineRef.current = engine;
16200
- return () => {
16201
- engine.destroy();
16202
- if (engineRef.current === engine) engineRef.current = null;
16203
- };
16204
- }, [narrationStore, index, minOverlayDurationMs]);
16133
+ return () => engineRef.current?.destroy();
16134
+ }, [narrationStore, index, viewport, minOverlayDurationMs]);
16205
16135
  const abortRef = useRef27(null);
16206
16136
  const debounceRef = useRef27(null);
16207
16137
  const lastChunkRef = useRef27(null);
@@ -16382,6 +16312,11 @@ function TutorModeContainer({
16382
16312
  }, idleTimeoutMs + 100);
16383
16313
  return () => clearTimeout(t);
16384
16314
  }, [currentChunk, idleTimeoutMs, narrationStore]);
16315
+ const page = index.byPage.get(pageNumber);
16316
+ const dpiScale = page ? page.page_dimensions.dpi / 72 : 1;
16317
+ const rasterScale = dpiScale * (scale || 1);
16318
+ const baseW = page ? page.page_dimensions.width * (scale || 1) : 0;
16319
+ const baseH = page ? page.page_dimensions.height * (scale || 1) : 0;
16385
16320
  const isReady = !!page && !!pageProxy;
16386
16321
  return /* @__PURE__ */ jsxs41(
16387
16322
  "div",
@@ -16460,7 +16395,7 @@ function TutorModeContainer({
16460
16395
  page,
16461
16396
  index,
16462
16397
  overlays: activeOverlays,
16463
- coordScale
16398
+ scale: scale || 1
16464
16399
  }
16465
16400
  )
16466
16401
  ]
@@ -16473,8 +16408,7 @@ function TutorModeContainer({
16473
16408
  index,
16474
16409
  currentPage: pageNumber,
16475
16410
  camera,
16476
- viewport,
16477
- coordScale
16411
+ viewport
16478
16412
  }
16479
16413
  ),
16480
16414
  /* @__PURE__ */ jsx55(
@@ -16484,8 +16418,7 @@ function TutorModeContainer({
16484
16418
  index,
16485
16419
  currentPage: pageNumber,
16486
16420
  camera,
16487
- viewport,
16488
- coordScale
16421
+ viewport
16489
16422
  }
16490
16423
  ),
16491
16424
  /* @__PURE__ */ jsx55(GhostReferenceOverlay, { overlays: activeOverlays, index })