pdfjs-reader-core 0.5.7 → 0.5.9

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",
@@ -14409,10 +14408,14 @@ function StickyLabel({ screenAnchor, action }) {
14409
14408
  letterSpacing: 0.6,
14410
14409
  textTransform: "uppercase",
14411
14410
  fontWeight: 500,
14412
- whiteSpace: "nowrap",
14411
+ // Wrap instead of truncating with an ellipsis. Short labels stay
14412
+ // single-line naturally; longer ones grow in height rather than
14413
+ // losing their tail. `overflowWrap: 'anywhere'` keeps a stray
14414
+ // long word from pushing the pill past `maxWidth` (normal word
14415
+ // breaking stops at whitespace and can't break inside words).
14413
14416
  maxWidth: PILL_MAX_W_BODY,
14414
- overflow: "hidden",
14415
- textOverflow: "ellipsis",
14417
+ whiteSpace: "normal",
14418
+ overflowWrap: "anywhere",
14416
14419
  // Warm two-layer shadow (matches GhostReference's palette).
14417
14420
  boxShadow: "0 1px 2px rgba(42, 36, 32, 0.12), 0 8px 18px -6px rgba(42, 36, 32, 0.22)",
14418
14421
  // Internal left accent rule — a 2px terracotta stripe.
@@ -14514,8 +14517,7 @@ function LabelOverlay({
14514
14517
  index,
14515
14518
  currentPage,
14516
14519
  camera,
14517
- viewport,
14518
- coordScale
14520
+ viewport
14519
14521
  }) {
14520
14522
  const labels = overlays.filter((o) => o.kind === "label");
14521
14523
  const page = index.byPage.get(currentPage);
@@ -14542,8 +14544,7 @@ function LabelOverlay({
14542
14544
  a.position,
14543
14545
  page,
14544
14546
  camera,
14545
- viewport,
14546
- coordScale
14547
+ viewport
14547
14548
  );
14548
14549
  return /* @__PURE__ */ jsx52(
14549
14550
  StickyLabel,
@@ -14557,7 +14558,7 @@ function LabelOverlay({
14557
14558
  }
14558
14559
  );
14559
14560
  }
14560
- function computeScreenAnchor(bbox, where, page, camera, viewport, coordScale) {
14561
+ function computeScreenAnchor(bbox, where, page, camera, viewport) {
14561
14562
  const [x1, y1, x2, y2] = bbox;
14562
14563
  const pageCX = page.page_dimensions.width / 2;
14563
14564
  const pageCY = page.page_dimensions.height / 2;
@@ -14583,8 +14584,8 @@ function computeScreenAnchor(bbox, where, page, camera, viewport, coordScale) {
14583
14584
  px = (x1 + x2) / 2;
14584
14585
  py = y1;
14585
14586
  }
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;
14587
+ const screenX = viewport.width / 2 + camera.x + (px - pageCX) * camera.scale;
14588
+ const screenY = viewport.height / 2 + camera.y + (py - pageCY) * camera.scale;
14588
14589
  return { x: screenX, y: screenY };
14589
14590
  }
14590
14591
 
@@ -14596,8 +14597,7 @@ function CalloutLabelOverlay({
14596
14597
  index,
14597
14598
  currentPage,
14598
14599
  camera,
14599
- viewport,
14600
- coordScale
14600
+ viewport
14601
14601
  }) {
14602
14602
  const callouts = overlays.filter(
14603
14603
  (o) => o.kind === "callout" && o.action.label
@@ -14626,8 +14626,7 @@ function CalloutLabelOverlay({
14626
14626
  toHit.block.bbox,
14627
14627
  page,
14628
14628
  camera,
14629
- viewport,
14630
- coordScale
14629
+ viewport
14631
14630
  );
14632
14631
  return /* @__PURE__ */ jsx53(
14633
14632
  CalloutLabelPill,
@@ -14642,7 +14641,7 @@ function CalloutLabelOverlay({
14642
14641
  }
14643
14642
  );
14644
14643
  }
14645
- function computePillAnchor(fromBbox, toBbox, page, camera, viewport, coordScale) {
14644
+ function computePillAnchor(fromBbox, toBbox, page, camera, viewport) {
14646
14645
  const aCX = (fromBbox[0] + fromBbox[2]) / 2;
14647
14646
  const aCY = (fromBbox[1] + fromBbox[3]) / 2;
14648
14647
  const bCX = (toBbox[0] + toBbox[2]) / 2;
@@ -14659,8 +14658,8 @@ function computePillAnchor(fromBbox, toBbox, page, camera, viewport, coordScale)
14659
14658
  const toY = bCY - uy * bOff;
14660
14659
  const pageCX = page.page_dimensions.width / 2;
14661
14660
  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;
14661
+ const tipScreenX = viewport.width / 2 + camera.x + (toX - pageCX) * camera.scale;
14662
+ const tipScreenY = viewport.height / 2 + camera.y + (toY - pageCY) * camera.scale;
14664
14663
  const isVertical = Math.abs(dy) >= Math.abs(dx);
14665
14664
  const OFFSET = resolvePillOffset(viewport.width);
14666
14665
  const MAX_PILL_W = resolveMaxPillW(viewport.width);
@@ -14713,10 +14712,13 @@ function CalloutLabelPill({
14713
14712
  letterSpacing: 0.6,
14714
14713
  textTransform: "uppercase",
14715
14714
  fontWeight: 500,
14716
- whiteSpace: "nowrap",
14715
+ // Wrap instead of truncating. Short labels stay single-line;
14716
+ // longer ones grow taller rather than losing their tail to an
14717
+ // ellipsis. `overflowWrap: 'anywhere'` guards against a stray
14718
+ // long word pushing the pill past `maxWidth`.
14717
14719
  maxWidth: PILL_MAX_W_CAPS,
14718
- overflow: "hidden",
14719
- textOverflow: "ellipsis",
14720
+ whiteSpace: "normal",
14721
+ overflowWrap: "anywhere",
14720
14722
  boxShadow: "0 1px 2px rgba(42, 36, 32, 0.12), 0 8px 18px -6px rgba(42, 36, 32, 0.22)",
14721
14723
  // Accent rule on the "inward" edge (the one closest to the arrow).
14722
14724
  backgroundImage: spec.accentGradient,
@@ -14803,41 +14805,6 @@ function SubtitleBar({ text }) {
14803
14805
  ) : null });
14804
14806
  }
14805
14807
 
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
14808
  // src/director/storyboard-engine.ts
14842
14809
  init_narration_store();
14843
14810
  init_camera_math();
@@ -14934,14 +14901,18 @@ var StoryboardEngine = class {
14934
14901
  this.overlayRemovalTimers.clear();
14935
14902
  }
14936
14903
  /**
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
14904
+ * Full destructor — cancels BOTH pending step timers AND overlay
14905
+ * removal timers. Use this in component unmount / cleanup.
14906
+ *
14907
+ * The per-storyboard `cancelPending()` deliberately leaves overlay
14908
+ * removal timers alone so a mid-flight overlay doesn't get stranded
14909
+ * when a new storyboard arrives (see `overlayRemovalTimers` doc).
14910
+ * That invariant is correct inside a session, but at teardown we
14911
+ * must release every timer — otherwise their setTimeout closures
14942
14912
  * 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.
14913
+ * lifetime of this engine. Over many component recreations on iOS
14914
+ * Safari (viewport state churns during address-bar scroll
14915
+ * animation), that cumulative retention contributes to tab reloads.
14945
14916
  */
14946
14917
  destroy() {
14947
14918
  this.cancelPending();
@@ -16094,7 +16065,6 @@ function TutorModeContainer({
16094
16065
  loadingComponent,
16095
16066
  onPageChange,
16096
16067
  storyboardProvider,
16097
- renderScale,
16098
16068
  className
16099
16069
  }) {
16100
16070
  const containerRef = useRef27(null);
@@ -16109,27 +16079,6 @@ function TutorModeContainer({
16109
16079
  const [viewport, setViewport] = useState30({ width: 800, height: 1e3 });
16110
16080
  const camera = useStore2(narrationStore, (s) => s.camera);
16111
16081
  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
16082
  useEffect28(() => {
16134
16083
  if (numPages <= 0) return;
16135
16084
  if (pageNumber < 1 || pageNumber > numPages) return;
@@ -16145,13 +16094,7 @@ function TutorModeContainer({
16145
16094
  useEffect28(() => {
16146
16095
  if (!containerRef.current) return;
16147
16096
  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
- };
16097
+ const update = () => setViewport({ width: el.clientWidth, height: el.clientHeight });
16155
16098
  update();
16156
16099
  const ro = new ResizeObserver(update);
16157
16100
  ro.observe(el);
@@ -16179,29 +16122,23 @@ function TutorModeContainer({
16179
16122
  const page2 = index.byPage.get(pageNumber);
16180
16123
  if (!page2) return;
16181
16124
  if (viewport.width === 0 || viewport.height === 0) return;
16182
- if (baseW === 0 || baseH === 0) return;
16183
16125
  if (narrationStore.getState().activeOverlays.length > 0) return;
16184
- const fit = Math.min(viewport.width / baseW, viewport.height / baseH) * 0.95;
16126
+ const fit = Math.min(
16127
+ viewport.width / page2.page_dimensions.width,
16128
+ viewport.height / page2.page_dimensions.height
16129
+ ) * 0.95;
16185
16130
  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]);
16131
+ }, [pageNumber, viewport, index, narrationStore]);
16191
16132
  const engineRef = useRef27(null);
16192
16133
  useEffect28(() => {
16193
- const engine = new StoryboardEngine({
16134
+ engineRef.current = new StoryboardEngine({
16194
16135
  narrationStore,
16195
16136
  bboxIndex: index,
16196
- getViewport: () => viewportRef.current,
16137
+ getViewport: () => viewport,
16197
16138
  minOverlayDurationMs
16198
16139
  });
16199
- engineRef.current = engine;
16200
- return () => {
16201
- engine.destroy();
16202
- if (engineRef.current === engine) engineRef.current = null;
16203
- };
16204
- }, [narrationStore, index, minOverlayDurationMs]);
16140
+ return () => engineRef.current?.destroy();
16141
+ }, [narrationStore, index, viewport, minOverlayDurationMs]);
16205
16142
  const abortRef = useRef27(null);
16206
16143
  const debounceRef = useRef27(null);
16207
16144
  const lastChunkRef = useRef27(null);
@@ -16382,6 +16319,11 @@ function TutorModeContainer({
16382
16319
  }, idleTimeoutMs + 100);
16383
16320
  return () => clearTimeout(t);
16384
16321
  }, [currentChunk, idleTimeoutMs, narrationStore]);
16322
+ const page = index.byPage.get(pageNumber);
16323
+ const dpiScale = page ? page.page_dimensions.dpi / 72 : 1;
16324
+ const rasterScale = dpiScale * (scale || 1);
16325
+ const baseW = page ? page.page_dimensions.width * (scale || 1) : 0;
16326
+ const baseH = page ? page.page_dimensions.height * (scale || 1) : 0;
16385
16327
  const isReady = !!page && !!pageProxy;
16386
16328
  return /* @__PURE__ */ jsxs41(
16387
16329
  "div",
@@ -16460,7 +16402,7 @@ function TutorModeContainer({
16460
16402
  page,
16461
16403
  index,
16462
16404
  overlays: activeOverlays,
16463
- coordScale
16405
+ scale: scale || 1
16464
16406
  }
16465
16407
  )
16466
16408
  ]
@@ -16473,8 +16415,7 @@ function TutorModeContainer({
16473
16415
  index,
16474
16416
  currentPage: pageNumber,
16475
16417
  camera,
16476
- viewport,
16477
- coordScale
16418
+ viewport
16478
16419
  }
16479
16420
  ),
16480
16421
  /* @__PURE__ */ jsx55(
@@ -16484,8 +16425,7 @@ function TutorModeContainer({
16484
16425
  index,
16485
16426
  currentPage: pageNumber,
16486
16427
  camera,
16487
- viewport,
16488
- coordScale
16428
+ viewport
16489
16429
  }
16490
16430
  ),
16491
16431
  /* @__PURE__ */ jsx55(GhostReferenceOverlay, { overlays: activeOverlays, index })