pdfjs-reader-core 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1861,6 +1861,17 @@ function computeCameraForBlock(bbox, page, viewport, opts = {}) {
1861
1861
  const y = (pageCY - blockCY) * scale;
1862
1862
  return { scale, x, y };
1863
1863
  }
1864
+ function clampCamera(target, page, viewport) {
1865
+ const pageWScreen = page.width * target.scale;
1866
+ const pageHScreen = page.height * target.scale;
1867
+ const maxOffsetX = Math.max(0, (pageWScreen - viewport.width) / 2);
1868
+ const maxOffsetY = Math.max(0, (pageHScreen - viewport.height) / 2);
1869
+ return {
1870
+ scale: target.scale,
1871
+ x: Math.max(-maxOffsetX, Math.min(maxOffsetX, target.x)),
1872
+ y: Math.max(-maxOffsetY, Math.min(maxOffsetY, target.y))
1873
+ };
1874
+ }
1864
1875
  var init_camera_math = __esm({
1865
1876
  "src/utils/camera-math.ts"() {
1866
1877
  "use strict";
@@ -3717,8 +3728,8 @@ var init_PluginManager = __esm({
3717
3728
  /**
3718
3729
  * Get toolbar items by position
3719
3730
  */
3720
- getToolbarItemsByPosition(position2) {
3721
- return this.getToolbarItems().filter((item) => item.position === position2);
3731
+ getToolbarItemsByPosition(position) {
3732
+ return this.getToolbarItems().filter((item) => item.position === position);
3722
3733
  }
3723
3734
  /**
3724
3735
  * Get all sidebar panels from all plugins
@@ -4825,7 +4836,7 @@ var init_MobileToolbar = __esm({
4825
4836
  sidebarOpen,
4826
4837
  theme,
4827
4838
  onThemeChange,
4828
- position: position2 = "bottom",
4839
+ position = "bottom",
4829
4840
  className
4830
4841
  }) {
4831
4842
  const [showMoreMenu, setShowMoreMenu] = useState5(false);
@@ -4859,8 +4870,8 @@ var init_MobileToolbar = __esm({
4859
4870
  "bg-white dark:bg-gray-800",
4860
4871
  "border-gray-200 dark:border-gray-700",
4861
4872
  "px-2 py-1 safe-area-inset",
4862
- position2 === "top" && "top-0 border-b",
4863
- position2 === "bottom" && "bottom-0 border-t",
4873
+ position === "top" && "top-0 border-b",
4874
+ position === "bottom" && "bottom-0 border-t",
4864
4875
  className
4865
4876
  ),
4866
4877
  children: /* @__PURE__ */ jsxs2("div", { className: "flex items-center justify-between gap-1", children: [
@@ -4986,7 +4997,7 @@ var init_MobileToolbar = __esm({
4986
4997
  "bg-white dark:bg-gray-800",
4987
4998
  "rounded-lg shadow-lg",
4988
4999
  "border border-gray-200 dark:border-gray-700",
4989
- position2 === "bottom" ? "bottom-full mb-2" : "top-full mt-2"
5000
+ position === "bottom" ? "bottom-full mb-2" : "top-full mt-2"
4990
5001
  ),
4991
5002
  children: [
4992
5003
  /* @__PURE__ */ jsx3("div", { className: "px-2 py-1 text-xs text-gray-500 dark:text-gray-400 font-medium", children: "Theme" }),
@@ -6812,7 +6823,7 @@ var init_AnnotationToolbar = __esm({
6812
6823
  onShapeTypeChange: onShapeTypeChangeProp,
6813
6824
  onColorChange: onColorChangeProp,
6814
6825
  onStrokeWidthChange: onStrokeWidthChangeProp,
6815
- position: position2 = "top",
6826
+ position = "top",
6816
6827
  className
6817
6828
  }) {
6818
6829
  const storeActiveTool = useAnnotationStore((s) => s.activeAnnotationTool);
@@ -6862,9 +6873,9 @@ var init_AnnotationToolbar = __esm({
6862
6873
  {
6863
6874
  className: cn(
6864
6875
  "annotation-toolbar flex items-center gap-1 p-2 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700",
6865
- position2 === "floating" && "fixed bottom-20 left-1/2 -translate-x-1/2 z-50",
6866
- position2 === "top" && "sticky top-0 z-40",
6867
- position2 === "bottom" && "sticky bottom-0 z-40",
6876
+ position === "floating" && "fixed bottom-20 left-1/2 -translate-x-1/2 z-50",
6877
+ position === "top" && "sticky top-0 z-40",
6878
+ position === "bottom" && "sticky bottom-0 z-40",
6868
6879
  !isActive && "opacity-90",
6869
6880
  className
6870
6881
  ),
@@ -8513,7 +8524,7 @@ var init_SelectionToolbar = __esm({
8513
8524
  activeColor = "yellow",
8514
8525
  className
8515
8526
  }) {
8516
- const [position2, setPosition] = useState16({ top: 0, left: 0, visible: false });
8527
+ const [position, setPosition] = useState16({ top: 0, left: 0, visible: false });
8517
8528
  const toolbarRef = useRef14(null);
8518
8529
  useEffect17(() => {
8519
8530
  if (selection && selection.text && selection.rects.length > 0) {
@@ -8552,7 +8563,7 @@ var init_SelectionToolbar = __esm({
8552
8563
  const handleCopy = useCallback27(() => {
8553
8564
  onCopy?.();
8554
8565
  }, [onCopy]);
8555
- if (!position2.visible || !selection?.text) {
8566
+ if (!position.visible || !selection?.text) {
8556
8567
  return null;
8557
8568
  }
8558
8569
  return /* @__PURE__ */ jsxs18(
@@ -8570,8 +8581,8 @@ var init_SelectionToolbar = __esm({
8570
8581
  className
8571
8582
  ),
8572
8583
  style: {
8573
- top: position2.top,
8574
- left: position2.left,
8584
+ top: position.top,
8585
+ left: position.left,
8575
8586
  transform: "translateX(-50%)"
8576
8587
  },
8577
8588
  onMouseDown: (e) => {
@@ -8688,7 +8699,7 @@ var init_HighlightPopover = __esm({
8688
8699
  }) {
8689
8700
  const [isEditingComment, setIsEditingComment] = useState17(false);
8690
8701
  const [comment, setComment] = useState17(highlight?.comment ?? "");
8691
- const [position2, setPosition] = useState17({ top: 0, left: 0, visible: false });
8702
+ const [position, setPosition] = useState17({ top: 0, left: 0, visible: false });
8692
8703
  const popoverRef = useRef15(null);
8693
8704
  const textareaRef = useRef15(null);
8694
8705
  useEffect18(() => {
@@ -8736,11 +8747,11 @@ var init_HighlightPopover = __esm({
8736
8747
  onClose();
8737
8748
  }
8738
8749
  }
8739
- if (position2.visible) {
8750
+ if (position.visible) {
8740
8751
  document.addEventListener("mousedown", handleClickOutside);
8741
8752
  return () => document.removeEventListener("mousedown", handleClickOutside);
8742
8753
  }
8743
- }, [position2.visible, onClose]);
8754
+ }, [position.visible, onClose]);
8744
8755
  useEffect18(() => {
8745
8756
  function handleKeyDown(event) {
8746
8757
  if (event.key === "Escape") {
@@ -8752,11 +8763,11 @@ var init_HighlightPopover = __esm({
8752
8763
  }
8753
8764
  }
8754
8765
  }
8755
- if (position2.visible) {
8766
+ if (position.visible) {
8756
8767
  document.addEventListener("keydown", handleKeyDown);
8757
8768
  return () => document.removeEventListener("keydown", handleKeyDown);
8758
8769
  }
8759
- }, [position2.visible, isEditingComment, highlight?.comment, onClose]);
8770
+ }, [position.visible, isEditingComment, highlight?.comment, onClose]);
8760
8771
  const handleColorClick = useCallback28(
8761
8772
  (color) => {
8762
8773
  if (highlight) {
@@ -8783,7 +8794,7 @@ var init_HighlightPopover = __esm({
8783
8794
  onCopy?.(highlight.text);
8784
8795
  }
8785
8796
  }, [highlight, onCopy]);
8786
- if (!highlight || !position2.visible) {
8797
+ if (!highlight || !position.visible) {
8787
8798
  return null;
8788
8799
  }
8789
8800
  return /* @__PURE__ */ jsxs19(
@@ -8800,8 +8811,8 @@ var init_HighlightPopover = __esm({
8800
8811
  className
8801
8812
  ),
8802
8813
  style: {
8803
- top: position2.top,
8804
- left: position2.left,
8814
+ top: position.top,
8815
+ left: position.left,
8805
8816
  transform: "translate(-50%, -100%)",
8806
8817
  width: 280
8807
8818
  },
@@ -10213,7 +10224,7 @@ var init_FloatingZoomControls = __esm({
10213
10224
  init_hooks();
10214
10225
  init_utils();
10215
10226
  FloatingZoomControls = memo27(function FloatingZoomControls2({
10216
- position: position2 = "bottom-right",
10227
+ position = "bottom-right",
10217
10228
  className,
10218
10229
  showFitToWidth = true,
10219
10230
  showFitToPage = false,
@@ -10254,7 +10265,7 @@ var init_FloatingZoomControls = __esm({
10254
10265
  "bg-white dark:bg-gray-800 rounded-lg shadow-lg",
10255
10266
  "border border-gray-200 dark:border-gray-700",
10256
10267
  "p-1",
10257
- positionClasses[position2],
10268
+ positionClasses[position],
10258
10269
  className
10259
10270
  ),
10260
10271
  children: [
@@ -11934,7 +11945,7 @@ import { jsx as jsx34 } from "react/jsx-runtime";
11934
11945
  var QuickNoteButton = memo33(function QuickNoteButton2({
11935
11946
  pageNumber,
11936
11947
  scale,
11937
- position: position2 = "top-right",
11948
+ position = "top-right",
11938
11949
  onClick,
11939
11950
  className,
11940
11951
  visible = true
@@ -11943,11 +11954,11 @@ var QuickNoteButton = memo33(function QuickNoteButton2({
11943
11954
  const handleClick = useCallback38(
11944
11955
  (e) => {
11945
11956
  e.stopPropagation();
11946
- const x = position2 === "top-right" ? 80 : 80;
11947
- const y = position2 === "top-right" ? 20 : 80;
11957
+ const x = position === "top-right" ? 80 : 80;
11958
+ const y = position === "top-right" ? 20 : 80;
11948
11959
  onClick(pageNumber, x / scale, y / scale);
11949
11960
  },
11950
- [pageNumber, onClick, position2, scale]
11961
+ [pageNumber, onClick, position, scale]
11951
11962
  );
11952
11963
  if (!visible) {
11953
11964
  return null;
@@ -11968,8 +11979,8 @@ var QuickNoteButton = memo33(function QuickNoteButton2({
11968
11979
  "transition-all duration-200",
11969
11980
  "focus:outline-none focus:ring-2 focus:ring-yellow-500 focus:ring-offset-2",
11970
11981
  isHovered && "scale-110",
11971
- position2 === "top-right" && "top-3 right-3",
11972
- position2 === "bottom-right" && "bottom-3 right-3",
11982
+ position === "top-right" && "top-3 right-3",
11983
+ position === "bottom-right" && "bottom-3 right-3",
11973
11984
  className
11974
11985
  ),
11975
11986
  title: "Add quick note",
@@ -11995,7 +12006,7 @@ import { memo as memo34, useCallback as useCallback39, useState as useState27, u
11995
12006
  import { jsx as jsx35, jsxs as jsxs29 } from "react/jsx-runtime";
11996
12007
  var QuickNotePopover = memo34(function QuickNotePopover2({
11997
12008
  visible,
11998
- position: position2,
12009
+ position,
11999
12010
  initialContent = "",
12000
12011
  agentLastStatement,
12001
12012
  onSave,
@@ -12005,7 +12016,7 @@ var QuickNotePopover = memo34(function QuickNotePopover2({
12005
12016
  const [content, setContent] = useState27(initialContent);
12006
12017
  const textareaRef = useRef24(null);
12007
12018
  const popoverRef = useRef24(null);
12008
- const [adjustedPosition, setAdjustedPosition] = useState27(position2);
12019
+ const [adjustedPosition, setAdjustedPosition] = useState27(position);
12009
12020
  useEffect25(() => {
12010
12021
  if (visible && textareaRef.current) {
12011
12022
  textareaRef.current.focus();
@@ -12020,7 +12031,7 @@ var QuickNotePopover = memo34(function QuickNotePopover2({
12020
12031
  if (!visible || !popoverRef.current) return;
12021
12032
  const rect = popoverRef.current.getBoundingClientRect();
12022
12033
  const padding = 10;
12023
- let { x, y } = position2;
12034
+ let { x, y } = position;
12024
12035
  if (x + rect.width > window.innerWidth - padding) {
12025
12036
  x = window.innerWidth - rect.width - padding;
12026
12037
  }
@@ -12034,7 +12045,7 @@ var QuickNotePopover = memo34(function QuickNotePopover2({
12034
12045
  y = padding;
12035
12046
  }
12036
12047
  setAdjustedPosition({ x, y });
12037
- }, [position2, visible]);
12048
+ }, [position, visible]);
12038
12049
  const handleSave = useCallback39(() => {
12039
12050
  if (content.trim()) {
12040
12051
  onSave(content.trim());
@@ -12151,11 +12162,11 @@ import { jsx as jsx36, jsxs as jsxs30 } from "react/jsx-runtime";
12151
12162
  var AskAboutOverlay = memo35(function AskAboutOverlay2({
12152
12163
  visible,
12153
12164
  progress,
12154
- position: position2,
12165
+ position,
12155
12166
  size = 60,
12156
12167
  className
12157
12168
  }) {
12158
- if (!visible || !position2) {
12169
+ if (!visible || !position) {
12159
12170
  return null;
12160
12171
  }
12161
12172
  const strokeWidth = 4;
@@ -12171,8 +12182,8 @@ var AskAboutOverlay = memo35(function AskAboutOverlay2({
12171
12182
  className
12172
12183
  ),
12173
12184
  style: {
12174
- left: position2.x - size / 2,
12175
- top: position2.y - size / 2
12185
+ left: position.x - size / 2,
12186
+ top: position.y - size / 2
12176
12187
  },
12177
12188
  children: [
12178
12189
  /* @__PURE__ */ jsxs30(
@@ -12260,20 +12271,20 @@ init_utils();
12260
12271
  import { memo as memo36, useCallback as useCallback40, useState as useState28, useRef as useRef25, useEffect as useEffect26 } from "react";
12261
12272
  import { jsx as jsx37, jsxs as jsxs31 } from "react/jsx-runtime";
12262
12273
  var AskAboutTrigger = memo36(function AskAboutTrigger2({
12263
- position: position2,
12274
+ position,
12264
12275
  onConfirm,
12265
12276
  onCancel,
12266
12277
  visible,
12267
12278
  autoHideDelay = 5e3,
12268
12279
  className
12269
12280
  }) {
12270
- const [adjustedPosition, setAdjustedPosition] = useState28(position2);
12281
+ const [adjustedPosition, setAdjustedPosition] = useState28(position);
12271
12282
  const triggerRef = useRef25(null);
12272
12283
  useEffect26(() => {
12273
12284
  if (!visible || !triggerRef.current) return;
12274
12285
  const rect = triggerRef.current.getBoundingClientRect();
12275
12286
  const padding = 10;
12276
- let { x, y } = position2;
12287
+ let { x, y } = position;
12277
12288
  if (x + rect.width / 2 > window.innerWidth - padding) {
12278
12289
  x = window.innerWidth - rect.width / 2 - padding;
12279
12290
  }
@@ -12281,10 +12292,10 @@ var AskAboutTrigger = memo36(function AskAboutTrigger2({
12281
12292
  x = rect.width / 2 + padding;
12282
12293
  }
12283
12294
  if (y + rect.height > window.innerHeight - padding) {
12284
- y = position2.y - rect.height - 20;
12295
+ y = position.y - rect.height - 20;
12285
12296
  }
12286
12297
  setAdjustedPosition({ x, y });
12287
- }, [position2, visible]);
12298
+ }, [position, visible]);
12288
12299
  useEffect26(() => {
12289
12300
  if (!visible || autoHideDelay === 0) return;
12290
12301
  const timer = setTimeout(onCancel, autoHideDelay);
@@ -12978,21 +12989,43 @@ import { AnimatePresence } from "framer-motion";
12978
12989
  // src/components/TutorMode/SpotlightMask.tsx
12979
12990
  import { useId } from "react";
12980
12991
  import { motion as motion2 } from "framer-motion";
12992
+
12993
+ // src/components/TutorMode/tokens.ts
12994
+ var INK = "#2a2420";
12995
+ var PAPER = "#faf6ec";
12996
+ var ACCENT = "#b04a1a";
12997
+ var ACCENT_SOFT = "rgba(176, 74, 26, 0.18)";
12998
+ var ACCENT_GLOW = "rgba(176, 74, 26, 0.35)";
12999
+ var MARKER = "#e6b422";
13000
+ var MARKER_SOFT = "rgba(230, 180, 34, 0.38)";
13001
+ var SERIF = "'Iowan Old Style', 'Palatino Linotype', Palatino, 'Book Antiqua', 'EB Garamond', 'Hoefler Text', Georgia, serif";
13002
+ var EASE_OUT_EXPO = [0.22, 1, 0.36, 1];
13003
+
13004
+ // src/components/TutorMode/SpotlightMask.tsx
12981
13005
  import { jsx as jsx42, jsxs as jsxs35 } from "react/jsx-runtime";
12982
13006
  function SpotlightMask({
12983
13007
  page,
12984
13008
  bbox,
12985
13009
  action,
12986
- durationMs = 400
13010
+ durationMs = 500
12987
13011
  }) {
12988
13012
  const maskId = useId();
12989
13013
  const filterId = `${maskId}-blur`;
12990
- const [x1, y1, x2, y2] = bbox;
12991
- const w = Math.max(0, x2 - x1);
12992
- const h = Math.max(0, y2 - y1);
12993
- const rx = action.shape === "rounded" ? 12 : action.shape === "ellipse" ? w / 2 : 0;
12994
- const ry = action.shape === "rounded" ? 12 : action.shape === "ellipse" ? h / 2 : 0;
12995
- const feather = action.feather_px;
13014
+ const [rawX1, rawY1, rawX2, rawY2] = bbox;
13015
+ const rawW = Math.max(0, rawX2 - rawX1);
13016
+ const rawH = Math.max(0, rawY2 - rawY1);
13017
+ const pad = Math.min(28, Math.max(10, Math.min(rawW, rawH) * 0.06));
13018
+ const x1 = rawX1 - pad;
13019
+ const y1 = rawY1 - pad;
13020
+ const x2 = rawX2 + pad;
13021
+ const y2 = rawY2 + pad;
13022
+ const w = x2 - x1;
13023
+ const h = y2 - y1;
13024
+ const rx = action.shape === "rounded" ? 14 : action.shape === "ellipse" ? w / 2 : 0;
13025
+ const ry = action.shape === "rounded" ? 14 : action.shape === "ellipse" ? h / 2 : 0;
13026
+ const feather = Math.max(16, action.feather_px);
13027
+ const cx = (x1 + x2) / 2;
13028
+ const cy = (y1 + y2) / 2;
12996
13029
  return /* @__PURE__ */ jsxs35(
12997
13030
  "svg",
12998
13031
  {
@@ -13005,7 +13038,8 @@ function SpotlightMask({
13005
13038
  inset: 0,
13006
13039
  pointerEvents: "none",
13007
13040
  width: page.width,
13008
- height: page.height
13041
+ height: page.height,
13042
+ overflow: "visible"
13009
13043
  },
13010
13044
  "data-role": "spotlight-mask",
13011
13045
  children: [
@@ -13016,8 +13050,8 @@ function SpotlightMask({
13016
13050
  action.shape === "ellipse" ? /* @__PURE__ */ jsx42(
13017
13051
  "ellipse",
13018
13052
  {
13019
- cx: (x1 + x2) / 2,
13020
- cy: (y1 + y2) / 2,
13053
+ cx,
13054
+ cy,
13021
13055
  rx: w / 2,
13022
13056
  ry: h / 2,
13023
13057
  fill: "black",
@@ -13045,14 +13079,82 @@ function SpotlightMask({
13045
13079
  y: 0,
13046
13080
  width: page.width,
13047
13081
  height: page.height,
13048
- fill: "black",
13082
+ fill: INK,
13049
13083
  mask: `url(#${maskId})`,
13050
13084
  initial: { fillOpacity: 0 },
13051
13085
  animate: { fillOpacity: action.dim_opacity },
13052
13086
  exit: { fillOpacity: 0 },
13053
- transition: { duration: durationMs / 1e3, ease: "easeOut" }
13087
+ transition: { duration: durationMs / 1e3, ease: EASE_OUT_EXPO }
13054
13088
  }
13055
- )
13089
+ ),
13090
+ action.shape === "ellipse" ? /* @__PURE__ */ jsx42(
13091
+ motion2.ellipse,
13092
+ {
13093
+ cx,
13094
+ cy,
13095
+ rx: w / 2,
13096
+ ry: h / 2,
13097
+ fill: "none",
13098
+ stroke: ACCENT,
13099
+ strokeWidth: 3,
13100
+ initial: { opacity: 0, scale: 1.08 },
13101
+ animate: { opacity: 0.9, scale: 1 },
13102
+ exit: { opacity: 0 },
13103
+ style: { transformOrigin: `${cx}px ${cy}px`, transformBox: "fill-box" },
13104
+ transition: {
13105
+ duration: durationMs / 1e3,
13106
+ delay: 0.15,
13107
+ ease: EASE_OUT_EXPO
13108
+ }
13109
+ }
13110
+ ) : /* @__PURE__ */ jsx42(
13111
+ motion2.rect,
13112
+ {
13113
+ x: x1,
13114
+ y: y1,
13115
+ width: w,
13116
+ height: h,
13117
+ rx,
13118
+ ry,
13119
+ fill: "none",
13120
+ stroke: ACCENT,
13121
+ strokeWidth: 3,
13122
+ initial: { opacity: 0, scale: 1.04 },
13123
+ animate: { opacity: 0.9, scale: 1 },
13124
+ exit: { opacity: 0 },
13125
+ style: {
13126
+ transformOrigin: `${cx}px ${cy}px`,
13127
+ transformBox: "fill-box"
13128
+ },
13129
+ transition: {
13130
+ duration: durationMs / 1e3,
13131
+ delay: 0.15,
13132
+ ease: EASE_OUT_EXPO
13133
+ }
13134
+ }
13135
+ ),
13136
+ action.shape !== "ellipse" ? /* @__PURE__ */ jsx42(
13137
+ motion2.rect,
13138
+ {
13139
+ x: x1 - 2,
13140
+ y: y1 - 2,
13141
+ width: w + 4,
13142
+ height: h + 4,
13143
+ rx: rx + 2,
13144
+ ry: ry + 2,
13145
+ fill: "none",
13146
+ stroke: ACCENT_GLOW,
13147
+ strokeWidth: 8,
13148
+ initial: { opacity: 0 },
13149
+ animate: { opacity: 0.6 },
13150
+ exit: { opacity: 0 },
13151
+ transition: {
13152
+ duration: durationMs / 1e3,
13153
+ delay: 0.2,
13154
+ ease: EASE_OUT_EXPO
13155
+ }
13156
+ }
13157
+ ) : null
13056
13158
  ]
13057
13159
  }
13058
13160
  );
@@ -13060,36 +13162,59 @@ function SpotlightMask({
13060
13162
 
13061
13163
  // src/components/TutorMode/AnimatedUnderline.tsx
13062
13164
  import { motion as motion3 } from "framer-motion";
13063
- import { jsx as jsx43 } from "react/jsx-runtime";
13165
+ import { jsx as jsx43, jsxs as jsxs36 } from "react/jsx-runtime";
13166
+ function jitterAt(i) {
13167
+ const h = Math.sin(i * 12.9898) * 43758.5453;
13168
+ return (h - Math.floor(h) - 0.5) * 4;
13169
+ }
13064
13170
  function pathForStyle(x1, x2, y, style) {
13065
- if (style === "straight") return `M ${x1} ${y} L ${x2} ${y}`;
13066
- if (style === "double")
13067
- return `M ${x1} ${y - 3} L ${x2} ${y - 3} M ${x1} ${y + 3} L ${x2} ${y + 3}`;
13171
+ const x1e = x1 - 4;
13172
+ const x2e = x2 + 4;
13173
+ if (style === "straight") {
13174
+ return { primary: `M ${x1e} ${y} L ${x2e} ${y}` };
13175
+ }
13176
+ if (style === "double") {
13177
+ return {
13178
+ primary: `M ${x1e} ${y - 3} L ${x2e} ${y - 3}`,
13179
+ ghost: `M ${x1e} ${y + 3} L ${x2e} ${y + 3}`
13180
+ };
13181
+ }
13068
13182
  if (style === "wavy") {
13069
- const steps = Math.max(8, Math.floor((x2 - x1) / 18));
13070
- let d2 = `M ${x1} ${y}`;
13183
+ const len = x2e - x1e;
13184
+ const steps = Math.max(12, Math.floor(len / 10));
13185
+ const amp = 3.2;
13186
+ let d = `M ${x1e} ${y}`;
13071
13187
  for (let i = 1; i <= steps; i++) {
13072
- const px = x1 + (x2 - x1) * i / steps;
13073
- const dy = i % 2 === 0 ? 4 : -4;
13074
- d2 += ` Q ${px - (x2 - x1) / (2 * steps)} ${y + dy} ${px} ${y}`;
13188
+ const t = i / steps;
13189
+ const px = x1e + len * t;
13190
+ const py = y + Math.sin(t * Math.PI * 4) * amp;
13191
+ const prevT = (i - 1) / steps;
13192
+ const cpx = x1e + len * (prevT + (t - prevT) / 2);
13193
+ const cpy = y + Math.sin((prevT + (t - prevT) / 2) * Math.PI * 4) * amp;
13194
+ d += ` Q ${cpx} ${cpy} ${px} ${py}`;
13075
13195
  }
13076
- return d2;
13196
+ return { primary: d };
13077
13197
  }
13078
- const segs = 6;
13079
- let d = `M ${x1} ${y}`;
13198
+ const segs = 8;
13199
+ let primary = `M ${x1e} ${y + jitterAt(0)}`;
13200
+ let ghost = `M ${x1e} ${y + jitterAt(100) + 1.5}`;
13080
13201
  for (let i = 1; i <= segs; i++) {
13081
- const px = x1 + (x2 - x1) * i / segs;
13082
- const jitter = (Math.random() - 0.5) * 4;
13083
- d += ` L ${px} ${y + jitter}`;
13202
+ const px = x1e + (x2e - x1e) * i / segs;
13203
+ primary += ` L ${px} ${y + jitterAt(i)}`;
13204
+ ghost += ` L ${px} ${y + jitterAt(i + 100) + 1.5}`;
13084
13205
  }
13085
- return d;
13206
+ return { primary, ghost };
13086
13207
  }
13087
13208
  function AnimatedUnderline({ bbox, action }) {
13088
13209
  const [x1, , x2, y2] = bbox;
13089
13210
  const y = y2 + 6;
13090
- const d = pathForStyle(x1, x2, y, action.style);
13211
+ const { primary, ghost } = pathForStyle(x1, x2, y, action.style);
13091
13212
  const duration = action.draw_duration_ms / 1e3;
13092
- return /* @__PURE__ */ jsx43(
13213
+ const stroke = action.color && action.color !== "#FBBF24" ? action.color : ACCENT;
13214
+ const blotX = x2 + 4;
13215
+ const blotY = y;
13216
+ const strokeWeight = action.style === "wavy" ? 3 : 4;
13217
+ return /* @__PURE__ */ jsxs36(
13093
13218
  "svg",
13094
13219
  {
13095
13220
  style: {
@@ -13099,119 +13224,365 @@ function AnimatedUnderline({ bbox, action }) {
13099
13224
  overflow: "visible"
13100
13225
  },
13101
13226
  "data-role": "underline",
13102
- children: /* @__PURE__ */ jsx43(
13103
- motion3.path,
13104
- {
13105
- d,
13106
- fill: "none",
13107
- stroke: action.color,
13108
- strokeWidth: 4,
13109
- strokeLinecap: "round",
13110
- initial: { pathLength: 0, opacity: 0 },
13111
- animate: { pathLength: 1, opacity: 1 },
13112
- exit: { opacity: 0 },
13113
- transition: { duration, ease: "easeOut" }
13114
- }
13115
- )
13227
+ children: [
13228
+ ghost ? /* @__PURE__ */ jsx43(
13229
+ motion3.path,
13230
+ {
13231
+ d: ghost,
13232
+ fill: "none",
13233
+ stroke,
13234
+ strokeWidth: strokeWeight - 1.5,
13235
+ strokeLinecap: "round",
13236
+ strokeOpacity: 0.35,
13237
+ initial: { pathLength: 0, opacity: 0 },
13238
+ animate: { pathLength: 1, opacity: 0.55 },
13239
+ exit: { opacity: 0 },
13240
+ transition: { duration, ease: EASE_OUT_EXPO }
13241
+ }
13242
+ ) : null,
13243
+ /* @__PURE__ */ jsx43(
13244
+ motion3.path,
13245
+ {
13246
+ d: primary,
13247
+ fill: "none",
13248
+ stroke,
13249
+ strokeWidth: strokeWeight,
13250
+ strokeLinecap: "round",
13251
+ strokeLinejoin: "round",
13252
+ initial: { pathLength: 0, opacity: 0 },
13253
+ animate: { pathLength: 1, opacity: 1 },
13254
+ exit: { opacity: 0 },
13255
+ transition: { duration, ease: EASE_OUT_EXPO }
13256
+ }
13257
+ ),
13258
+ /* @__PURE__ */ jsx43(
13259
+ motion3.circle,
13260
+ {
13261
+ cx: blotX,
13262
+ cy: blotY,
13263
+ r: strokeWeight / 2 + 0.5,
13264
+ fill: stroke,
13265
+ initial: { scale: 0, opacity: 0 },
13266
+ animate: { scale: 1, opacity: 0.9 },
13267
+ exit: { opacity: 0 },
13268
+ style: {
13269
+ transformOrigin: `${blotX}px ${blotY}px`,
13270
+ transformBox: "fill-box"
13271
+ },
13272
+ transition: {
13273
+ duration: 0.25,
13274
+ delay: duration - 0.1,
13275
+ ease: EASE_OUT_EXPO
13276
+ }
13277
+ }
13278
+ )
13279
+ ]
13116
13280
  }
13117
13281
  );
13118
13282
  }
13119
13283
 
13120
13284
  // src/components/TutorMode/AnimatedHighlight.tsx
13285
+ import { useId as useId2 } from "react";
13121
13286
  import { motion as motion4 } from "framer-motion";
13122
- import { jsx as jsx44 } from "react/jsx-runtime";
13287
+ import { jsx as jsx44, jsxs as jsxs37 } from "react/jsx-runtime";
13123
13288
  function AnimatedHighlight({ bbox, action }) {
13124
13289
  const [x1, y1, x2, y2] = bbox;
13125
- const w = x2 - x1;
13126
- const h = y2 - y1;
13127
- return /* @__PURE__ */ jsx44(
13128
- motion4.div,
13290
+ const h = Math.max(1, y2 - y1);
13291
+ const bleed = Math.min(4, h * 0.12);
13292
+ const yTop = y1 - bleed;
13293
+ const yBot = y2 + bleed;
13294
+ const height = yBot - yTop;
13295
+ const duration = action.draw_duration_ms / 1e3;
13296
+ const filterId = useId2();
13297
+ const fill = action.color && action.color !== "rgba(250, 204, 21, 0.35)" && action.color !== "rgba(250,204,21,0.35)" ? action.color : MARKER_SOFT;
13298
+ const inner = action.color && action.color !== "rgba(250, 204, 21, 0.35)" && action.color !== "rgba(250,204,21,0.35)" ? action.color : MARKER;
13299
+ const taper = Math.min(6, h * 0.2);
13300
+ const pathD = `
13301
+ M ${x1 - 2} ${yTop + taper}
13302
+ L ${x1 + 2} ${yTop}
13303
+ L ${x2 - 2} ${yTop}
13304
+ L ${x2 + 2} ${yTop + taper}
13305
+ L ${x2 + 2} ${yBot - taper}
13306
+ L ${x2 - 2} ${yBot}
13307
+ L ${x1 + 2} ${yBot}
13308
+ L ${x1 - 2} ${yBot - taper}
13309
+ Z
13310
+ `;
13311
+ return /* @__PURE__ */ jsxs37(
13312
+ "svg",
13129
13313
  {
13130
13314
  style: {
13131
13315
  position: "absolute",
13132
- left: x1,
13133
- top: y1,
13134
- height: h,
13135
- background: action.color,
13136
- borderRadius: 4,
13137
- mixBlendMode: "multiply",
13138
- transformOrigin: "0% 50%",
13139
- pointerEvents: "none"
13316
+ inset: 0,
13317
+ pointerEvents: "none",
13318
+ overflow: "visible",
13319
+ mixBlendMode: "multiply"
13140
13320
  },
13141
- initial: { width: 0, opacity: 0.9 },
13142
- animate: { width: w, opacity: 0.9 },
13143
- exit: { opacity: 0 },
13144
- transition: { duration: action.draw_duration_ms / 1e3, ease: "easeOut" },
13145
- "data-role": "highlight"
13321
+ "data-role": "highlight",
13322
+ children: [
13323
+ /* @__PURE__ */ jsx44("defs", { children: /* @__PURE__ */ jsxs37("filter", { id: filterId, children: [
13324
+ /* @__PURE__ */ jsx44(
13325
+ "feTurbulence",
13326
+ {
13327
+ type: "fractalNoise",
13328
+ baseFrequency: "1.6",
13329
+ numOctaves: "1",
13330
+ seed: 3,
13331
+ result: "noise"
13332
+ }
13333
+ ),
13334
+ /* @__PURE__ */ jsx44("feDisplacementMap", { in: "SourceGraphic", in2: "noise", scale: 1.4 })
13335
+ ] }) }),
13336
+ /* @__PURE__ */ jsxs37(
13337
+ motion4.g,
13338
+ {
13339
+ initial: { clipPath: `inset(0 100% 0 0)` },
13340
+ animate: { clipPath: `inset(0 0% 0 0)` },
13341
+ exit: { opacity: 0 },
13342
+ transition: { duration, ease: EASE_OUT_EXPO },
13343
+ children: [
13344
+ /* @__PURE__ */ jsx44(
13345
+ "path",
13346
+ {
13347
+ d: pathD,
13348
+ fill,
13349
+ opacity: 0.85,
13350
+ filter: `url(#${filterId})`
13351
+ }
13352
+ ),
13353
+ /* @__PURE__ */ jsx44(
13354
+ "rect",
13355
+ {
13356
+ x: x1 - 1,
13357
+ y: y1 - bleed * 0.4,
13358
+ width: x2 - x1 + 2,
13359
+ height: height - bleed * 0.8,
13360
+ fill: inner,
13361
+ opacity: 0.5,
13362
+ filter: `url(#${filterId})`
13363
+ }
13364
+ )
13365
+ ]
13366
+ }
13367
+ )
13368
+ ]
13146
13369
  }
13147
13370
  );
13148
13371
  }
13149
13372
 
13150
13373
  // src/components/TutorMode/PulseOverlay.tsx
13151
13374
  import { motion as motion5 } from "framer-motion";
13152
- import { jsx as jsx45 } from "react/jsx-runtime";
13375
+ import { jsx as jsx45, jsxs as jsxs38 } from "react/jsx-runtime";
13153
13376
  var INTENSITY = {
13154
- subtle: { scale: 1.02, border: "2px solid rgba(59,130,246,0.6)" },
13155
- normal: { scale: 1.05, border: "3px solid rgba(59,130,246,0.8)" },
13156
- strong: { scale: 1.1, border: "4px solid rgba(59,130,246,1.0)" }
13377
+ subtle: { bracketLen: 14, strokeWeight: 2, coreOpacity: 0.5, ringScale: 1.08 },
13378
+ normal: { bracketLen: 20, strokeWeight: 2.5, coreOpacity: 0.75, ringScale: 1.14 },
13379
+ strong: { bracketLen: 26, strokeWeight: 3, coreOpacity: 1, ringScale: 1.22 }
13157
13380
  };
13158
13381
  function PulseOverlay({ bbox, action }) {
13159
13382
  const [x1, y1, x2, y2] = bbox;
13160
- const { scale, border } = INTENSITY[action.intensity];
13161
- const repeat = action.count === 1 ? 0 : action.count - 1;
13162
- return /* @__PURE__ */ jsx45(
13163
- motion5.div,
13383
+ const w = Math.max(1, x2 - x1);
13384
+ const h = Math.max(1, y2 - y1);
13385
+ const cx = (x1 + x2) / 2;
13386
+ const cy = (y1 + y2) / 2;
13387
+ const spec = INTENSITY[action.intensity] ?? INTENSITY.normal;
13388
+ const L = Math.min(spec.bracketLen, Math.min(w, h) / 2.5);
13389
+ const PAD = 6;
13390
+ return /* @__PURE__ */ jsxs38(
13391
+ "svg",
13164
13392
  {
13165
13393
  style: {
13166
13394
  position: "absolute",
13167
- left: x1,
13168
- top: y1,
13169
- width: x2 - x1,
13170
- height: y2 - y1,
13171
- border,
13172
- borderRadius: 8,
13395
+ inset: 0,
13173
13396
  pointerEvents: "none",
13174
- boxSizing: "border-box"
13175
- },
13176
- animate: { scale: [1, scale, 1] },
13177
- transition: {
13178
- duration: 1.2,
13179
- times: [0, 0.5, 1],
13180
- ease: "easeInOut",
13181
- repeat,
13182
- repeatType: "loop"
13397
+ overflow: "visible"
13183
13398
  },
13399
+ "data-role": "pulse",
13400
+ children: [
13401
+ /* @__PURE__ */ jsx45(
13402
+ motion5.ellipse,
13403
+ {
13404
+ cx,
13405
+ cy,
13406
+ rx: w / 2 + 10,
13407
+ ry: h / 2 + 10,
13408
+ fill: ACCENT_GLOW,
13409
+ style: {
13410
+ transformOrigin: `${cx}px ${cy}px`,
13411
+ transformBox: "fill-box",
13412
+ filter: "blur(16px)"
13413
+ },
13414
+ initial: { opacity: 0, scale: 0.95 },
13415
+ animate: {
13416
+ opacity: [0, spec.coreOpacity * 0.45, 0.15],
13417
+ scale: [0.95, spec.ringScale, 1]
13418
+ },
13419
+ exit: { opacity: 0 },
13420
+ transition: {
13421
+ duration: 1.3,
13422
+ times: [0, 0.5, 1],
13423
+ ease: EASE_OUT_EXPO
13424
+ }
13425
+ }
13426
+ ),
13427
+ /* @__PURE__ */ jsx45(
13428
+ Bracket,
13429
+ {
13430
+ originX: x1 - PAD,
13431
+ originY: y1 - PAD,
13432
+ direction: "tl",
13433
+ length: L,
13434
+ weight: spec.strokeWeight,
13435
+ delay: 0
13436
+ }
13437
+ ),
13438
+ /* @__PURE__ */ jsx45(
13439
+ Bracket,
13440
+ {
13441
+ originX: x2 + PAD,
13442
+ originY: y1 - PAD,
13443
+ direction: "tr",
13444
+ length: L,
13445
+ weight: spec.strokeWeight,
13446
+ delay: 0.04
13447
+ }
13448
+ ),
13449
+ /* @__PURE__ */ jsx45(
13450
+ Bracket,
13451
+ {
13452
+ originX: x1 - PAD,
13453
+ originY: y2 + PAD,
13454
+ direction: "bl",
13455
+ length: L,
13456
+ weight: spec.strokeWeight,
13457
+ delay: 0.08
13458
+ }
13459
+ ),
13460
+ /* @__PURE__ */ jsx45(
13461
+ Bracket,
13462
+ {
13463
+ originX: x2 + PAD,
13464
+ originY: y2 + PAD,
13465
+ direction: "br",
13466
+ length: L,
13467
+ weight: spec.strokeWeight,
13468
+ delay: 0.12
13469
+ }
13470
+ ),
13471
+ /* @__PURE__ */ jsx45(
13472
+ motion5.rect,
13473
+ {
13474
+ x: x1 - PAD,
13475
+ y: y1 - PAD,
13476
+ width: w + PAD * 2,
13477
+ height: h + PAD * 2,
13478
+ fill: "none",
13479
+ stroke: ACCENT,
13480
+ strokeWidth: 1,
13481
+ rx: 4,
13482
+ style: {
13483
+ transformOrigin: `${cx}px ${cy}px`,
13484
+ transformBox: "fill-box"
13485
+ },
13486
+ initial: { scale: 1, opacity: 0 },
13487
+ animate: {
13488
+ scale: [1, spec.ringScale, 1],
13489
+ opacity: [0, spec.coreOpacity * 0.5, 0]
13490
+ },
13491
+ exit: { opacity: 0 },
13492
+ transition: {
13493
+ duration: 1.3,
13494
+ times: [0, 0.5, 1],
13495
+ ease: EASE_OUT_EXPO,
13496
+ delay: 0.2
13497
+ }
13498
+ }
13499
+ )
13500
+ ]
13501
+ }
13502
+ );
13503
+ }
13504
+ function Bracket({
13505
+ originX,
13506
+ originY,
13507
+ direction,
13508
+ length,
13509
+ weight,
13510
+ delay
13511
+ }) {
13512
+ const xSign = direction === "tl" || direction === "bl" ? 1 : -1;
13513
+ const ySign = direction === "tl" || direction === "tr" ? 1 : -1;
13514
+ const d = `
13515
+ M ${originX + xSign * length} ${originY}
13516
+ L ${originX} ${originY}
13517
+ L ${originX} ${originY + ySign * length}
13518
+ `;
13519
+ const slideX = -xSign * 8;
13520
+ const slideY = -ySign * 8;
13521
+ return /* @__PURE__ */ jsx45(
13522
+ motion5.path,
13523
+ {
13524
+ d,
13525
+ fill: "none",
13526
+ stroke: ACCENT,
13527
+ strokeWidth: weight,
13528
+ strokeLinecap: "round",
13529
+ strokeLinejoin: "round",
13530
+ initial: { opacity: 0, x: slideX, y: slideY },
13531
+ animate: { opacity: 1, x: 0, y: 0 },
13184
13532
  exit: { opacity: 0 },
13185
- "data-role": "pulse"
13533
+ transition: {
13534
+ duration: 0.4,
13535
+ delay,
13536
+ ease: EASE_OUT_EXPO
13537
+ }
13186
13538
  }
13187
13539
  );
13188
13540
  }
13189
13541
 
13190
13542
  // src/components/TutorMode/CalloutArrow.tsx
13543
+ import { useId as useId3 } from "react";
13191
13544
  import { motion as motion6 } from "framer-motion";
13192
- import { jsx as jsx46, jsxs as jsxs36 } from "react/jsx-runtime";
13545
+ import { jsx as jsx46, jsxs as jsxs39 } from "react/jsx-runtime";
13193
13546
  function centerOf(b) {
13194
13547
  return { x: (b[0] + b[2]) / 2, y: (b[1] + b[3]) / 2 };
13195
13548
  }
13196
- function arrowPath(fromBbox, toBbox, curve) {
13549
+ function edgePoints(fromBbox, toBbox) {
13197
13550
  const a = centerOf(fromBbox);
13198
13551
  const b = centerOf(toBbox);
13199
- if (curve === "straight") return `M ${a.x} ${a.y} L ${b.x} ${b.y}`;
13200
- if (curve === "zigzag") {
13201
- const mx = (a.x + b.x) / 2;
13202
- return `M ${a.x} ${a.y} L ${mx} ${a.y} L ${mx} ${b.y} L ${b.x} ${b.y}`;
13203
- }
13204
13552
  const dx = b.x - a.x;
13205
13553
  const dy = b.y - a.y;
13206
- const cx = (a.x + b.x) / 2 - dy * 0.25;
13207
- const cy = (a.y + b.y) / 2 + dx * 0.25;
13208
- return `M ${a.x} ${a.y} Q ${cx} ${cy} ${b.x} ${b.y}`;
13554
+ const len = Math.hypot(dx, dy) || 1;
13555
+ const ux = dx / len;
13556
+ const uy = dy / len;
13557
+ const aHalfW = (fromBbox[2] - fromBbox[0]) / 2;
13558
+ const aHalfH = (fromBbox[3] - fromBbox[1]) / 2;
13559
+ const bHalfW = (toBbox[2] - toBbox[0]) / 2;
13560
+ const bHalfH = (toBbox[3] - toBbox[1]) / 2;
13561
+ const aOff = Math.min(Math.max(aHalfW, aHalfH), 60);
13562
+ const bOff = Math.min(Math.max(bHalfW, bHalfH), 60);
13563
+ return {
13564
+ from: { x: a.x + ux * aOff, y: a.y + uy * aOff },
13565
+ to: { x: b.x - ux * bOff, y: b.y - uy * bOff }
13566
+ };
13567
+ }
13568
+ function arrowPath(from, to, curve) {
13569
+ if (curve === "straight") return `M ${from.x} ${from.y} L ${to.x} ${to.y}`;
13570
+ if (curve === "zigzag") {
13571
+ const mx = (from.x + to.x) / 2;
13572
+ return `M ${from.x} ${from.y} L ${mx} ${from.y} L ${mx} ${to.y} L ${to.x} ${to.y}`;
13573
+ }
13574
+ const dx = to.x - from.x;
13575
+ const dy = to.y - from.y;
13576
+ const cx = (from.x + to.x) / 2 - dy * 0.22;
13577
+ const cy = (from.y + to.y) / 2 + dx * 0.22;
13578
+ return `M ${from.x} ${from.y} Q ${cx} ${cy} ${to.x} ${to.y}`;
13209
13579
  }
13210
13580
  function CalloutArrow({ fromBbox, toBbox, action }) {
13211
- const d = arrowPath(fromBbox, toBbox, action.curve);
13212
- const label = action.label;
13213
- const target = centerOf(toBbox);
13214
- return /* @__PURE__ */ jsxs36(
13581
+ const markerId = useId3();
13582
+ const glowId = `${markerId}-glow`;
13583
+ const { from, to } = edgePoints(fromBbox, toBbox);
13584
+ const d = arrowPath(from, to, action.curve);
13585
+ return /* @__PURE__ */ jsxs39(
13215
13586
  "svg",
13216
13587
  {
13217
13588
  style: {
@@ -13222,247 +13593,142 @@ function CalloutArrow({ fromBbox, toBbox, action }) {
13222
13593
  },
13223
13594
  "data-role": "callout",
13224
13595
  children: [
13225
- /* @__PURE__ */ jsx46("defs", { children: /* @__PURE__ */ jsx46(
13226
- "marker",
13596
+ /* @__PURE__ */ jsxs39("defs", { children: [
13597
+ /* @__PURE__ */ jsx46(
13598
+ "marker",
13599
+ {
13600
+ id: markerId,
13601
+ viewBox: "0 0 12 10",
13602
+ refX: 10,
13603
+ refY: 5,
13604
+ markerWidth: 10,
13605
+ markerHeight: 10,
13606
+ orient: "auto",
13607
+ children: /* @__PURE__ */ jsx46(
13608
+ "path",
13609
+ {
13610
+ d: "M 1 1 L 10 5 L 1 9",
13611
+ fill: "none",
13612
+ stroke: ACCENT,
13613
+ strokeWidth: 1.8,
13614
+ strokeLinecap: "round",
13615
+ strokeLinejoin: "round"
13616
+ }
13617
+ )
13618
+ }
13619
+ ),
13620
+ /* @__PURE__ */ jsx46("filter", { id: glowId, x: "-20%", y: "-20%", width: "140%", height: "140%", children: /* @__PURE__ */ jsx46("feGaussianBlur", { stdDeviation: "2.5" }) })
13621
+ ] }),
13622
+ /* @__PURE__ */ jsx46(
13623
+ motion6.circle,
13624
+ {
13625
+ cx: from.x,
13626
+ cy: from.y,
13627
+ r: 4,
13628
+ fill: ACCENT,
13629
+ initial: { scale: 0, opacity: 0 },
13630
+ animate: { scale: 1, opacity: 1 },
13631
+ exit: { opacity: 0 },
13632
+ style: {
13633
+ transformOrigin: `${from.x}px ${from.y}px`,
13634
+ transformBox: "fill-box"
13635
+ },
13636
+ transition: { duration: 0.3, ease: EASE_OUT_EXPO }
13637
+ }
13638
+ ),
13639
+ /* @__PURE__ */ jsx46(
13640
+ motion6.circle,
13227
13641
  {
13228
- id: "arrowhead",
13229
- viewBox: "0 0 10 10",
13230
- refX: "8",
13231
- refY: "5",
13232
- markerWidth: "8",
13233
- markerHeight: "8",
13234
- orient: "auto",
13235
- children: /* @__PURE__ */ jsx46("path", { d: "M 0 0 L 10 5 L 0 10 z", fill: "#3B82F6" })
13642
+ cx: from.x,
13643
+ cy: from.y,
13644
+ r: 7,
13645
+ fill: "none",
13646
+ stroke: ACCENT,
13647
+ strokeWidth: 1.2,
13648
+ strokeOpacity: 0.4,
13649
+ initial: { scale: 0, opacity: 0 },
13650
+ animate: { scale: 1, opacity: 0.5 },
13651
+ exit: { opacity: 0 },
13652
+ style: {
13653
+ transformOrigin: `${from.x}px ${from.y}px`,
13654
+ transformBox: "fill-box"
13655
+ },
13656
+ transition: { duration: 0.4, delay: 0.08, ease: EASE_OUT_EXPO }
13236
13657
  }
13237
- ) }),
13658
+ ),
13238
13659
  /* @__PURE__ */ jsx46(
13239
13660
  motion6.path,
13240
13661
  {
13241
13662
  d,
13242
13663
  fill: "none",
13243
- stroke: "#3B82F6",
13244
- strokeWidth: 3,
13664
+ stroke: ACCENT,
13665
+ strokeWidth: 6,
13666
+ strokeOpacity: 0.18,
13245
13667
  strokeLinecap: "round",
13246
- markerEnd: "url(#arrowhead)",
13668
+ filter: `url(#${glowId})`,
13247
13669
  initial: { pathLength: 0, opacity: 0 },
13248
- animate: { pathLength: 1, opacity: 1 },
13670
+ animate: { pathLength: 1, opacity: 0.8 },
13249
13671
  exit: { opacity: 0 },
13250
- transition: { duration: 0.6, ease: "easeOut" }
13672
+ transition: { duration: 0.7, delay: 0.12, ease: EASE_OUT_EXPO }
13251
13673
  }
13252
13674
  ),
13253
- label ? /* @__PURE__ */ jsxs36(
13254
- motion6.g,
13675
+ /* @__PURE__ */ jsx46(
13676
+ motion6.path,
13255
13677
  {
13256
- initial: { opacity: 0 },
13257
- animate: { opacity: 1 },
13678
+ d,
13679
+ fill: "none",
13680
+ stroke: ACCENT,
13681
+ strokeWidth: 2.4,
13682
+ strokeLinecap: "round",
13683
+ strokeLinejoin: "round",
13684
+ markerEnd: `url(#${markerId})`,
13685
+ initial: { pathLength: 0, opacity: 0 },
13686
+ animate: { pathLength: 1, opacity: 1 },
13258
13687
  exit: { opacity: 0 },
13259
- transition: { delay: 0.3, duration: 0.3 },
13260
- children: [
13261
- /* @__PURE__ */ jsx46(
13262
- "rect",
13263
- {
13264
- x: target.x - 4,
13265
- y: target.y - 28,
13266
- width: label.length * 9 + 12,
13267
- height: 22,
13268
- rx: 4,
13269
- fill: "#1F2937"
13270
- }
13271
- ),
13272
- /* @__PURE__ */ jsx46(
13273
- "text",
13274
- {
13275
- x: target.x + 2,
13276
- y: target.y - 12,
13277
- fill: "white",
13278
- fontSize: 14,
13279
- fontFamily: "system-ui, sans-serif",
13280
- children: label
13281
- }
13282
- )
13283
- ]
13688
+ transition: { duration: 0.7, delay: 0.15, ease: EASE_OUT_EXPO }
13284
13689
  }
13285
- ) : null
13690
+ )
13286
13691
  ]
13287
13692
  }
13288
13693
  );
13289
13694
  }
13290
13695
 
13291
- // src/components/TutorMode/GhostReference.tsx
13696
+ // src/components/TutorMode/BoxOverlay.tsx
13292
13697
  import { motion as motion7 } from "framer-motion";
13293
- import { jsx as jsx47, jsxs as jsxs37 } from "react/jsx-runtime";
13294
- var POSITIONS = {
13295
- "top-right": { top: 40, right: 40 },
13296
- "top-left": { top: 40, left: 40 },
13297
- "bottom-right": { bottom: 40, right: 40 },
13298
- "bottom-left": { bottom: 40, left: 40 }
13299
- };
13300
- function GhostReference({
13301
- page,
13302
- sourceBbox,
13303
- sourceBlockText,
13304
- sourcePageNumber,
13305
- action
13306
- }) {
13307
- const width = 360;
13308
- const [x1, y1, x2, y2] = sourceBbox;
13309
- return /* @__PURE__ */ jsxs37(
13698
+ import { jsx as jsx47 } from "react/jsx-runtime";
13699
+ function BoxOverlay({ bbox, action }) {
13700
+ const [x1, y1, x2, y2] = bbox;
13701
+ const useSystem = !action.color || action.color === "#3B82F6" || action.color === "#3b82f6";
13702
+ const borderColor = useSystem ? ACCENT : action.color;
13703
+ const washColor = useSystem ? ACCENT_SOFT : void 0;
13704
+ return /* @__PURE__ */ jsx47(
13310
13705
  motion7.div,
13311
13706
  {
13312
- initial: { opacity: 0, y: 20, scale: 0.95 },
13313
- animate: { opacity: 1, y: 0, scale: 1 },
13314
- exit: { opacity: 0, y: 20, scale: 0.95 },
13315
- transition: { duration: 0.4, ease: "easeOut" },
13707
+ initial: { opacity: 0, scale: 0.98 },
13708
+ animate: { opacity: 1, scale: 1 },
13709
+ exit: { opacity: 0 },
13710
+ transition: { duration: 0.45, ease: EASE_OUT_EXPO },
13316
13711
  style: {
13317
13712
  position: "absolute",
13318
- width,
13319
- background: "#111",
13320
- color: "white",
13321
- borderRadius: 12,
13322
- padding: 12,
13323
- boxShadow: "0 10px 40px rgba(0,0,0,0.5)",
13713
+ left: x1 - 3,
13714
+ top: y1 - 3,
13715
+ width: x2 - x1 + 6,
13716
+ height: y2 - y1 + 6,
13717
+ border: `${action.style === "dashed" ? "2px dashed" : "2px solid"} ${borderColor}`,
13718
+ borderRadius: 4,
13719
+ background: washColor,
13324
13720
  pointerEvents: "none",
13325
- fontFamily: "system-ui, sans-serif",
13326
- fontSize: 13,
13327
- ...POSITIONS[action.position]
13328
- },
13329
- "data-role": "ghost-reference",
13330
- children: [
13331
- /* @__PURE__ */ jsxs37("div", { style: { opacity: 0.7, fontSize: 11, marginBottom: 6 }, children: [
13332
- "Page ",
13333
- sourcePageNumber,
13334
- " \u2014 ",
13335
- action.target_block
13336
- ] }),
13337
- /* @__PURE__ */ jsxs37(
13338
- "svg",
13339
- {
13340
- width: width - 24,
13341
- height: 160,
13342
- viewBox: `0 0 ${page.width} ${page.height}`,
13343
- style: { background: "#1F2937", borderRadius: 6, display: "block" },
13344
- preserveAspectRatio: "xMidYMid meet",
13345
- children: [
13346
- /* @__PURE__ */ jsx47(
13347
- "rect",
13348
- {
13349
- x: 0,
13350
- y: 0,
13351
- width: page.width,
13352
- height: page.height,
13353
- fill: "#1F2937"
13354
- }
13355
- ),
13356
- /* @__PURE__ */ jsx47(
13357
- "rect",
13358
- {
13359
- x: x1,
13360
- y: y1,
13361
- width: x2 - x1,
13362
- height: y2 - y1,
13363
- fill: "rgba(250,204,21,0.45)",
13364
- stroke: "#FBBF24",
13365
- strokeWidth: 8
13366
- }
13367
- )
13368
- ]
13369
- }
13370
- ),
13371
- /* @__PURE__ */ jsx47(
13372
- "div",
13373
- {
13374
- style: {
13375
- marginTop: 8,
13376
- fontSize: 12,
13377
- lineHeight: 1.4,
13378
- opacity: 0.9
13379
- },
13380
- children: sourceBlockText ?? "(figure)"
13381
- }
13382
- )
13383
- ]
13384
- }
13385
- );
13386
- }
13387
-
13388
- // src/components/TutorMode/BoxOverlay.tsx
13389
- import { motion as motion8 } from "framer-motion";
13390
- import { jsx as jsx48 } from "react/jsx-runtime";
13391
- function BoxOverlay({ bbox, action }) {
13392
- const [x1, y1, x2, y2] = bbox;
13393
- return /* @__PURE__ */ jsx48(
13394
- motion8.div,
13395
- {
13396
- initial: { opacity: 0, scale: 0.97 },
13397
- animate: { opacity: 1, scale: 1 },
13398
- exit: { opacity: 0 },
13399
- transition: { duration: 0.35, ease: "easeOut" },
13400
- style: {
13401
- position: "absolute",
13402
- left: x1,
13403
- top: y1,
13404
- width: x2 - x1,
13405
- height: y2 - y1,
13406
- border: `${action.style === "dashed" ? "3px dashed" : "3px solid"} ${action.color}`,
13407
- borderRadius: 6,
13408
- pointerEvents: "none",
13409
- boxSizing: "border-box"
13721
+ boxSizing: "border-box",
13722
+ // Subtle outer glow for the accent version, tinted to match.
13723
+ boxShadow: useSystem ? `0 0 0 1px rgba(176, 74, 26, 0.10), 0 8px 24px -10px rgba(176, 74, 26, 0.35)` : void 0
13410
13724
  },
13411
13725
  "data-role": "box"
13412
13726
  }
13413
13727
  );
13414
13728
  }
13415
13729
 
13416
- // src/components/TutorMode/StickyLabel.tsx
13417
- import { motion as motion9 } from "framer-motion";
13418
- import { jsx as jsx49 } from "react/jsx-runtime";
13419
- function position(bbox, where) {
13420
- const [x1, y1, x2, y2] = bbox;
13421
- const cx = (x1 + x2) / 2;
13422
- const cy = (y1 + y2) / 2;
13423
- const PAD = 16;
13424
- switch (where) {
13425
- case "top":
13426
- return { left: cx, top: y1 - PAD, transform: "translate(-50%, -100%)" };
13427
- case "bottom":
13428
- return { left: cx, top: y2 + PAD, transform: "translate(-50%, 0)" };
13429
- case "left":
13430
- return { left: x1 - PAD, top: cy, transform: "translate(-100%, -50%)" };
13431
- case "right":
13432
- return { left: x2 + PAD, top: cy, transform: "translate(0, -50%)" };
13433
- default:
13434
- return { left: cx, top: y1, transform: "translate(-50%, -100%)" };
13435
- }
13436
- }
13437
- function StickyLabel({ bbox, action }) {
13438
- return /* @__PURE__ */ jsx49(
13439
- motion9.div,
13440
- {
13441
- initial: { opacity: 0, scale: 0.9 },
13442
- animate: { opacity: 1, scale: 1 },
13443
- exit: { opacity: 0 },
13444
- transition: { duration: 0.35, ease: "easeOut" },
13445
- style: {
13446
- position: "absolute",
13447
- padding: "6px 10px",
13448
- background: "#FEF3C7",
13449
- color: "#78350F",
13450
- borderRadius: 6,
13451
- boxShadow: "0 3px 10px rgba(0,0,0,0.2)",
13452
- fontSize: 14,
13453
- fontFamily: "system-ui, sans-serif",
13454
- maxWidth: 280,
13455
- pointerEvents: "none",
13456
- ...position(bbox, action.position)
13457
- },
13458
- "data-role": "label",
13459
- children: action.text
13460
- }
13461
- );
13462
- }
13463
-
13464
13730
  // src/components/TutorMode/CinemaLayer.tsx
13465
- import { jsx as jsx50 } from "react/jsx-runtime";
13731
+ import { jsx as jsx48 } from "react/jsx-runtime";
13466
13732
  function blockBbox(index, block_id) {
13467
13733
  return index.blockById.get(block_id)?.block.bbox;
13468
13734
  }
@@ -13472,7 +13738,7 @@ function CinemaLayer({
13472
13738
  overlays,
13473
13739
  scale
13474
13740
  }) {
13475
- return /* @__PURE__ */ jsx50(
13741
+ return /* @__PURE__ */ jsx48(
13476
13742
  "div",
13477
13743
  {
13478
13744
  "data-role": "cinema-layer",
@@ -13492,13 +13758,13 @@ function CinemaLayer({
13492
13758
  // reachable because it sits OUTSIDE this stacking context.
13493
13759
  zIndex: 100
13494
13760
  },
13495
- children: /* @__PURE__ */ jsx50(AnimatePresence, { children: overlays.map((overlay) => {
13761
+ children: /* @__PURE__ */ jsx48(AnimatePresence, { children: overlays.map((overlay) => {
13496
13762
  switch (overlay.kind) {
13497
13763
  case "spotlight": {
13498
13764
  const a = overlay.action;
13499
13765
  const b = blockBbox(index, a.target_block);
13500
13766
  if (!b) return null;
13501
- return /* @__PURE__ */ jsx50(
13767
+ return /* @__PURE__ */ jsx48(
13502
13768
  SpotlightMask,
13503
13769
  {
13504
13770
  page: page.page_dimensions,
@@ -13512,26 +13778,26 @@ function CinemaLayer({
13512
13778
  const a = overlay.action;
13513
13779
  const b = blockBbox(index, a.target_block);
13514
13780
  if (!b) return null;
13515
- return /* @__PURE__ */ jsx50(AnimatedUnderline, { bbox: b, action: a }, overlay.id);
13781
+ return /* @__PURE__ */ jsx48(AnimatedUnderline, { bbox: b, action: a }, overlay.id);
13516
13782
  }
13517
13783
  case "highlight": {
13518
13784
  const a = overlay.action;
13519
13785
  const b = blockBbox(index, a.target_block);
13520
13786
  if (!b) return null;
13521
- return /* @__PURE__ */ jsx50(AnimatedHighlight, { bbox: b, action: a }, overlay.id);
13787
+ return /* @__PURE__ */ jsx48(AnimatedHighlight, { bbox: b, action: a }, overlay.id);
13522
13788
  }
13523
13789
  case "pulse": {
13524
13790
  const a = overlay.action;
13525
13791
  const b = blockBbox(index, a.target_block);
13526
13792
  if (!b) return null;
13527
- return /* @__PURE__ */ jsx50(PulseOverlay, { bbox: b, action: a }, overlay.id);
13793
+ return /* @__PURE__ */ jsx48(PulseOverlay, { bbox: b, action: a }, overlay.id);
13528
13794
  }
13529
13795
  case "callout": {
13530
13796
  const a = overlay.action;
13531
13797
  const from = blockBbox(index, a.from_block);
13532
13798
  const to = blockBbox(index, a.to_block);
13533
13799
  if (!from || !to) return null;
13534
- return /* @__PURE__ */ jsx50(
13800
+ return /* @__PURE__ */ jsx48(
13535
13801
  CalloutArrow,
13536
13802
  {
13537
13803
  fromBbox: from,
@@ -13541,36 +13807,16 @@ function CinemaLayer({
13541
13807
  overlay.id
13542
13808
  );
13543
13809
  }
13544
- case "ghost_reference": {
13545
- const a = overlay.action;
13546
- const hit = index.blockById.get(a.target_block);
13547
- if (!hit) return null;
13548
- const targetPage = index.byPage.get(a.target_page);
13549
- if (!targetPage) return null;
13550
- return /* @__PURE__ */ jsx50(
13551
- GhostReference,
13552
- {
13553
- page: targetPage.page_dimensions,
13554
- sourceBbox: hit.block.bbox,
13555
- sourceBlockText: hit.block.text,
13556
- sourcePageNumber: hit.pageNumber,
13557
- action: a
13558
- },
13559
- overlay.id
13560
- );
13561
- }
13810
+ case "ghost_reference":
13811
+ return null;
13562
13812
  case "box": {
13563
13813
  const a = overlay.action;
13564
13814
  const b = blockBbox(index, a.target_block);
13565
13815
  if (!b) return null;
13566
- return /* @__PURE__ */ jsx50(BoxOverlay, { bbox: b, action: a }, overlay.id);
13567
- }
13568
- case "label": {
13569
- const a = overlay.action;
13570
- const b = blockBbox(index, a.target_block);
13571
- if (!b) return null;
13572
- return /* @__PURE__ */ jsx50(StickyLabel, { bbox: b, action: a }, overlay.id);
13816
+ return /* @__PURE__ */ jsx48(BoxOverlay, { bbox: b, action: a }, overlay.id);
13573
13817
  }
13818
+ case "label":
13819
+ return null;
13574
13820
  case "clear":
13575
13821
  case "camera":
13576
13822
  return null;
@@ -13580,12 +13826,774 @@ function CinemaLayer({
13580
13826
  );
13581
13827
  }
13582
13828
 
13829
+ // src/components/TutorMode/GhostReferenceOverlay.tsx
13830
+ import { AnimatePresence as AnimatePresence2 } from "framer-motion";
13831
+
13832
+ // src/components/TutorMode/GhostReference.tsx
13833
+ import { motion as motion8 } from "framer-motion";
13834
+ import { jsx as jsx49, jsxs as jsxs40 } from "react/jsx-runtime";
13835
+ var POSITIONS = {
13836
+ "top-right": {
13837
+ top: "clamp(12px, 4vw, 40px)",
13838
+ right: "clamp(12px, 4vw, 40px)"
13839
+ },
13840
+ "top-left": {
13841
+ top: "clamp(12px, 4vw, 40px)",
13842
+ left: "clamp(12px, 4vw, 40px)"
13843
+ },
13844
+ "bottom-right": {
13845
+ bottom: "clamp(12px, 4vw, 40px)",
13846
+ right: "clamp(12px, 4vw, 40px)"
13847
+ },
13848
+ "bottom-left": {
13849
+ bottom: "clamp(12px, 4vw, 40px)",
13850
+ left: "clamp(12px, 4vw, 40px)"
13851
+ }
13852
+ };
13853
+ var INK2 = "#2a2420";
13854
+ var PAPER2 = "#faf6ec";
13855
+ var PAPER_DEEP = "#f3ece0";
13856
+ var ACCENT2 = "#b04a1a";
13857
+ var RULE = "rgba(42, 36, 32, 0.10)";
13858
+ var SERIF2 = "'Iowan Old Style', 'Palatino Linotype', Palatino, 'Book Antiqua', 'EB Garamond', 'Hoefler Text', Georgia, serif";
13859
+ function GhostReference({
13860
+ page,
13861
+ sourceBbox,
13862
+ sourceBlockText,
13863
+ action
13864
+ }) {
13865
+ const [x1, y1, x2, y2] = sourceBbox;
13866
+ const text = sourceBlockText ?? "(figure)";
13867
+ return /* @__PURE__ */ jsxs40(
13868
+ motion8.div,
13869
+ {
13870
+ initial: { opacity: 0, y: 24, scale: 0.97 },
13871
+ animate: { opacity: 1, y: 0, scale: 1 },
13872
+ exit: { opacity: 0, y: 20, scale: 0.97 },
13873
+ transition: {
13874
+ duration: 0.55,
13875
+ ease: [0.22, 1, 0.36, 1]
13876
+ // custom out-expo for an unhurried settle
13877
+ },
13878
+ style: {
13879
+ position: "absolute",
13880
+ width: "min(420px, calc(100vw - clamp(24px, 8vw, 80px)))",
13881
+ background: PAPER2,
13882
+ color: INK2,
13883
+ border: `1px solid ${RULE}`,
13884
+ // Barely-there corner radius — square-ish corners feel editorial,
13885
+ // 12px+ radius feels SaaS-notification. 3px is the sweet spot.
13886
+ borderRadius: 3,
13887
+ overflow: "hidden",
13888
+ // Warm, two-layer shadow: a tight contact shadow for definition,
13889
+ // a wider diffuse one tinted toward ink rather than pure grey.
13890
+ boxShadow: "0 1px 2px rgba(42, 36, 32, 0.10), 0 20px 44px -14px rgba(42, 36, 32, 0.22), 0 8px 20px -10px rgba(42, 36, 32, 0.14)",
13891
+ pointerEvents: "none",
13892
+ ...POSITIONS[action.position]
13893
+ },
13894
+ "data-role": "ghost-reference",
13895
+ children: [
13896
+ /* @__PURE__ */ jsx49(
13897
+ motion8.span,
13898
+ {
13899
+ "aria-hidden": true,
13900
+ initial: { scaleY: 0 },
13901
+ animate: { scaleY: 1 },
13902
+ exit: { scaleY: 0 },
13903
+ transition: { duration: 0.55, delay: 0.06, ease: [0.22, 1, 0.36, 1] },
13904
+ style: {
13905
+ position: "absolute",
13906
+ left: 0,
13907
+ top: 0,
13908
+ bottom: 0,
13909
+ width: 3,
13910
+ background: ACCENT2,
13911
+ transformOrigin: "top"
13912
+ }
13913
+ }
13914
+ ),
13915
+ /* @__PURE__ */ jsx49(
13916
+ "span",
13917
+ {
13918
+ "aria-hidden": true,
13919
+ style: {
13920
+ position: "absolute",
13921
+ inset: 0,
13922
+ pointerEvents: "none",
13923
+ background: `
13924
+ radial-gradient(120% 80% at 0% 0%, rgba(255, 244, 220, 0.5) 0%, transparent 55%),
13925
+ radial-gradient(100% 80% at 100% 100%, rgba(243, 229, 200, 0.35) 0%, transparent 60%)
13926
+ `
13927
+ }
13928
+ }
13929
+ ),
13930
+ /* @__PURE__ */ jsxs40(
13931
+ "div",
13932
+ {
13933
+ style: {
13934
+ position: "relative",
13935
+ padding: "20px 22px 20px 26px",
13936
+ display: "flex",
13937
+ gap: 16,
13938
+ alignItems: "flex-start"
13939
+ },
13940
+ children: [
13941
+ /* @__PURE__ */ jsx49(
13942
+ motion8.div,
13943
+ {
13944
+ "aria-hidden": true,
13945
+ initial: { opacity: 0, scale: 0.92 },
13946
+ animate: { opacity: 1, scale: 1 },
13947
+ exit: { opacity: 0, scale: 0.92 },
13948
+ transition: { duration: 0.45, delay: 0.18, ease: [0.22, 1, 0.36, 1] },
13949
+ style: {
13950
+ flexShrink: 0,
13951
+ width: 46,
13952
+ aspectRatio: `${page.width} / ${page.height}`,
13953
+ background: PAPER_DEEP,
13954
+ borderRadius: 2,
13955
+ overflow: "hidden",
13956
+ boxShadow: "inset 0 0 0 1px rgba(42, 36, 32, 0.12), 0 1px 3px rgba(42, 36, 32, 0.10)"
13957
+ },
13958
+ children: /* @__PURE__ */ jsxs40(
13959
+ "svg",
13960
+ {
13961
+ width: "100%",
13962
+ height: "100%",
13963
+ viewBox: `0 0 ${page.width} ${page.height}`,
13964
+ preserveAspectRatio: "xMidYMid meet",
13965
+ style: { display: "block" },
13966
+ children: [
13967
+ /* @__PURE__ */ jsx49(
13968
+ "rect",
13969
+ {
13970
+ x: 0,
13971
+ y: 0,
13972
+ width: page.width,
13973
+ height: page.height,
13974
+ fill: PAPER_DEEP
13975
+ }
13976
+ ),
13977
+ TEXT_LINES.map((ln, i) => /* @__PURE__ */ jsx49(
13978
+ "rect",
13979
+ {
13980
+ x: page.width * ln.x,
13981
+ y: page.height * ln.y,
13982
+ width: page.width * ln.w,
13983
+ height: page.height * 0.012,
13984
+ fill: "rgba(42, 36, 32, 0.18)"
13985
+ },
13986
+ i
13987
+ )),
13988
+ /* @__PURE__ */ jsx49(
13989
+ "rect",
13990
+ {
13991
+ x: x1,
13992
+ y: y1,
13993
+ width: x2 - x1,
13994
+ height: y2 - y1,
13995
+ fill: "rgba(176, 74, 26, 0.28)",
13996
+ stroke: ACCENT2,
13997
+ strokeWidth: 18
13998
+ }
13999
+ )
14000
+ ]
14001
+ }
14002
+ )
14003
+ }
14004
+ ),
14005
+ /* @__PURE__ */ jsxs40(
14006
+ motion8.div,
14007
+ {
14008
+ initial: { opacity: 0, y: 6 },
14009
+ animate: { opacity: 1, y: 0 },
14010
+ exit: { opacity: 0 },
14011
+ transition: { duration: 0.5, delay: 0.26, ease: [0.22, 1, 0.36, 1] },
14012
+ style: {
14013
+ flex: 1,
14014
+ minWidth: 0,
14015
+ fontFamily: SERIF2,
14016
+ fontSize: 15.5,
14017
+ lineHeight: 1.55,
14018
+ color: INK2,
14019
+ fontFeatureSettings: "'liga' 1, 'kern' 1, 'onum' 1",
14020
+ textRendering: "optimizeLegibility",
14021
+ letterSpacing: 0.05,
14022
+ // Hang an ornamental opening glyph outside the text column
14023
+ // so the reader's eye falls into the paragraph as if into a
14024
+ // well-set pull quote.
14025
+ position: "relative",
14026
+ paddingLeft: 2
14027
+ },
14028
+ children: [
14029
+ /* @__PURE__ */ jsx49(
14030
+ "span",
14031
+ {
14032
+ "aria-hidden": true,
14033
+ style: {
14034
+ position: "absolute",
14035
+ left: -14,
14036
+ top: -2,
14037
+ color: ACCENT2,
14038
+ fontSize: 22,
14039
+ lineHeight: 1,
14040
+ fontWeight: 500
14041
+ // ornamental flourish anchoring the paragraph
14042
+ },
14043
+ children: "\u2767"
14044
+ }
14045
+ ),
14046
+ /* @__PURE__ */ jsx49(
14047
+ "span",
14048
+ {
14049
+ style: {
14050
+ display: "-webkit-box",
14051
+ WebkitLineClamp: 8,
14052
+ WebkitBoxOrient: "vertical",
14053
+ overflow: "hidden"
14054
+ },
14055
+ children: text
14056
+ }
14057
+ )
14058
+ ]
14059
+ }
14060
+ )
14061
+ ]
14062
+ }
14063
+ ),
14064
+ /* @__PURE__ */ jsx49(
14065
+ motion8.span,
14066
+ {
14067
+ "aria-hidden": true,
14068
+ initial: { scaleX: 0, opacity: 0 },
14069
+ animate: { scaleX: 1, opacity: 0.6 },
14070
+ exit: { opacity: 0 },
14071
+ transition: { duration: 0.5, delay: 0.42, ease: [0.22, 1, 0.36, 1] },
14072
+ style: {
14073
+ position: "absolute",
14074
+ right: 22,
14075
+ bottom: 10,
14076
+ height: 1,
14077
+ width: 28,
14078
+ background: ACCENT2,
14079
+ transformOrigin: "right"
14080
+ }
14081
+ }
14082
+ )
14083
+ ]
14084
+ }
14085
+ );
14086
+ }
14087
+ var TEXT_LINES = [
14088
+ { x: 0.1, y: 0.12, w: 0.54 },
14089
+ { x: 0.1, y: 0.18, w: 0.68 },
14090
+ { x: 0.1, y: 0.24, w: 0.48 },
14091
+ { x: 0.1, y: 0.34, w: 0.72 },
14092
+ { x: 0.1, y: 0.4, w: 0.62 },
14093
+ { x: 0.1, y: 0.46, w: 0.66 },
14094
+ { x: 0.1, y: 0.56, w: 0.56 },
14095
+ { x: 0.1, y: 0.62, w: 0.7 },
14096
+ { x: 0.1, y: 0.68, w: 0.44 },
14097
+ { x: 0.1, y: 0.78, w: 0.64 },
14098
+ { x: 0.1, y: 0.84, w: 0.58 }
14099
+ ];
14100
+
14101
+ // src/components/TutorMode/GhostReferenceOverlay.tsx
14102
+ import { jsx as jsx50 } from "react/jsx-runtime";
14103
+ function GhostReferenceOverlay({
14104
+ overlays,
14105
+ index
14106
+ }) {
14107
+ const ghosts = overlays.filter((o) => o.kind === "ghost_reference");
14108
+ return /* @__PURE__ */ jsx50(
14109
+ "div",
14110
+ {
14111
+ "data-role": "ghost-reference-overlay",
14112
+ style: {
14113
+ position: "absolute",
14114
+ inset: 0,
14115
+ pointerEvents: "none",
14116
+ // Sits above the Reset view button (z-60) so a ghost card doesn't
14117
+ // disappear behind it if the host also renders the Reset button.
14118
+ zIndex: 70
14119
+ },
14120
+ children: /* @__PURE__ */ jsx50(AnimatePresence2, { children: ghosts.map((overlay) => {
14121
+ const a = overlay.action;
14122
+ const hit = index.blockById.get(a.target_block);
14123
+ if (!hit) return null;
14124
+ const targetPage = index.byPage.get(a.target_page);
14125
+ if (!targetPage) return null;
14126
+ return /* @__PURE__ */ jsx50(
14127
+ GhostReference,
14128
+ {
14129
+ page: targetPage.page_dimensions,
14130
+ sourceBbox: hit.block.bbox,
14131
+ sourceBlockText: hit.block.text,
14132
+ sourcePageNumber: hit.pageNumber,
14133
+ action: a
14134
+ },
14135
+ overlay.id
14136
+ );
14137
+ }) })
14138
+ }
14139
+ );
14140
+ }
14141
+
14142
+ // src/components/TutorMode/LabelOverlay.tsx
14143
+ import { AnimatePresence as AnimatePresence3 } from "framer-motion";
14144
+
14145
+ // src/components/TutorMode/StickyLabel.tsx
14146
+ import { motion as motion9 } from "framer-motion";
14147
+ import { jsx as jsx51, jsxs as jsxs41 } from "react/jsx-runtime";
14148
+ var INK3 = "#2a2420";
14149
+ var PAPER3 = "#faf6ec";
14150
+ var ACCENT3 = "#b04a1a";
14151
+ var SERIF3 = "'Iowan Old Style', 'Palatino Linotype', Palatino, 'Book Antiqua', 'EB Garamond', 'Hoefler Text', Georgia, serif";
14152
+ var STEM = 18;
14153
+ function StickyLabel({ screenAnchor, action }) {
14154
+ const { x, y } = screenAnchor;
14155
+ const layout = LAYOUTS[action.position] ?? LAYOUTS.top;
14156
+ return /* @__PURE__ */ jsxs41(
14157
+ motion9.div,
14158
+ {
14159
+ initial: { opacity: 0, scale: 0.88 },
14160
+ animate: { opacity: 1, scale: 1 },
14161
+ exit: { opacity: 0, scale: 0.92 },
14162
+ transition: {
14163
+ duration: 0.4,
14164
+ ease: [0.22, 1, 0.36, 1]
14165
+ },
14166
+ style: {
14167
+ position: "absolute",
14168
+ left: x,
14169
+ top: y,
14170
+ // Wrapper positions at the anchor; the body's transform places
14171
+ // it on the correct side via `layout.containerTransform`.
14172
+ transform: layout.containerTransform,
14173
+ pointerEvents: "none",
14174
+ // Compose transform-origin toward the anchor so scale-in feels
14175
+ // tethered to the block rather than free-floating.
14176
+ transformOrigin: layout.origin
14177
+ },
14178
+ "data-role": "label",
14179
+ children: [
14180
+ /* @__PURE__ */ jsx51(
14181
+ motion9.span,
14182
+ {
14183
+ "aria-hidden": true,
14184
+ initial: { scaleX: 0, scaleY: 0, opacity: 0 },
14185
+ animate: { scaleX: 1, scaleY: 1, opacity: 1 },
14186
+ exit: { opacity: 0 },
14187
+ transition: { duration: 0.35, ease: [0.22, 1, 0.36, 1] },
14188
+ style: {
14189
+ position: "absolute",
14190
+ background: ACCENT3,
14191
+ transformOrigin: layout.stemOrigin,
14192
+ ...layout.stem
14193
+ }
14194
+ }
14195
+ ),
14196
+ /* @__PURE__ */ jsx51(
14197
+ motion9.span,
14198
+ {
14199
+ "aria-hidden": true,
14200
+ initial: { scale: 0 },
14201
+ animate: { scale: 1 },
14202
+ exit: { scale: 0 },
14203
+ transition: { duration: 0.3, delay: 0.15, ease: [0.22, 1, 0.36, 1] },
14204
+ style: {
14205
+ position: "absolute",
14206
+ width: 6,
14207
+ height: 6,
14208
+ borderRadius: "50%",
14209
+ background: ACCENT3,
14210
+ boxShadow: `0 0 0 2px ${PAPER3}, 0 0 0 3px rgba(176, 74, 26, 0.25)`,
14211
+ ...layout.dot
14212
+ }
14213
+ }
14214
+ ),
14215
+ /* @__PURE__ */ jsx51(
14216
+ motion9.div,
14217
+ {
14218
+ initial: { y: layout.bodyIn.y, x: layout.bodyIn.x, opacity: 0 },
14219
+ animate: { y: 0, x: 0, opacity: 1 },
14220
+ exit: { opacity: 0 },
14221
+ transition: { duration: 0.4, delay: 0.08, ease: [0.22, 1, 0.36, 1] },
14222
+ style: {
14223
+ position: "absolute",
14224
+ ...layout.bodyAnchor,
14225
+ background: PAPER3,
14226
+ color: INK3,
14227
+ border: "1px solid rgba(42, 36, 32, 0.10)",
14228
+ borderRadius: 3,
14229
+ padding: "6px 12px 6px 14px",
14230
+ fontFamily: SERIF3,
14231
+ fontSize: 12.5,
14232
+ lineHeight: 1.25,
14233
+ letterSpacing: 0.6,
14234
+ textTransform: "uppercase",
14235
+ fontWeight: 500,
14236
+ whiteSpace: "nowrap",
14237
+ maxWidth: 240,
14238
+ overflow: "hidden",
14239
+ textOverflow: "ellipsis",
14240
+ // Warm two-layer shadow (matches GhostReference's palette).
14241
+ boxShadow: "0 1px 2px rgba(42, 36, 32, 0.12), 0 8px 18px -6px rgba(42, 36, 32, 0.22)",
14242
+ // Internal left accent rule — a 2px terracotta stripe.
14243
+ backgroundImage: `linear-gradient(to right, ${ACCENT3} 0, ${ACCENT3} 2px, transparent 2px)`,
14244
+ backgroundRepeat: "no-repeat",
14245
+ backgroundSize: "2px 100%",
14246
+ backgroundPosition: "left top"
14247
+ },
14248
+ children: action.text
14249
+ }
14250
+ )
14251
+ ]
14252
+ }
14253
+ );
14254
+ }
14255
+ var LAYOUTS = {
14256
+ top: {
14257
+ containerTransform: "translate(-50%, -100%)",
14258
+ origin: "50% 100%",
14259
+ stem: {
14260
+ left: "50%",
14261
+ bottom: -STEM,
14262
+ width: 1,
14263
+ height: STEM - 4,
14264
+ transform: "translateX(-50%)"
14265
+ },
14266
+ stemOrigin: "bottom",
14267
+ dot: {
14268
+ left: "50%",
14269
+ bottom: -STEM + 1,
14270
+ transform: "translate(-50%, 100%)"
14271
+ },
14272
+ bodyAnchor: { bottom: 0, left: "50%", transform: "translateX(-50%)" },
14273
+ bodyIn: { x: 0, y: -4 }
14274
+ },
14275
+ bottom: {
14276
+ containerTransform: "translate(-50%, 0%)",
14277
+ origin: "50% 0%",
14278
+ stem: {
14279
+ left: "50%",
14280
+ top: STEM - (STEM - 4),
14281
+ width: 1,
14282
+ height: STEM - 4,
14283
+ transform: "translateX(-50%)"
14284
+ },
14285
+ stemOrigin: "top",
14286
+ dot: {
14287
+ left: "50%",
14288
+ top: -1,
14289
+ transform: "translate(-50%, -100%)"
14290
+ },
14291
+ bodyAnchor: { top: STEM, left: "50%", transform: "translateX(-50%)" },
14292
+ bodyIn: { x: 0, y: 4 }
14293
+ },
14294
+ left: {
14295
+ containerTransform: "translate(-100%, -50%)",
14296
+ origin: "100% 50%",
14297
+ stem: {
14298
+ top: "50%",
14299
+ right: -STEM,
14300
+ width: STEM - 4,
14301
+ height: 1,
14302
+ transform: "translateY(-50%)"
14303
+ },
14304
+ stemOrigin: "right",
14305
+ dot: {
14306
+ right: -STEM + 1,
14307
+ top: "50%",
14308
+ transform: "translate(100%, -50%)"
14309
+ },
14310
+ bodyAnchor: { right: 0, top: "50%", transform: "translateY(-50%)" },
14311
+ bodyIn: { x: -4, y: 0 }
14312
+ },
14313
+ right: {
14314
+ containerTransform: "translate(0%, -50%)",
14315
+ origin: "0% 50%",
14316
+ stem: {
14317
+ top: "50%",
14318
+ left: STEM - (STEM - 4),
14319
+ width: STEM - 4,
14320
+ height: 1,
14321
+ transform: "translateY(-50%)"
14322
+ },
14323
+ stemOrigin: "left",
14324
+ dot: {
14325
+ left: -1,
14326
+ top: "50%",
14327
+ transform: "translate(-100%, -50%)"
14328
+ },
14329
+ bodyAnchor: { left: STEM, top: "50%", transform: "translateY(-50%)" },
14330
+ bodyIn: { x: 4, y: 0 }
14331
+ }
14332
+ };
14333
+
14334
+ // src/components/TutorMode/LabelOverlay.tsx
14335
+ import { jsx as jsx52 } from "react/jsx-runtime";
14336
+ function LabelOverlay({
14337
+ overlays,
14338
+ index,
14339
+ currentPage,
14340
+ camera,
14341
+ viewport
14342
+ }) {
14343
+ const labels = overlays.filter((o) => o.kind === "label");
14344
+ const page = index.byPage.get(currentPage);
14345
+ return /* @__PURE__ */ jsx52(
14346
+ "div",
14347
+ {
14348
+ "data-role": "label-overlay",
14349
+ style: {
14350
+ position: "absolute",
14351
+ inset: 0,
14352
+ pointerEvents: "none",
14353
+ overflow: "hidden",
14354
+ // Above CameraView and the Reset button but not above
14355
+ // GhostReferenceOverlay (z:70) — labels are block-attached and
14356
+ // should not cover a cross-page reference card.
14357
+ zIndex: 65
14358
+ },
14359
+ children: /* @__PURE__ */ jsx52(AnimatePresence3, { children: page ? labels.map((overlay) => {
14360
+ const a = overlay.action;
14361
+ const hit = index.blockById.get(a.target_block);
14362
+ if (!hit) return null;
14363
+ const anchor = computeScreenAnchor(
14364
+ hit.block.bbox,
14365
+ a.position,
14366
+ page,
14367
+ camera,
14368
+ viewport
14369
+ );
14370
+ return /* @__PURE__ */ jsx52(
14371
+ StickyLabel,
14372
+ {
14373
+ screenAnchor: anchor,
14374
+ action: a
14375
+ },
14376
+ overlay.id
14377
+ );
14378
+ }) : null })
14379
+ }
14380
+ );
14381
+ }
14382
+ function computeScreenAnchor(bbox, where, page, camera, viewport) {
14383
+ const [x1, y1, x2, y2] = bbox;
14384
+ const pageCX = page.page_dimensions.width / 2;
14385
+ const pageCY = page.page_dimensions.height / 2;
14386
+ let px, py;
14387
+ switch (where) {
14388
+ case "top":
14389
+ px = (x1 + x2) / 2;
14390
+ py = y1;
14391
+ break;
14392
+ case "bottom":
14393
+ px = (x1 + x2) / 2;
14394
+ py = y2;
14395
+ break;
14396
+ case "left":
14397
+ px = x1;
14398
+ py = (y1 + y2) / 2;
14399
+ break;
14400
+ case "right":
14401
+ px = x2;
14402
+ py = (y1 + y2) / 2;
14403
+ break;
14404
+ default:
14405
+ px = (x1 + x2) / 2;
14406
+ py = y1;
14407
+ }
14408
+ const screenX = viewport.width / 2 + camera.x + (px - pageCX) * camera.scale;
14409
+ const screenY = viewport.height / 2 + camera.y + (py - pageCY) * camera.scale;
14410
+ return { x: screenX, y: screenY };
14411
+ }
14412
+
14413
+ // src/components/TutorMode/CalloutLabelOverlay.tsx
14414
+ import { AnimatePresence as AnimatePresence4, motion as motion10 } from "framer-motion";
14415
+ import { jsx as jsx53 } from "react/jsx-runtime";
14416
+ function CalloutLabelOverlay({
14417
+ overlays,
14418
+ index,
14419
+ currentPage,
14420
+ camera,
14421
+ viewport
14422
+ }) {
14423
+ const callouts = overlays.filter(
14424
+ (o) => o.kind === "callout" && o.action.label
14425
+ );
14426
+ const page = index.byPage.get(currentPage);
14427
+ return /* @__PURE__ */ jsx53(
14428
+ "div",
14429
+ {
14430
+ "data-role": "callout-label-overlay",
14431
+ style: {
14432
+ position: "absolute",
14433
+ inset: 0,
14434
+ pointerEvents: "none",
14435
+ overflow: "hidden",
14436
+ // Above the arrow stroke (which is inside CameraView) and the
14437
+ // reset button, below the ghost card.
14438
+ zIndex: 68
14439
+ },
14440
+ children: /* @__PURE__ */ jsx53(AnimatePresence4, { children: page ? callouts.map((overlay) => {
14441
+ const a = overlay.action;
14442
+ const fromHit = index.blockById.get(a.from_block);
14443
+ const toHit = index.blockById.get(a.to_block);
14444
+ if (!fromHit || !toHit || !a.label) return null;
14445
+ const pos = computePillAnchor(
14446
+ fromHit.block.bbox,
14447
+ toHit.block.bbox,
14448
+ page,
14449
+ camera,
14450
+ viewport
14451
+ );
14452
+ return /* @__PURE__ */ jsx53(
14453
+ CalloutLabelPill,
14454
+ {
14455
+ label: a.label,
14456
+ anchor: pos,
14457
+ side: pos.side
14458
+ },
14459
+ overlay.id
14460
+ );
14461
+ }) : null })
14462
+ }
14463
+ );
14464
+ }
14465
+ function computePillAnchor(fromBbox, toBbox, page, camera, viewport) {
14466
+ const aCX = (fromBbox[0] + fromBbox[2]) / 2;
14467
+ const aCY = (fromBbox[1] + fromBbox[3]) / 2;
14468
+ const bCX = (toBbox[0] + toBbox[2]) / 2;
14469
+ const bCY = (toBbox[1] + toBbox[3]) / 2;
14470
+ const dx = bCX - aCX;
14471
+ const dy = bCY - aCY;
14472
+ const len = Math.hypot(dx, dy) || 1;
14473
+ const ux = dx / len;
14474
+ const uy = dy / len;
14475
+ const bHalfW = (toBbox[2] - toBbox[0]) / 2;
14476
+ const bHalfH = (toBbox[3] - toBbox[1]) / 2;
14477
+ const bOff = Math.min(Math.max(bHalfW, bHalfH), 60);
14478
+ const toX = bCX - ux * bOff;
14479
+ const toY = bCY - uy * bOff;
14480
+ const pageCX = page.page_dimensions.width / 2;
14481
+ const pageCY = page.page_dimensions.height / 2;
14482
+ const tipScreenX = viewport.width / 2 + camera.x + (toX - pageCX) * camera.scale;
14483
+ const tipScreenY = viewport.height / 2 + camera.y + (toY - pageCY) * camera.scale;
14484
+ const isVertical = Math.abs(dy) >= Math.abs(dx);
14485
+ const OFFSET = 32;
14486
+ const MAX_PILL_W = 220;
14487
+ const MAX_PILL_H = 30;
14488
+ const SAFE = 16;
14489
+ if (isVertical) {
14490
+ const canFitRight = tipScreenX + OFFSET + MAX_PILL_W < viewport.width - SAFE;
14491
+ const side2 = canFitRight ? "right" : "left";
14492
+ return {
14493
+ x: tipScreenX + (side2 === "right" ? OFFSET : -OFFSET),
14494
+ y: tipScreenY,
14495
+ side: side2
14496
+ };
14497
+ }
14498
+ const canFitBelow = tipScreenY + OFFSET + MAX_PILL_H < viewport.height - SAFE;
14499
+ const side = canFitBelow ? "below" : "above";
14500
+ return {
14501
+ x: tipScreenX,
14502
+ y: tipScreenY + (side === "below" ? OFFSET : -OFFSET),
14503
+ side
14504
+ };
14505
+ }
14506
+ function CalloutLabelPill({
14507
+ label,
14508
+ anchor,
14509
+ side
14510
+ }) {
14511
+ const spec = PILL_SIDE_SPECS[side];
14512
+ return /* @__PURE__ */ jsx53(
14513
+ motion10.div,
14514
+ {
14515
+ initial: { opacity: 0, scale: 0.92, ...spec.slideIn },
14516
+ animate: { opacity: 1, scale: 1, x: 0, y: 0 },
14517
+ exit: { opacity: 0, scale: 0.94 },
14518
+ transition: { duration: 0.45, delay: 0.5, ease: EASE_OUT_EXPO },
14519
+ style: {
14520
+ position: "absolute",
14521
+ left: anchor.x,
14522
+ top: anchor.y,
14523
+ transform: spec.transform,
14524
+ pointerEvents: "none",
14525
+ background: PAPER,
14526
+ color: INK,
14527
+ border: "1px solid rgba(42, 36, 32, 0.10)",
14528
+ borderRadius: 3,
14529
+ padding: spec.padding,
14530
+ fontFamily: SERIF,
14531
+ fontSize: 11.5,
14532
+ lineHeight: 1.2,
14533
+ letterSpacing: 0.6,
14534
+ textTransform: "uppercase",
14535
+ fontWeight: 500,
14536
+ whiteSpace: "nowrap",
14537
+ maxWidth: 220,
14538
+ overflow: "hidden",
14539
+ textOverflow: "ellipsis",
14540
+ boxShadow: "0 1px 2px rgba(42, 36, 32, 0.12), 0 8px 18px -6px rgba(42, 36, 32, 0.22)",
14541
+ // Accent rule on the "inward" edge (the one closest to the arrow).
14542
+ backgroundImage: spec.accentGradient,
14543
+ backgroundRepeat: "no-repeat",
14544
+ backgroundSize: spec.accentSize,
14545
+ backgroundPosition: spec.accentPosition
14546
+ },
14547
+ "data-role": "callout-label",
14548
+ children: label
14549
+ }
14550
+ );
14551
+ }
14552
+ var PILL_SIDE_SPECS = {
14553
+ // Pill sits to the RIGHT of a vertical arrow → left edge anchors at
14554
+ // offset point, accent rule on the left (pointing back toward arrow).
14555
+ right: {
14556
+ transform: "translate(0, -50%)",
14557
+ slideIn: { x: -6 },
14558
+ padding: "5px 12px 5px 14px",
14559
+ accentGradient: `linear-gradient(to right, ${ACCENT} 0, ${ACCENT} 2px, transparent 2px)`,
14560
+ accentSize: "2px 100%",
14561
+ accentPosition: "left top"
14562
+ },
14563
+ left: {
14564
+ transform: "translate(-100%, -50%)",
14565
+ slideIn: { x: 6 },
14566
+ padding: "5px 14px 5px 12px",
14567
+ accentGradient: `linear-gradient(to left, ${ACCENT} 0, ${ACCENT} 2px, transparent 2px)`,
14568
+ accentSize: "2px 100%",
14569
+ accentPosition: "right top"
14570
+ },
14571
+ // Pill sits BELOW a horizontal arrow → top edge anchors at offset
14572
+ // point, accent rule on the top (pointing back up toward arrow).
14573
+ below: {
14574
+ transform: "translate(-50%, 0)",
14575
+ slideIn: { y: -6 },
14576
+ padding: "7px 12px 5px 12px",
14577
+ accentGradient: `linear-gradient(to bottom, ${ACCENT} 0, ${ACCENT} 2px, transparent 2px)`,
14578
+ accentSize: "100% 2px",
14579
+ accentPosition: "left top"
14580
+ },
14581
+ above: {
14582
+ transform: "translate(-50%, -100%)",
14583
+ slideIn: { y: 6 },
14584
+ padding: "5px 12px 7px 12px",
14585
+ accentGradient: `linear-gradient(to top, ${ACCENT} 0, ${ACCENT} 2px, transparent 2px)`,
14586
+ accentSize: "100% 2px",
14587
+ accentPosition: "left bottom"
14588
+ }
14589
+ };
14590
+
13583
14591
  // src/components/TutorMode/SubtitleBar.tsx
13584
- import { AnimatePresence as AnimatePresence2, motion as motion10 } from "framer-motion";
13585
- import { jsx as jsx51 } from "react/jsx-runtime";
14592
+ import { AnimatePresence as AnimatePresence5, motion as motion11 } from "framer-motion";
14593
+ import { jsx as jsx54 } from "react/jsx-runtime";
13586
14594
  function SubtitleBar({ text }) {
13587
- return /* @__PURE__ */ jsx51(AnimatePresence2, { children: text ? /* @__PURE__ */ jsx51(
13588
- motion10.div,
14595
+ return /* @__PURE__ */ jsx54(AnimatePresence5, { children: text ? /* @__PURE__ */ jsx54(
14596
+ motion11.div,
13589
14597
  {
13590
14598
  initial: { opacity: 0, y: 20 },
13591
14599
  animate: { opacity: 1, y: 0 },
@@ -13621,7 +14629,20 @@ init_camera_math();
13621
14629
  var DEFAULT_MIN_OVERLAY_MS = 3500;
13622
14630
  var StoryboardEngine = class {
13623
14631
  constructor(deps) {
13624
- this.pendingTimers = /* @__PURE__ */ new Set();
14632
+ /**
14633
+ * Timers that schedule the START of a step (via `setTimeout(runStep, at_ms)`).
14634
+ * These are storyboard-scoped: when a new storyboard arrives, anything still
14635
+ * pending should be abandoned.
14636
+ */
14637
+ this.pendingStepTimers = /* @__PURE__ */ new Set();
14638
+ /**
14639
+ * Timers that auto-REMOVE an already-placed overlay after its visible
14640
+ * duration. Keyed by overlay id so we can cancel one specifically. These are
14641
+ * NOT cancelled when a new storyboard starts — otherwise the still-visible
14642
+ * overlay from the previous beat would get stranded in the store forever
14643
+ * (the "stuck spotlight" bug).
14644
+ */
14645
+ this.overlayRemovalTimers = /* @__PURE__ */ new Map();
13625
14646
  this.currentStoryboardId = 0;
13626
14647
  this.deps = deps;
13627
14648
  }
@@ -13664,13 +14685,13 @@ var StoryboardEngine = class {
13664
14685
  if (storyboardId !== this.currentStoryboardId) return;
13665
14686
  this.runStep(step);
13666
14687
  }, step.at_ms);
13667
- this.pendingTimers.add(timer);
14688
+ this.pendingStepTimers.add(timer);
13668
14689
  }
13669
14690
  const markExecuting = setTimeout(() => {
13670
14691
  if (storyboardId !== this.currentStoryboardId) return;
13671
14692
  narrationStore.getState().setEngineStatus("executing");
13672
14693
  }, 0);
13673
- this.pendingTimers.add(markExecuting);
14694
+ this.pendingStepTimers.add(markExecuting);
13674
14695
  const last = steps[steps.length - 1];
13675
14696
  if (last) {
13676
14697
  const totalMs = last.at_ms + last.duration_ms;
@@ -13678,18 +14699,29 @@ var StoryboardEngine = class {
13678
14699
  if (storyboardId !== this.currentStoryboardId) return;
13679
14700
  narrationStore.getState().setEngineStatus("idle");
13680
14701
  }, totalMs + 50);
13681
- this.pendingTimers.add(markIdle);
14702
+ this.pendingStepTimers.add(markIdle);
13682
14703
  }
13683
14704
  }
13684
- /** Abort all pending steps and set engine status to idle. */
14705
+ /**
14706
+ * Abort pending STEP dispatches from the current storyboard. Overlay
14707
+ * removal timers are left alone so already-visible overlays still auto-
14708
+ * expire on their own schedule. To force-clear every overlay, call
14709
+ * `resetVisuals()` instead.
14710
+ */
13685
14711
  cancelPending() {
13686
- for (const t of this.pendingTimers) clearTimeout(t);
13687
- this.pendingTimers.clear();
14712
+ for (const t of this.pendingStepTimers) clearTimeout(t);
14713
+ this.pendingStepTimers.clear();
13688
14714
  this.deps.narrationStore.getState().setEngineStatus("idle");
13689
14715
  }
13690
- /** Reset visuals: clear overlays, fit camera back to page. */
14716
+ /** Cancel every removal timer (used by resetVisuals only). */
14717
+ cancelAllRemovalTimers() {
14718
+ for (const t of this.overlayRemovalTimers.values()) clearTimeout(t);
14719
+ this.overlayRemovalTimers.clear();
14720
+ }
14721
+ /** Reset visuals: clear overlays, cancel every removal timer, fit camera. */
13691
14722
  resetVisuals() {
13692
14723
  this.cancelPending();
14724
+ this.cancelAllRemovalTimers();
13693
14725
  const { narrationStore, bboxIndex, getViewport } = this.deps;
13694
14726
  narrationStore.getState().clearOverlays();
13695
14727
  const viewport = getViewport();
@@ -13760,8 +14792,9 @@ var StoryboardEngine = class {
13760
14792
  narrationStore.getState().addOverlay(overlay);
13761
14793
  const timer = setTimeout(() => {
13762
14794
  narrationStore.getState().removeOverlay(overlay.id);
14795
+ this.overlayRemovalTimers.delete(overlay.id);
13763
14796
  }, visibleMs);
13764
- this.pendingTimers.add(timer);
14797
+ this.overlayRemovalTimers.set(overlay.id, timer);
13765
14798
  return true;
13766
14799
  }
13767
14800
  applyCamera(action, durationMs) {
@@ -13786,12 +14819,17 @@ var StoryboardEngine = class {
13786
14819
  const blockCY = (y1 + y2) / 2;
13787
14820
  const pageCX = pageDims.page_dimensions.width / 2;
13788
14821
  const pageCY = pageDims.page_dimensions.height / 2;
13789
- const x = (pageCX - blockCX) * finalScale;
13790
- const y = (pageCY - blockCY) * finalScale;
14822
+ const rawX = (pageCX - blockCX) * finalScale;
14823
+ const rawY = (pageCY - blockCY) * finalScale;
14824
+ const clamped = clampCamera(
14825
+ { scale: finalScale, x: rawX, y: rawY },
14826
+ pageDims.page_dimensions,
14827
+ viewport
14828
+ );
13791
14829
  const camera = {
13792
- scale: finalScale,
13793
- x,
13794
- y,
14830
+ scale: clamped.scale,
14831
+ x: clamped.x,
14832
+ y: clamped.y,
13795
14833
  easing: action.easing
13796
14834
  };
13797
14835
  narrationStore.getState().setCamera(camera);
@@ -14776,7 +15814,7 @@ function storyboardFromMatch(match, page) {
14776
15814
  }
14777
15815
 
14778
15816
  // src/components/TutorMode/TutorModeContainer.tsx
14779
- import { jsx as jsx52, jsxs as jsxs38 } from "react/jsx-runtime";
15817
+ import { Fragment as Fragment4, jsx as jsx55, jsxs as jsxs42 } from "react/jsx-runtime";
14780
15818
  function buildBBoxIndex(bboxData) {
14781
15819
  const byPage = /* @__PURE__ */ new Map();
14782
15820
  const blockById = /* @__PURE__ */ new Map();
@@ -14812,15 +15850,35 @@ function TutorModeContainer({
14812
15850
  showExitButton = true,
14813
15851
  onExitTutorMode,
14814
15852
  minOverlayDurationMs,
15853
+ backgroundColor = "#ffffff",
15854
+ loadingComponent,
15855
+ onPageChange,
14815
15856
  className
14816
15857
  }) {
14817
15858
  const containerRef = useRef27(null);
14818
15859
  const index = useMemo15(() => buildBBoxIndex(bboxData), [bboxData]);
14819
- const { document: document2 } = usePDFViewer();
15860
+ const {
15861
+ document: document2,
15862
+ currentPage: viewerCurrentPage,
15863
+ numPages,
15864
+ goToPage: viewerGoToPage
15865
+ } = usePDFViewer();
14820
15866
  const [pageProxy, setPageProxy] = useState30(null);
14821
15867
  const [viewport, setViewport] = useState30({ width: 800, height: 1e3 });
14822
15868
  const camera = useStore2(narrationStore, (s) => s.camera);
14823
15869
  const activeOverlays = useStore2(narrationStore, (s) => s.activeOverlays);
15870
+ useEffect28(() => {
15871
+ if (numPages <= 0) return;
15872
+ if (pageNumber < 1 || pageNumber > numPages) return;
15873
+ if (viewerCurrentPage === pageNumber) return;
15874
+ viewerGoToPage(pageNumber);
15875
+ }, [pageNumber, numPages, viewerCurrentPage, viewerGoToPage]);
15876
+ useEffect28(() => {
15877
+ if (!onPageChange) return;
15878
+ if (viewerCurrentPage === pageNumber) return;
15879
+ if (viewerCurrentPage < 1) return;
15880
+ onPageChange(viewerCurrentPage);
15881
+ }, [viewerCurrentPage, pageNumber, onPageChange]);
14824
15882
  useEffect28(() => {
14825
15883
  if (!containerRef.current) return;
14826
15884
  const el = containerRef.current;
@@ -14976,7 +16034,8 @@ function TutorModeContainer({
14976
16034
  const rasterScale = dpiScale * (scale || 1);
14977
16035
  const baseW = page ? page.page_dimensions.width * (scale || 1) : 0;
14978
16036
  const baseH = page ? page.page_dimensions.height * (scale || 1) : 0;
14979
- return /* @__PURE__ */ jsxs38(
16037
+ const isReady = !!page && !!pageProxy;
16038
+ return /* @__PURE__ */ jsxs42(
14980
16039
  "div",
14981
16040
  {
14982
16041
  ref: containerRef,
@@ -14986,12 +16045,12 @@ function TutorModeContainer({
14986
16045
  width: "100%",
14987
16046
  height: "100%",
14988
16047
  overflow: "hidden",
14989
- background: "#111"
16048
+ background: backgroundColor
14990
16049
  },
14991
16050
  "data-role": "tutor-mode-container",
14992
- "data-page-loaded": page ? "true" : "false",
16051
+ "data-page-loaded": isReady ? "true" : "false",
14993
16052
  children: [
14994
- showExitButton ? /* @__PURE__ */ jsx52(
16053
+ showExitButton && isReady ? /* @__PURE__ */ jsx55(
14995
16054
  "button",
14996
16055
  {
14997
16056
  onClick: () => {
@@ -15008,7 +16067,9 @@ function TutorModeContainer({
15008
16067
  padding: "8px 14px",
15009
16068
  border: "none",
15010
16069
  borderRadius: 8,
15011
- background: "rgba(255,255,255,0.12)",
16070
+ // Dark translucent pill with white text reads cleanly on both
16071
+ // light and dark container backgrounds.
16072
+ background: "rgba(17,24,39,0.72)",
15012
16073
  color: "white",
15013
16074
  cursor: "pointer",
15014
16075
  fontFamily: "system-ui, sans-serif",
@@ -15020,43 +16081,127 @@ function TutorModeContainer({
15020
16081
  children: "Reset view"
15021
16082
  }
15022
16083
  ) : null,
15023
- page ? /* @__PURE__ */ jsx52(CameraView, { camera, children: /* @__PURE__ */ jsxs38(
16084
+ isReady ? /* @__PURE__ */ jsxs42(Fragment4, { children: [
16085
+ /* @__PURE__ */ jsx55(CameraView, { camera, children: /* @__PURE__ */ jsxs42(
16086
+ "div",
16087
+ {
16088
+ style: {
16089
+ position: "absolute",
16090
+ top: "50%",
16091
+ left: "50%",
16092
+ width: baseW,
16093
+ height: baseH,
16094
+ transform: "translate(-50%, -50%)"
16095
+ },
16096
+ children: [
16097
+ /* @__PURE__ */ jsx55(
16098
+ PDFPage,
16099
+ {
16100
+ pageNumber,
16101
+ page: pageProxy,
16102
+ scale: rasterScale,
16103
+ rotation,
16104
+ showTextLayer: false,
16105
+ showHighlightLayer: false,
16106
+ showAnnotationLayer: false
16107
+ }
16108
+ ),
16109
+ /* @__PURE__ */ jsx55(
16110
+ CinemaLayer,
16111
+ {
16112
+ page,
16113
+ index,
16114
+ overlays: activeOverlays,
16115
+ scale: scale || 1
16116
+ }
16117
+ )
16118
+ ]
16119
+ }
16120
+ ) }),
16121
+ /* @__PURE__ */ jsx55(
16122
+ LabelOverlay,
16123
+ {
16124
+ overlays: activeOverlays,
16125
+ index,
16126
+ currentPage: pageNumber,
16127
+ camera,
16128
+ viewport
16129
+ }
16130
+ ),
16131
+ /* @__PURE__ */ jsx55(
16132
+ CalloutLabelOverlay,
16133
+ {
16134
+ overlays: activeOverlays,
16135
+ index,
16136
+ currentPage: pageNumber,
16137
+ camera,
16138
+ viewport
16139
+ }
16140
+ ),
16141
+ /* @__PURE__ */ jsx55(GhostReferenceOverlay, { overlays: activeOverlays, index })
16142
+ ] }) : /* @__PURE__ */ jsx55(TutorLoadingState, { custom: loadingComponent }),
16143
+ showSubtitles ? /* @__PURE__ */ jsx55(SubtitleBar, { text: currentChunk ?? null }) : null
16144
+ ]
16145
+ }
16146
+ );
16147
+ }
16148
+ function TutorLoadingState({
16149
+ custom
16150
+ }) {
16151
+ if (custom) {
16152
+ return /* @__PURE__ */ jsx55(
16153
+ "div",
16154
+ {
16155
+ style: {
16156
+ position: "absolute",
16157
+ inset: 0,
16158
+ display: "flex",
16159
+ alignItems: "center",
16160
+ justifyContent: "center"
16161
+ },
16162
+ "data-role": "tutor-loading",
16163
+ children: custom
16164
+ }
16165
+ );
16166
+ }
16167
+ return /* @__PURE__ */ jsxs42(
16168
+ "div",
16169
+ {
16170
+ style: {
16171
+ position: "absolute",
16172
+ inset: 0,
16173
+ display: "flex",
16174
+ flexDirection: "column",
16175
+ alignItems: "center",
16176
+ justifyContent: "center",
16177
+ gap: 12,
16178
+ color: "rgba(0,0,0,0.55)",
16179
+ fontFamily: "system-ui, sans-serif",
16180
+ fontSize: 13
16181
+ },
16182
+ "data-role": "tutor-loading",
16183
+ children: [
16184
+ /* @__PURE__ */ jsx55(
15024
16185
  "div",
15025
16186
  {
16187
+ "aria-hidden": true,
15026
16188
  style: {
15027
- position: "absolute",
15028
- top: "50%",
15029
- left: "50%",
15030
- width: baseW,
15031
- height: baseH,
15032
- transform: "translate(-50%, -50%)"
15033
- },
15034
- children: [
15035
- /* @__PURE__ */ jsx52(
15036
- PDFPage,
15037
- {
15038
- pageNumber,
15039
- page: pageProxy,
15040
- scale: rasterScale,
15041
- rotation,
15042
- showTextLayer: false,
15043
- showHighlightLayer: false,
15044
- showAnnotationLayer: false
15045
- }
15046
- ),
15047
- /* @__PURE__ */ jsx52(
15048
- CinemaLayer,
15049
- {
15050
- page,
15051
- index,
15052
- overlays: activeOverlays,
15053
- scale: scale || 1
15054
- }
15055
- )
15056
- ]
16189
+ width: 36,
16190
+ height: 36,
16191
+ borderRadius: "50%",
16192
+ border: "3px solid rgba(0,0,0,0.1)",
16193
+ borderTopColor: "rgba(0,0,0,0.45)",
16194
+ animation: "pdf-tutor-spin 0.9s linear infinite"
16195
+ }
15057
16196
  }
15058
- ) }) : null,
15059
- showSubtitles ? /* @__PURE__ */ jsx52(SubtitleBar, { text: currentChunk ?? null }) : null
16197
+ ),
16198
+ /* @__PURE__ */ jsx55("span", { children: "Loading document\u2026" }),
16199
+ /* @__PURE__ */ jsx55("style", { children: `
16200
+ @keyframes pdf-tutor-spin {
16201
+ from { transform: rotate(0deg); }
16202
+ to { transform: rotate(360deg); }
16203
+ }
16204
+ ` })
15060
16205
  ]
15061
16206
  }
15062
16207
  );