pdfjs-reader-core 0.4.1 → 0.4.3

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.cjs CHANGED
@@ -1885,6 +1885,17 @@ function computeCameraForBlock(bbox, page, viewport, opts = {}) {
1885
1885
  const y = (pageCY - blockCY) * scale;
1886
1886
  return { scale, x, y };
1887
1887
  }
1888
+ function clampCamera(target, page, viewport) {
1889
+ const pageWScreen = page.width * target.scale;
1890
+ const pageHScreen = page.height * target.scale;
1891
+ const maxOffsetX = Math.max(0, (pageWScreen - viewport.width) / 2);
1892
+ const maxOffsetY = Math.max(0, (pageHScreen - viewport.height) / 2);
1893
+ return {
1894
+ scale: target.scale,
1895
+ x: Math.max(-maxOffsetX, Math.min(maxOffsetX, target.x)),
1896
+ y: Math.max(-maxOffsetY, Math.min(maxOffsetY, target.y))
1897
+ };
1898
+ }
1888
1899
  var init_camera_math = __esm({
1889
1900
  "src/utils/camera-math.ts"() {
1890
1901
  "use strict";
@@ -3748,8 +3759,8 @@ var init_PluginManager = __esm({
3748
3759
  /**
3749
3760
  * Get toolbar items by position
3750
3761
  */
3751
- getToolbarItemsByPosition(position2) {
3752
- return this.getToolbarItems().filter((item) => item.position === position2);
3762
+ getToolbarItemsByPosition(position) {
3763
+ return this.getToolbarItems().filter((item) => item.position === position);
3753
3764
  }
3754
3765
  /**
3755
3766
  * Get all sidebar panels from all plugins
@@ -4862,7 +4873,7 @@ var init_MobileToolbar = __esm({
4862
4873
  sidebarOpen,
4863
4874
  theme,
4864
4875
  onThemeChange,
4865
- position: position2 = "bottom",
4876
+ position = "bottom",
4866
4877
  className
4867
4878
  }) {
4868
4879
  const [showMoreMenu, setShowMoreMenu] = (0, import_react17.useState)(false);
@@ -4896,8 +4907,8 @@ var init_MobileToolbar = __esm({
4896
4907
  "bg-white dark:bg-gray-800",
4897
4908
  "border-gray-200 dark:border-gray-700",
4898
4909
  "px-2 py-1 safe-area-inset",
4899
- position2 === "top" && "top-0 border-b",
4900
- position2 === "bottom" && "bottom-0 border-t",
4910
+ position === "top" && "top-0 border-b",
4911
+ position === "bottom" && "bottom-0 border-t",
4901
4912
  className
4902
4913
  ),
4903
4914
  children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex items-center justify-between gap-1", children: [
@@ -5023,7 +5034,7 @@ var init_MobileToolbar = __esm({
5023
5034
  "bg-white dark:bg-gray-800",
5024
5035
  "rounded-lg shadow-lg",
5025
5036
  "border border-gray-200 dark:border-gray-700",
5026
- position2 === "bottom" ? "bottom-full mb-2" : "top-full mt-2"
5037
+ position === "bottom" ? "bottom-full mb-2" : "top-full mt-2"
5027
5038
  ),
5028
5039
  children: [
5029
5040
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "px-2 py-1 text-xs text-gray-500 dark:text-gray-400 font-medium", children: "Theme" }),
@@ -6849,7 +6860,7 @@ var init_AnnotationToolbar = __esm({
6849
6860
  onShapeTypeChange: onShapeTypeChangeProp,
6850
6861
  onColorChange: onColorChangeProp,
6851
6862
  onStrokeWidthChange: onStrokeWidthChangeProp,
6852
- position: position2 = "top",
6863
+ position = "top",
6853
6864
  className
6854
6865
  }) {
6855
6866
  const storeActiveTool = useAnnotationStore((s) => s.activeAnnotationTool);
@@ -6899,9 +6910,9 @@ var init_AnnotationToolbar = __esm({
6899
6910
  {
6900
6911
  className: cn(
6901
6912
  "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",
6902
- position2 === "floating" && "fixed bottom-20 left-1/2 -translate-x-1/2 z-50",
6903
- position2 === "top" && "sticky top-0 z-40",
6904
- position2 === "bottom" && "sticky bottom-0 z-40",
6913
+ position === "floating" && "fixed bottom-20 left-1/2 -translate-x-1/2 z-50",
6914
+ position === "top" && "sticky top-0 z-40",
6915
+ position === "bottom" && "sticky bottom-0 z-40",
6905
6916
  !isActive && "opacity-90",
6906
6917
  className
6907
6918
  ),
@@ -8550,7 +8561,7 @@ var init_SelectionToolbar = __esm({
8550
8561
  activeColor = "yellow",
8551
8562
  className
8552
8563
  }) {
8553
- const [position2, setPosition] = (0, import_react35.useState)({ top: 0, left: 0, visible: false });
8564
+ const [position, setPosition] = (0, import_react35.useState)({ top: 0, left: 0, visible: false });
8554
8565
  const toolbarRef = (0, import_react35.useRef)(null);
8555
8566
  (0, import_react35.useEffect)(() => {
8556
8567
  if (selection && selection.text && selection.rects.length > 0) {
@@ -8589,7 +8600,7 @@ var init_SelectionToolbar = __esm({
8589
8600
  const handleCopy = (0, import_react35.useCallback)(() => {
8590
8601
  onCopy?.();
8591
8602
  }, [onCopy]);
8592
- if (!position2.visible || !selection?.text) {
8603
+ if (!position.visible || !selection?.text) {
8593
8604
  return null;
8594
8605
  }
8595
8606
  return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
@@ -8607,8 +8618,8 @@ var init_SelectionToolbar = __esm({
8607
8618
  className
8608
8619
  ),
8609
8620
  style: {
8610
- top: position2.top,
8611
- left: position2.left,
8621
+ top: position.top,
8622
+ left: position.left,
8612
8623
  transform: "translateX(-50%)"
8613
8624
  },
8614
8625
  onMouseDown: (e) => {
@@ -8725,7 +8736,7 @@ var init_HighlightPopover = __esm({
8725
8736
  }) {
8726
8737
  const [isEditingComment, setIsEditingComment] = (0, import_react36.useState)(false);
8727
8738
  const [comment, setComment] = (0, import_react36.useState)(highlight?.comment ?? "");
8728
- const [position2, setPosition] = (0, import_react36.useState)({ top: 0, left: 0, visible: false });
8739
+ const [position, setPosition] = (0, import_react36.useState)({ top: 0, left: 0, visible: false });
8729
8740
  const popoverRef = (0, import_react36.useRef)(null);
8730
8741
  const textareaRef = (0, import_react36.useRef)(null);
8731
8742
  (0, import_react36.useEffect)(() => {
@@ -8773,11 +8784,11 @@ var init_HighlightPopover = __esm({
8773
8784
  onClose();
8774
8785
  }
8775
8786
  }
8776
- if (position2.visible) {
8787
+ if (position.visible) {
8777
8788
  document.addEventListener("mousedown", handleClickOutside);
8778
8789
  return () => document.removeEventListener("mousedown", handleClickOutside);
8779
8790
  }
8780
- }, [position2.visible, onClose]);
8791
+ }, [position.visible, onClose]);
8781
8792
  (0, import_react36.useEffect)(() => {
8782
8793
  function handleKeyDown(event) {
8783
8794
  if (event.key === "Escape") {
@@ -8789,11 +8800,11 @@ var init_HighlightPopover = __esm({
8789
8800
  }
8790
8801
  }
8791
8802
  }
8792
- if (position2.visible) {
8803
+ if (position.visible) {
8793
8804
  document.addEventListener("keydown", handleKeyDown);
8794
8805
  return () => document.removeEventListener("keydown", handleKeyDown);
8795
8806
  }
8796
- }, [position2.visible, isEditingComment, highlight?.comment, onClose]);
8807
+ }, [position.visible, isEditingComment, highlight?.comment, onClose]);
8797
8808
  const handleColorClick = (0, import_react36.useCallback)(
8798
8809
  (color) => {
8799
8810
  if (highlight) {
@@ -8820,7 +8831,7 @@ var init_HighlightPopover = __esm({
8820
8831
  onCopy?.(highlight.text);
8821
8832
  }
8822
8833
  }, [highlight, onCopy]);
8823
- if (!highlight || !position2.visible) {
8834
+ if (!highlight || !position.visible) {
8824
8835
  return null;
8825
8836
  }
8826
8837
  return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(
@@ -8837,8 +8848,8 @@ var init_HighlightPopover = __esm({
8837
8848
  className
8838
8849
  ),
8839
8850
  style: {
8840
- top: position2.top,
8841
- left: position2.left,
8851
+ top: position.top,
8852
+ left: position.left,
8842
8853
  transform: "translate(-50%, -100%)",
8843
8854
  width: 280
8844
8855
  },
@@ -10250,7 +10261,7 @@ var init_FloatingZoomControls = __esm({
10250
10261
  init_utils();
10251
10262
  import_jsx_runtime28 = require("react/jsx-runtime");
10252
10263
  FloatingZoomControls = (0, import_react42.memo)(function FloatingZoomControls2({
10253
- position: position2 = "bottom-right",
10264
+ position = "bottom-right",
10254
10265
  className,
10255
10266
  showFitToWidth = true,
10256
10267
  showFitToPage = false,
@@ -10291,7 +10302,7 @@ var init_FloatingZoomControls = __esm({
10291
10302
  "bg-white dark:bg-gray-800 rounded-lg shadow-lg",
10292
10303
  "border border-gray-200 dark:border-gray-700",
10293
10304
  "p-1",
10294
- positionClasses[position2],
10305
+ positionClasses[position],
10295
10306
  className
10296
10307
  ),
10297
10308
  children: [
@@ -12119,7 +12130,7 @@ var import_jsx_runtime34 = require("react/jsx-runtime");
12119
12130
  var QuickNoteButton = (0, import_react48.memo)(function QuickNoteButton2({
12120
12131
  pageNumber,
12121
12132
  scale,
12122
- position: position2 = "top-right",
12133
+ position = "top-right",
12123
12134
  onClick,
12124
12135
  className,
12125
12136
  visible = true
@@ -12128,11 +12139,11 @@ var QuickNoteButton = (0, import_react48.memo)(function QuickNoteButton2({
12128
12139
  const handleClick = (0, import_react48.useCallback)(
12129
12140
  (e) => {
12130
12141
  e.stopPropagation();
12131
- const x = position2 === "top-right" ? 80 : 80;
12132
- const y = position2 === "top-right" ? 20 : 80;
12142
+ const x = position === "top-right" ? 80 : 80;
12143
+ const y = position === "top-right" ? 20 : 80;
12133
12144
  onClick(pageNumber, x / scale, y / scale);
12134
12145
  },
12135
- [pageNumber, onClick, position2, scale]
12146
+ [pageNumber, onClick, position, scale]
12136
12147
  );
12137
12148
  if (!visible) {
12138
12149
  return null;
@@ -12153,8 +12164,8 @@ var QuickNoteButton = (0, import_react48.memo)(function QuickNoteButton2({
12153
12164
  "transition-all duration-200",
12154
12165
  "focus:outline-none focus:ring-2 focus:ring-yellow-500 focus:ring-offset-2",
12155
12166
  isHovered && "scale-110",
12156
- position2 === "top-right" && "top-3 right-3",
12157
- position2 === "bottom-right" && "bottom-3 right-3",
12167
+ position === "top-right" && "top-3 right-3",
12168
+ position === "bottom-right" && "bottom-3 right-3",
12158
12169
  className
12159
12170
  ),
12160
12171
  title: "Add quick note",
@@ -12180,7 +12191,7 @@ init_utils();
12180
12191
  var import_jsx_runtime35 = require("react/jsx-runtime");
12181
12192
  var QuickNotePopover = (0, import_react49.memo)(function QuickNotePopover2({
12182
12193
  visible,
12183
- position: position2,
12194
+ position,
12184
12195
  initialContent = "",
12185
12196
  agentLastStatement,
12186
12197
  onSave,
@@ -12190,7 +12201,7 @@ var QuickNotePopover = (0, import_react49.memo)(function QuickNotePopover2({
12190
12201
  const [content, setContent] = (0, import_react49.useState)(initialContent);
12191
12202
  const textareaRef = (0, import_react49.useRef)(null);
12192
12203
  const popoverRef = (0, import_react49.useRef)(null);
12193
- const [adjustedPosition, setAdjustedPosition] = (0, import_react49.useState)(position2);
12204
+ const [adjustedPosition, setAdjustedPosition] = (0, import_react49.useState)(position);
12194
12205
  (0, import_react49.useEffect)(() => {
12195
12206
  if (visible && textareaRef.current) {
12196
12207
  textareaRef.current.focus();
@@ -12205,7 +12216,7 @@ var QuickNotePopover = (0, import_react49.memo)(function QuickNotePopover2({
12205
12216
  if (!visible || !popoverRef.current) return;
12206
12217
  const rect = popoverRef.current.getBoundingClientRect();
12207
12218
  const padding = 10;
12208
- let { x, y } = position2;
12219
+ let { x, y } = position;
12209
12220
  if (x + rect.width > window.innerWidth - padding) {
12210
12221
  x = window.innerWidth - rect.width - padding;
12211
12222
  }
@@ -12219,7 +12230,7 @@ var QuickNotePopover = (0, import_react49.memo)(function QuickNotePopover2({
12219
12230
  y = padding;
12220
12231
  }
12221
12232
  setAdjustedPosition({ x, y });
12222
- }, [position2, visible]);
12233
+ }, [position, visible]);
12223
12234
  const handleSave = (0, import_react49.useCallback)(() => {
12224
12235
  if (content.trim()) {
12225
12236
  onSave(content.trim());
@@ -12336,11 +12347,11 @@ var import_jsx_runtime36 = require("react/jsx-runtime");
12336
12347
  var AskAboutOverlay = (0, import_react50.memo)(function AskAboutOverlay2({
12337
12348
  visible,
12338
12349
  progress,
12339
- position: position2,
12350
+ position,
12340
12351
  size = 60,
12341
12352
  className
12342
12353
  }) {
12343
- if (!visible || !position2) {
12354
+ if (!visible || !position) {
12344
12355
  return null;
12345
12356
  }
12346
12357
  const strokeWidth = 4;
@@ -12356,8 +12367,8 @@ var AskAboutOverlay = (0, import_react50.memo)(function AskAboutOverlay2({
12356
12367
  className
12357
12368
  ),
12358
12369
  style: {
12359
- left: position2.x - size / 2,
12360
- top: position2.y - size / 2
12370
+ left: position.x - size / 2,
12371
+ top: position.y - size / 2
12361
12372
  },
12362
12373
  children: [
12363
12374
  /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(
@@ -12445,20 +12456,20 @@ var import_react51 = require("react");
12445
12456
  init_utils();
12446
12457
  var import_jsx_runtime37 = require("react/jsx-runtime");
12447
12458
  var AskAboutTrigger = (0, import_react51.memo)(function AskAboutTrigger2({
12448
- position: position2,
12459
+ position,
12449
12460
  onConfirm,
12450
12461
  onCancel,
12451
12462
  visible,
12452
12463
  autoHideDelay = 5e3,
12453
12464
  className
12454
12465
  }) {
12455
- const [adjustedPosition, setAdjustedPosition] = (0, import_react51.useState)(position2);
12466
+ const [adjustedPosition, setAdjustedPosition] = (0, import_react51.useState)(position);
12456
12467
  const triggerRef = (0, import_react51.useRef)(null);
12457
12468
  (0, import_react51.useEffect)(() => {
12458
12469
  if (!visible || !triggerRef.current) return;
12459
12470
  const rect = triggerRef.current.getBoundingClientRect();
12460
12471
  const padding = 10;
12461
- let { x, y } = position2;
12472
+ let { x, y } = position;
12462
12473
  if (x + rect.width / 2 > window.innerWidth - padding) {
12463
12474
  x = window.innerWidth - rect.width / 2 - padding;
12464
12475
  }
@@ -12466,10 +12477,10 @@ var AskAboutTrigger = (0, import_react51.memo)(function AskAboutTrigger2({
12466
12477
  x = rect.width / 2 + padding;
12467
12478
  }
12468
12479
  if (y + rect.height > window.innerHeight - padding) {
12469
- y = position2.y - rect.height - 20;
12480
+ y = position.y - rect.height - 20;
12470
12481
  }
12471
12482
  setAdjustedPosition({ x, y });
12472
- }, [position2, visible]);
12483
+ }, [position, visible]);
12473
12484
  (0, import_react51.useEffect)(() => {
12474
12485
  if (!visible || autoHideDelay === 0) return;
12475
12486
  const timer = setTimeout(onCancel, autoHideDelay);
@@ -13118,7 +13129,7 @@ function withErrorBoundary({ component, ...props }) {
13118
13129
  init_PDFLoadingScreen2();
13119
13130
 
13120
13131
  // src/components/TutorMode/TutorModeContainer.tsx
13121
- var import_react56 = require("react");
13132
+ var import_react58 = require("react");
13122
13133
  var import_zustand2 = require("zustand");
13123
13134
  init_PDFPage2();
13124
13135
  init_hooks();
@@ -13158,26 +13169,48 @@ function CameraView({
13158
13169
  }
13159
13170
 
13160
13171
  // src/components/TutorMode/CinemaLayer.tsx
13161
- var import_framer_motion10 = require("framer-motion");
13172
+ var import_framer_motion8 = require("framer-motion");
13162
13173
 
13163
13174
  // src/components/TutorMode/SpotlightMask.tsx
13164
13175
  var import_react55 = require("react");
13165
13176
  var import_framer_motion2 = require("framer-motion");
13177
+
13178
+ // src/components/TutorMode/tokens.ts
13179
+ var INK = "#2a2420";
13180
+ var PAPER = "#faf6ec";
13181
+ var ACCENT = "#b04a1a";
13182
+ var ACCENT_SOFT = "rgba(176, 74, 26, 0.18)";
13183
+ var ACCENT_GLOW = "rgba(176, 74, 26, 0.35)";
13184
+ var MARKER = "#e6b422";
13185
+ var MARKER_SOFT = "rgba(230, 180, 34, 0.38)";
13186
+ var SERIF = "'Iowan Old Style', 'Palatino Linotype', Palatino, 'Book Antiqua', 'EB Garamond', 'Hoefler Text', Georgia, serif";
13187
+ var EASE_OUT_EXPO = [0.22, 1, 0.36, 1];
13188
+
13189
+ // src/components/TutorMode/SpotlightMask.tsx
13166
13190
  var import_jsx_runtime42 = require("react/jsx-runtime");
13167
13191
  function SpotlightMask({
13168
13192
  page,
13169
13193
  bbox,
13170
13194
  action,
13171
- durationMs = 400
13195
+ durationMs = 500
13172
13196
  }) {
13173
13197
  const maskId = (0, import_react55.useId)();
13174
13198
  const filterId = `${maskId}-blur`;
13175
- const [x1, y1, x2, y2] = bbox;
13176
- const w = Math.max(0, x2 - x1);
13177
- const h = Math.max(0, y2 - y1);
13178
- const rx = action.shape === "rounded" ? 12 : action.shape === "ellipse" ? w / 2 : 0;
13179
- const ry = action.shape === "rounded" ? 12 : action.shape === "ellipse" ? h / 2 : 0;
13180
- const feather = action.feather_px;
13199
+ const [rawX1, rawY1, rawX2, rawY2] = bbox;
13200
+ const rawW = Math.max(0, rawX2 - rawX1);
13201
+ const rawH = Math.max(0, rawY2 - rawY1);
13202
+ const pad = Math.min(28, Math.max(10, Math.min(rawW, rawH) * 0.06));
13203
+ const x1 = rawX1 - pad;
13204
+ const y1 = rawY1 - pad;
13205
+ const x2 = rawX2 + pad;
13206
+ const y2 = rawY2 + pad;
13207
+ const w = x2 - x1;
13208
+ const h = y2 - y1;
13209
+ const rx = action.shape === "rounded" ? 14 : action.shape === "ellipse" ? w / 2 : 0;
13210
+ const ry = action.shape === "rounded" ? 14 : action.shape === "ellipse" ? h / 2 : 0;
13211
+ const feather = Math.max(16, action.feather_px);
13212
+ const cx = (x1 + x2) / 2;
13213
+ const cy = (y1 + y2) / 2;
13181
13214
  return /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(
13182
13215
  "svg",
13183
13216
  {
@@ -13190,7 +13223,8 @@ function SpotlightMask({
13190
13223
  inset: 0,
13191
13224
  pointerEvents: "none",
13192
13225
  width: page.width,
13193
- height: page.height
13226
+ height: page.height,
13227
+ overflow: "visible"
13194
13228
  },
13195
13229
  "data-role": "spotlight-mask",
13196
13230
  children: [
@@ -13201,8 +13235,8 @@ function SpotlightMask({
13201
13235
  action.shape === "ellipse" ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
13202
13236
  "ellipse",
13203
13237
  {
13204
- cx: (x1 + x2) / 2,
13205
- cy: (y1 + y2) / 2,
13238
+ cx,
13239
+ cy,
13206
13240
  rx: w / 2,
13207
13241
  ry: h / 2,
13208
13242
  fill: "black",
@@ -13230,14 +13264,82 @@ function SpotlightMask({
13230
13264
  y: 0,
13231
13265
  width: page.width,
13232
13266
  height: page.height,
13233
- fill: "black",
13267
+ fill: INK,
13234
13268
  mask: `url(#${maskId})`,
13235
13269
  initial: { fillOpacity: 0 },
13236
13270
  animate: { fillOpacity: action.dim_opacity },
13237
13271
  exit: { fillOpacity: 0 },
13238
- transition: { duration: durationMs / 1e3, ease: "easeOut" }
13272
+ transition: { duration: durationMs / 1e3, ease: EASE_OUT_EXPO }
13239
13273
  }
13240
- )
13274
+ ),
13275
+ action.shape === "ellipse" ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
13276
+ import_framer_motion2.motion.ellipse,
13277
+ {
13278
+ cx,
13279
+ cy,
13280
+ rx: w / 2,
13281
+ ry: h / 2,
13282
+ fill: "none",
13283
+ stroke: ACCENT,
13284
+ strokeWidth: 3,
13285
+ initial: { opacity: 0, scale: 1.08 },
13286
+ animate: { opacity: 0.9, scale: 1 },
13287
+ exit: { opacity: 0 },
13288
+ style: { transformOrigin: `${cx}px ${cy}px`, transformBox: "fill-box" },
13289
+ transition: {
13290
+ duration: durationMs / 1e3,
13291
+ delay: 0.15,
13292
+ ease: EASE_OUT_EXPO
13293
+ }
13294
+ }
13295
+ ) : /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
13296
+ import_framer_motion2.motion.rect,
13297
+ {
13298
+ x: x1,
13299
+ y: y1,
13300
+ width: w,
13301
+ height: h,
13302
+ rx,
13303
+ ry,
13304
+ fill: "none",
13305
+ stroke: ACCENT,
13306
+ strokeWidth: 3,
13307
+ initial: { opacity: 0, scale: 1.04 },
13308
+ animate: { opacity: 0.9, scale: 1 },
13309
+ exit: { opacity: 0 },
13310
+ style: {
13311
+ transformOrigin: `${cx}px ${cy}px`,
13312
+ transformBox: "fill-box"
13313
+ },
13314
+ transition: {
13315
+ duration: durationMs / 1e3,
13316
+ delay: 0.15,
13317
+ ease: EASE_OUT_EXPO
13318
+ }
13319
+ }
13320
+ ),
13321
+ action.shape !== "ellipse" ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
13322
+ import_framer_motion2.motion.rect,
13323
+ {
13324
+ x: x1 - 2,
13325
+ y: y1 - 2,
13326
+ width: w + 4,
13327
+ height: h + 4,
13328
+ rx: rx + 2,
13329
+ ry: ry + 2,
13330
+ fill: "none",
13331
+ stroke: ACCENT_GLOW,
13332
+ strokeWidth: 8,
13333
+ initial: { opacity: 0 },
13334
+ animate: { opacity: 0.6 },
13335
+ exit: { opacity: 0 },
13336
+ transition: {
13337
+ duration: durationMs / 1e3,
13338
+ delay: 0.2,
13339
+ ease: EASE_OUT_EXPO
13340
+ }
13341
+ }
13342
+ ) : null
13241
13343
  ]
13242
13344
  }
13243
13345
  );
@@ -13246,35 +13348,58 @@ function SpotlightMask({
13246
13348
  // src/components/TutorMode/AnimatedUnderline.tsx
13247
13349
  var import_framer_motion3 = require("framer-motion");
13248
13350
  var import_jsx_runtime43 = require("react/jsx-runtime");
13351
+ function jitterAt(i) {
13352
+ const h = Math.sin(i * 12.9898) * 43758.5453;
13353
+ return (h - Math.floor(h) - 0.5) * 4;
13354
+ }
13249
13355
  function pathForStyle(x1, x2, y, style) {
13250
- if (style === "straight") return `M ${x1} ${y} L ${x2} ${y}`;
13251
- if (style === "double")
13252
- return `M ${x1} ${y - 3} L ${x2} ${y - 3} M ${x1} ${y + 3} L ${x2} ${y + 3}`;
13356
+ const x1e = x1 - 4;
13357
+ const x2e = x2 + 4;
13358
+ if (style === "straight") {
13359
+ return { primary: `M ${x1e} ${y} L ${x2e} ${y}` };
13360
+ }
13361
+ if (style === "double") {
13362
+ return {
13363
+ primary: `M ${x1e} ${y - 3} L ${x2e} ${y - 3}`,
13364
+ ghost: `M ${x1e} ${y + 3} L ${x2e} ${y + 3}`
13365
+ };
13366
+ }
13253
13367
  if (style === "wavy") {
13254
- const steps = Math.max(8, Math.floor((x2 - x1) / 18));
13255
- let d2 = `M ${x1} ${y}`;
13368
+ const len = x2e - x1e;
13369
+ const steps = Math.max(12, Math.floor(len / 10));
13370
+ const amp = 3.2;
13371
+ let d = `M ${x1e} ${y}`;
13256
13372
  for (let i = 1; i <= steps; i++) {
13257
- const px = x1 + (x2 - x1) * i / steps;
13258
- const dy = i % 2 === 0 ? 4 : -4;
13259
- d2 += ` Q ${px - (x2 - x1) / (2 * steps)} ${y + dy} ${px} ${y}`;
13373
+ const t = i / steps;
13374
+ const px = x1e + len * t;
13375
+ const py = y + Math.sin(t * Math.PI * 4) * amp;
13376
+ const prevT = (i - 1) / steps;
13377
+ const cpx = x1e + len * (prevT + (t - prevT) / 2);
13378
+ const cpy = y + Math.sin((prevT + (t - prevT) / 2) * Math.PI * 4) * amp;
13379
+ d += ` Q ${cpx} ${cpy} ${px} ${py}`;
13260
13380
  }
13261
- return d2;
13381
+ return { primary: d };
13262
13382
  }
13263
- const segs = 6;
13264
- let d = `M ${x1} ${y}`;
13383
+ const segs = 8;
13384
+ let primary = `M ${x1e} ${y + jitterAt(0)}`;
13385
+ let ghost = `M ${x1e} ${y + jitterAt(100) + 1.5}`;
13265
13386
  for (let i = 1; i <= segs; i++) {
13266
- const px = x1 + (x2 - x1) * i / segs;
13267
- const jitter = (Math.random() - 0.5) * 4;
13268
- d += ` L ${px} ${y + jitter}`;
13387
+ const px = x1e + (x2e - x1e) * i / segs;
13388
+ primary += ` L ${px} ${y + jitterAt(i)}`;
13389
+ ghost += ` L ${px} ${y + jitterAt(i + 100) + 1.5}`;
13269
13390
  }
13270
- return d;
13391
+ return { primary, ghost };
13271
13392
  }
13272
13393
  function AnimatedUnderline({ bbox, action }) {
13273
13394
  const [x1, , x2, y2] = bbox;
13274
13395
  const y = y2 + 6;
13275
- const d = pathForStyle(x1, x2, y, action.style);
13396
+ const { primary, ghost } = pathForStyle(x1, x2, y, action.style);
13276
13397
  const duration = action.draw_duration_ms / 1e3;
13277
- return /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
13398
+ const stroke = action.color && action.color !== "#FBBF24" ? action.color : ACCENT;
13399
+ const blotX = x2 + 4;
13400
+ const blotY = y;
13401
+ const strokeWeight = action.style === "wavy" ? 3 : 4;
13402
+ return /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)(
13278
13403
  "svg",
13279
13404
  {
13280
13405
  style: {
@@ -13284,50 +13409,148 @@ function AnimatedUnderline({ bbox, action }) {
13284
13409
  overflow: "visible"
13285
13410
  },
13286
13411
  "data-role": "underline",
13287
- children: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
13288
- import_framer_motion3.motion.path,
13289
- {
13290
- d,
13291
- fill: "none",
13292
- stroke: action.color,
13293
- strokeWidth: 4,
13294
- strokeLinecap: "round",
13295
- initial: { pathLength: 0, opacity: 0 },
13296
- animate: { pathLength: 1, opacity: 1 },
13297
- exit: { opacity: 0 },
13298
- transition: { duration, ease: "easeOut" }
13299
- }
13300
- )
13412
+ children: [
13413
+ ghost ? /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
13414
+ import_framer_motion3.motion.path,
13415
+ {
13416
+ d: ghost,
13417
+ fill: "none",
13418
+ stroke,
13419
+ strokeWidth: strokeWeight - 1.5,
13420
+ strokeLinecap: "round",
13421
+ strokeOpacity: 0.35,
13422
+ initial: { pathLength: 0, opacity: 0 },
13423
+ animate: { pathLength: 1, opacity: 0.55 },
13424
+ exit: { opacity: 0 },
13425
+ transition: { duration, ease: EASE_OUT_EXPO }
13426
+ }
13427
+ ) : null,
13428
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
13429
+ import_framer_motion3.motion.path,
13430
+ {
13431
+ d: primary,
13432
+ fill: "none",
13433
+ stroke,
13434
+ strokeWidth: strokeWeight,
13435
+ strokeLinecap: "round",
13436
+ strokeLinejoin: "round",
13437
+ initial: { pathLength: 0, opacity: 0 },
13438
+ animate: { pathLength: 1, opacity: 1 },
13439
+ exit: { opacity: 0 },
13440
+ transition: { duration, ease: EASE_OUT_EXPO }
13441
+ }
13442
+ ),
13443
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
13444
+ import_framer_motion3.motion.circle,
13445
+ {
13446
+ cx: blotX,
13447
+ cy: blotY,
13448
+ r: strokeWeight / 2 + 0.5,
13449
+ fill: stroke,
13450
+ initial: { scale: 0, opacity: 0 },
13451
+ animate: { scale: 1, opacity: 0.9 },
13452
+ exit: { opacity: 0 },
13453
+ style: {
13454
+ transformOrigin: `${blotX}px ${blotY}px`,
13455
+ transformBox: "fill-box"
13456
+ },
13457
+ transition: {
13458
+ duration: 0.25,
13459
+ delay: duration - 0.1,
13460
+ ease: EASE_OUT_EXPO
13461
+ }
13462
+ }
13463
+ )
13464
+ ]
13301
13465
  }
13302
13466
  );
13303
13467
  }
13304
13468
 
13305
13469
  // src/components/TutorMode/AnimatedHighlight.tsx
13470
+ var import_react56 = require("react");
13306
13471
  var import_framer_motion4 = require("framer-motion");
13307
13472
  var import_jsx_runtime44 = require("react/jsx-runtime");
13308
13473
  function AnimatedHighlight({ bbox, action }) {
13309
13474
  const [x1, y1, x2, y2] = bbox;
13310
- const w = x2 - x1;
13311
- const h = y2 - y1;
13312
- return /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
13313
- import_framer_motion4.motion.div,
13475
+ const h = Math.max(1, y2 - y1);
13476
+ const bleed = Math.min(4, h * 0.12);
13477
+ const yTop = y1 - bleed;
13478
+ const yBot = y2 + bleed;
13479
+ const height = yBot - yTop;
13480
+ const duration = action.draw_duration_ms / 1e3;
13481
+ const filterId = (0, import_react56.useId)();
13482
+ const fill = action.color && action.color !== "rgba(250, 204, 21, 0.35)" && action.color !== "rgba(250,204,21,0.35)" ? action.color : MARKER_SOFT;
13483
+ const inner = action.color && action.color !== "rgba(250, 204, 21, 0.35)" && action.color !== "rgba(250,204,21,0.35)" ? action.color : MARKER;
13484
+ const taper = Math.min(6, h * 0.2);
13485
+ const pathD = `
13486
+ M ${x1 - 2} ${yTop + taper}
13487
+ L ${x1 + 2} ${yTop}
13488
+ L ${x2 - 2} ${yTop}
13489
+ L ${x2 + 2} ${yTop + taper}
13490
+ L ${x2 + 2} ${yBot - taper}
13491
+ L ${x2 - 2} ${yBot}
13492
+ L ${x1 + 2} ${yBot}
13493
+ L ${x1 - 2} ${yBot - taper}
13494
+ Z
13495
+ `;
13496
+ return /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)(
13497
+ "svg",
13314
13498
  {
13315
13499
  style: {
13316
13500
  position: "absolute",
13317
- left: x1,
13318
- top: y1,
13319
- height: h,
13320
- background: action.color,
13321
- borderRadius: 4,
13322
- mixBlendMode: "multiply",
13323
- transformOrigin: "0% 50%",
13324
- pointerEvents: "none"
13501
+ inset: 0,
13502
+ pointerEvents: "none",
13503
+ overflow: "visible",
13504
+ mixBlendMode: "multiply"
13325
13505
  },
13326
- initial: { width: 0, opacity: 0.9 },
13327
- animate: { width: w, opacity: 0.9 },
13328
- exit: { opacity: 0 },
13329
- transition: { duration: action.draw_duration_ms / 1e3, ease: "easeOut" },
13330
- "data-role": "highlight"
13506
+ "data-role": "highlight",
13507
+ children: [
13508
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("defs", { children: /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)("filter", { id: filterId, children: [
13509
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
13510
+ "feTurbulence",
13511
+ {
13512
+ type: "fractalNoise",
13513
+ baseFrequency: "1.6",
13514
+ numOctaves: "1",
13515
+ seed: 3,
13516
+ result: "noise"
13517
+ }
13518
+ ),
13519
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("feDisplacementMap", { in: "SourceGraphic", in2: "noise", scale: 1.4 })
13520
+ ] }) }),
13521
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)(
13522
+ import_framer_motion4.motion.g,
13523
+ {
13524
+ initial: { clipPath: `inset(0 100% 0 0)` },
13525
+ animate: { clipPath: `inset(0 0% 0 0)` },
13526
+ exit: { opacity: 0 },
13527
+ transition: { duration, ease: EASE_OUT_EXPO },
13528
+ children: [
13529
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
13530
+ "path",
13531
+ {
13532
+ d: pathD,
13533
+ fill,
13534
+ opacity: 0.85,
13535
+ filter: `url(#${filterId})`
13536
+ }
13537
+ ),
13538
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
13539
+ "rect",
13540
+ {
13541
+ x: x1 - 1,
13542
+ y: y1 - bleed * 0.4,
13543
+ width: x2 - x1 + 2,
13544
+ height: height - bleed * 0.8,
13545
+ fill: inner,
13546
+ opacity: 0.5,
13547
+ filter: `url(#${filterId})`
13548
+ }
13549
+ )
13550
+ ]
13551
+ }
13552
+ )
13553
+ ]
13331
13554
  }
13332
13555
  );
13333
13556
  }
@@ -13336,66 +13559,214 @@ function AnimatedHighlight({ bbox, action }) {
13336
13559
  var import_framer_motion5 = require("framer-motion");
13337
13560
  var import_jsx_runtime45 = require("react/jsx-runtime");
13338
13561
  var INTENSITY = {
13339
- subtle: { scale: 1.02, border: "2px solid rgba(59,130,246,0.6)" },
13340
- normal: { scale: 1.05, border: "3px solid rgba(59,130,246,0.8)" },
13341
- strong: { scale: 1.1, border: "4px solid rgba(59,130,246,1.0)" }
13562
+ subtle: { bracketLen: 14, strokeWeight: 2, coreOpacity: 0.5, ringScale: 1.08 },
13563
+ normal: { bracketLen: 20, strokeWeight: 2.5, coreOpacity: 0.75, ringScale: 1.14 },
13564
+ strong: { bracketLen: 26, strokeWeight: 3, coreOpacity: 1, ringScale: 1.22 }
13342
13565
  };
13343
13566
  function PulseOverlay({ bbox, action }) {
13344
13567
  const [x1, y1, x2, y2] = bbox;
13345
- const { scale, border } = INTENSITY[action.intensity];
13346
- const repeat = action.count === 1 ? 0 : action.count - 1;
13347
- return /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
13348
- import_framer_motion5.motion.div,
13568
+ const w = Math.max(1, x2 - x1);
13569
+ const h = Math.max(1, y2 - y1);
13570
+ const cx = (x1 + x2) / 2;
13571
+ const cy = (y1 + y2) / 2;
13572
+ const spec = INTENSITY[action.intensity] ?? INTENSITY.normal;
13573
+ const L = Math.min(spec.bracketLen, Math.min(w, h) / 2.5);
13574
+ const PAD = 6;
13575
+ return /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(
13576
+ "svg",
13349
13577
  {
13350
13578
  style: {
13351
13579
  position: "absolute",
13352
- left: x1,
13353
- top: y1,
13354
- width: x2 - x1,
13355
- height: y2 - y1,
13356
- border,
13357
- borderRadius: 8,
13580
+ inset: 0,
13358
13581
  pointerEvents: "none",
13359
- boxSizing: "border-box"
13360
- },
13361
- animate: { scale: [1, scale, 1] },
13362
- transition: {
13363
- duration: 1.2,
13364
- times: [0, 0.5, 1],
13365
- ease: "easeInOut",
13366
- repeat,
13367
- repeatType: "loop"
13582
+ overflow: "visible"
13368
13583
  },
13584
+ "data-role": "pulse",
13585
+ children: [
13586
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
13587
+ import_framer_motion5.motion.ellipse,
13588
+ {
13589
+ cx,
13590
+ cy,
13591
+ rx: w / 2 + 10,
13592
+ ry: h / 2 + 10,
13593
+ fill: ACCENT_GLOW,
13594
+ style: {
13595
+ transformOrigin: `${cx}px ${cy}px`,
13596
+ transformBox: "fill-box",
13597
+ filter: "blur(16px)"
13598
+ },
13599
+ initial: { opacity: 0, scale: 0.95 },
13600
+ animate: {
13601
+ opacity: [0, spec.coreOpacity * 0.45, 0.15],
13602
+ scale: [0.95, spec.ringScale, 1]
13603
+ },
13604
+ exit: { opacity: 0 },
13605
+ transition: {
13606
+ duration: 1.3,
13607
+ times: [0, 0.5, 1],
13608
+ ease: EASE_OUT_EXPO
13609
+ }
13610
+ }
13611
+ ),
13612
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
13613
+ Bracket,
13614
+ {
13615
+ originX: x1 - PAD,
13616
+ originY: y1 - PAD,
13617
+ direction: "tl",
13618
+ length: L,
13619
+ weight: spec.strokeWeight,
13620
+ delay: 0
13621
+ }
13622
+ ),
13623
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
13624
+ Bracket,
13625
+ {
13626
+ originX: x2 + PAD,
13627
+ originY: y1 - PAD,
13628
+ direction: "tr",
13629
+ length: L,
13630
+ weight: spec.strokeWeight,
13631
+ delay: 0.04
13632
+ }
13633
+ ),
13634
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
13635
+ Bracket,
13636
+ {
13637
+ originX: x1 - PAD,
13638
+ originY: y2 + PAD,
13639
+ direction: "bl",
13640
+ length: L,
13641
+ weight: spec.strokeWeight,
13642
+ delay: 0.08
13643
+ }
13644
+ ),
13645
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
13646
+ Bracket,
13647
+ {
13648
+ originX: x2 + PAD,
13649
+ originY: y2 + PAD,
13650
+ direction: "br",
13651
+ length: L,
13652
+ weight: spec.strokeWeight,
13653
+ delay: 0.12
13654
+ }
13655
+ ),
13656
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
13657
+ import_framer_motion5.motion.rect,
13658
+ {
13659
+ x: x1 - PAD,
13660
+ y: y1 - PAD,
13661
+ width: w + PAD * 2,
13662
+ height: h + PAD * 2,
13663
+ fill: "none",
13664
+ stroke: ACCENT,
13665
+ strokeWidth: 1,
13666
+ rx: 4,
13667
+ style: {
13668
+ transformOrigin: `${cx}px ${cy}px`,
13669
+ transformBox: "fill-box"
13670
+ },
13671
+ initial: { scale: 1, opacity: 0 },
13672
+ animate: {
13673
+ scale: [1, spec.ringScale, 1],
13674
+ opacity: [0, spec.coreOpacity * 0.5, 0]
13675
+ },
13676
+ exit: { opacity: 0 },
13677
+ transition: {
13678
+ duration: 1.3,
13679
+ times: [0, 0.5, 1],
13680
+ ease: EASE_OUT_EXPO,
13681
+ delay: 0.2
13682
+ }
13683
+ }
13684
+ )
13685
+ ]
13686
+ }
13687
+ );
13688
+ }
13689
+ function Bracket({
13690
+ originX,
13691
+ originY,
13692
+ direction,
13693
+ length,
13694
+ weight,
13695
+ delay
13696
+ }) {
13697
+ const xSign = direction === "tl" || direction === "bl" ? 1 : -1;
13698
+ const ySign = direction === "tl" || direction === "tr" ? 1 : -1;
13699
+ const d = `
13700
+ M ${originX + xSign * length} ${originY}
13701
+ L ${originX} ${originY}
13702
+ L ${originX} ${originY + ySign * length}
13703
+ `;
13704
+ const slideX = -xSign * 8;
13705
+ const slideY = -ySign * 8;
13706
+ return /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
13707
+ import_framer_motion5.motion.path,
13708
+ {
13709
+ d,
13710
+ fill: "none",
13711
+ stroke: ACCENT,
13712
+ strokeWidth: weight,
13713
+ strokeLinecap: "round",
13714
+ strokeLinejoin: "round",
13715
+ initial: { opacity: 0, x: slideX, y: slideY },
13716
+ animate: { opacity: 1, x: 0, y: 0 },
13369
13717
  exit: { opacity: 0 },
13370
- "data-role": "pulse"
13718
+ transition: {
13719
+ duration: 0.4,
13720
+ delay,
13721
+ ease: EASE_OUT_EXPO
13722
+ }
13371
13723
  }
13372
13724
  );
13373
13725
  }
13374
13726
 
13375
13727
  // src/components/TutorMode/CalloutArrow.tsx
13728
+ var import_react57 = require("react");
13376
13729
  var import_framer_motion6 = require("framer-motion");
13377
13730
  var import_jsx_runtime46 = require("react/jsx-runtime");
13378
13731
  function centerOf(b) {
13379
13732
  return { x: (b[0] + b[2]) / 2, y: (b[1] + b[3]) / 2 };
13380
13733
  }
13381
- function arrowPath(fromBbox, toBbox, curve) {
13734
+ function edgePoints(fromBbox, toBbox) {
13382
13735
  const a = centerOf(fromBbox);
13383
13736
  const b = centerOf(toBbox);
13384
- if (curve === "straight") return `M ${a.x} ${a.y} L ${b.x} ${b.y}`;
13385
- if (curve === "zigzag") {
13386
- const mx = (a.x + b.x) / 2;
13387
- return `M ${a.x} ${a.y} L ${mx} ${a.y} L ${mx} ${b.y} L ${b.x} ${b.y}`;
13388
- }
13389
13737
  const dx = b.x - a.x;
13390
13738
  const dy = b.y - a.y;
13391
- const cx = (a.x + b.x) / 2 - dy * 0.25;
13392
- const cy = (a.y + b.y) / 2 + dx * 0.25;
13393
- return `M ${a.x} ${a.y} Q ${cx} ${cy} ${b.x} ${b.y}`;
13739
+ const len = Math.hypot(dx, dy) || 1;
13740
+ const ux = dx / len;
13741
+ const uy = dy / len;
13742
+ const aHalfW = (fromBbox[2] - fromBbox[0]) / 2;
13743
+ const aHalfH = (fromBbox[3] - fromBbox[1]) / 2;
13744
+ const bHalfW = (toBbox[2] - toBbox[0]) / 2;
13745
+ const bHalfH = (toBbox[3] - toBbox[1]) / 2;
13746
+ const aOff = Math.min(Math.max(aHalfW, aHalfH), 60);
13747
+ const bOff = Math.min(Math.max(bHalfW, bHalfH), 60);
13748
+ return {
13749
+ from: { x: a.x + ux * aOff, y: a.y + uy * aOff },
13750
+ to: { x: b.x - ux * bOff, y: b.y - uy * bOff }
13751
+ };
13752
+ }
13753
+ function arrowPath(from, to, curve) {
13754
+ if (curve === "straight") return `M ${from.x} ${from.y} L ${to.x} ${to.y}`;
13755
+ if (curve === "zigzag") {
13756
+ const mx = (from.x + to.x) / 2;
13757
+ return `M ${from.x} ${from.y} L ${mx} ${from.y} L ${mx} ${to.y} L ${to.x} ${to.y}`;
13758
+ }
13759
+ const dx = to.x - from.x;
13760
+ const dy = to.y - from.y;
13761
+ const cx = (from.x + to.x) / 2 - dy * 0.22;
13762
+ const cy = (from.y + to.y) / 2 + dx * 0.22;
13763
+ return `M ${from.x} ${from.y} Q ${cx} ${cy} ${to.x} ${to.y}`;
13394
13764
  }
13395
13765
  function CalloutArrow({ fromBbox, toBbox, action }) {
13396
- const d = arrowPath(fromBbox, toBbox, action.curve);
13397
- const label = action.label;
13398
- const target = centerOf(toBbox);
13766
+ const markerId = (0, import_react57.useId)();
13767
+ const glowId = `${markerId}-glow`;
13768
+ const { from, to } = edgePoints(fromBbox, toBbox);
13769
+ const d = arrowPath(from, to, action.curve);
13399
13770
  return /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)(
13400
13771
  "svg",
13401
13772
  {
@@ -13407,247 +13778,142 @@ function CalloutArrow({ fromBbox, toBbox, action }) {
13407
13778
  },
13408
13779
  "data-role": "callout",
13409
13780
  children: [
13410
- /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("defs", { children: /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
13411
- "marker",
13781
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("defs", { children: [
13782
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
13783
+ "marker",
13784
+ {
13785
+ id: markerId,
13786
+ viewBox: "0 0 12 10",
13787
+ refX: 10,
13788
+ refY: 5,
13789
+ markerWidth: 10,
13790
+ markerHeight: 10,
13791
+ orient: "auto",
13792
+ children: /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
13793
+ "path",
13794
+ {
13795
+ d: "M 1 1 L 10 5 L 1 9",
13796
+ fill: "none",
13797
+ stroke: ACCENT,
13798
+ strokeWidth: 1.8,
13799
+ strokeLinecap: "round",
13800
+ strokeLinejoin: "round"
13801
+ }
13802
+ )
13803
+ }
13804
+ ),
13805
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("filter", { id: glowId, x: "-20%", y: "-20%", width: "140%", height: "140%", children: /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("feGaussianBlur", { stdDeviation: "2.5" }) })
13806
+ ] }),
13807
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
13808
+ import_framer_motion6.motion.circle,
13809
+ {
13810
+ cx: from.x,
13811
+ cy: from.y,
13812
+ r: 4,
13813
+ fill: ACCENT,
13814
+ initial: { scale: 0, opacity: 0 },
13815
+ animate: { scale: 1, opacity: 1 },
13816
+ exit: { opacity: 0 },
13817
+ style: {
13818
+ transformOrigin: `${from.x}px ${from.y}px`,
13819
+ transformBox: "fill-box"
13820
+ },
13821
+ transition: { duration: 0.3, ease: EASE_OUT_EXPO }
13822
+ }
13823
+ ),
13824
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
13825
+ import_framer_motion6.motion.circle,
13412
13826
  {
13413
- id: "arrowhead",
13414
- viewBox: "0 0 10 10",
13415
- refX: "8",
13416
- refY: "5",
13417
- markerWidth: "8",
13418
- markerHeight: "8",
13419
- orient: "auto",
13420
- children: /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("path", { d: "M 0 0 L 10 5 L 0 10 z", fill: "#3B82F6" })
13827
+ cx: from.x,
13828
+ cy: from.y,
13829
+ r: 7,
13830
+ fill: "none",
13831
+ stroke: ACCENT,
13832
+ strokeWidth: 1.2,
13833
+ strokeOpacity: 0.4,
13834
+ initial: { scale: 0, opacity: 0 },
13835
+ animate: { scale: 1, opacity: 0.5 },
13836
+ exit: { opacity: 0 },
13837
+ style: {
13838
+ transformOrigin: `${from.x}px ${from.y}px`,
13839
+ transformBox: "fill-box"
13840
+ },
13841
+ transition: { duration: 0.4, delay: 0.08, ease: EASE_OUT_EXPO }
13421
13842
  }
13422
- ) }),
13843
+ ),
13423
13844
  /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
13424
13845
  import_framer_motion6.motion.path,
13425
13846
  {
13426
13847
  d,
13427
13848
  fill: "none",
13428
- stroke: "#3B82F6",
13429
- strokeWidth: 3,
13849
+ stroke: ACCENT,
13850
+ strokeWidth: 6,
13851
+ strokeOpacity: 0.18,
13430
13852
  strokeLinecap: "round",
13431
- markerEnd: "url(#arrowhead)",
13853
+ filter: `url(#${glowId})`,
13432
13854
  initial: { pathLength: 0, opacity: 0 },
13433
- animate: { pathLength: 1, opacity: 1 },
13855
+ animate: { pathLength: 1, opacity: 0.8 },
13434
13856
  exit: { opacity: 0 },
13435
- transition: { duration: 0.6, ease: "easeOut" }
13857
+ transition: { duration: 0.7, delay: 0.12, ease: EASE_OUT_EXPO }
13436
13858
  }
13437
13859
  ),
13438
- label ? /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)(
13439
- import_framer_motion6.motion.g,
13860
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
13861
+ import_framer_motion6.motion.path,
13440
13862
  {
13441
- initial: { opacity: 0 },
13442
- animate: { opacity: 1 },
13863
+ d,
13864
+ fill: "none",
13865
+ stroke: ACCENT,
13866
+ strokeWidth: 2.4,
13867
+ strokeLinecap: "round",
13868
+ strokeLinejoin: "round",
13869
+ markerEnd: `url(#${markerId})`,
13870
+ initial: { pathLength: 0, opacity: 0 },
13871
+ animate: { pathLength: 1, opacity: 1 },
13443
13872
  exit: { opacity: 0 },
13444
- transition: { delay: 0.3, duration: 0.3 },
13445
- children: [
13446
- /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
13447
- "rect",
13448
- {
13449
- x: target.x - 4,
13450
- y: target.y - 28,
13451
- width: label.length * 9 + 12,
13452
- height: 22,
13453
- rx: 4,
13454
- fill: "#1F2937"
13455
- }
13456
- ),
13457
- /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
13458
- "text",
13459
- {
13460
- x: target.x + 2,
13461
- y: target.y - 12,
13462
- fill: "white",
13463
- fontSize: 14,
13464
- fontFamily: "system-ui, sans-serif",
13465
- children: label
13466
- }
13467
- )
13468
- ]
13873
+ transition: { duration: 0.7, delay: 0.15, ease: EASE_OUT_EXPO }
13469
13874
  }
13470
- ) : null
13875
+ )
13471
13876
  ]
13472
13877
  }
13473
13878
  );
13474
13879
  }
13475
13880
 
13476
- // src/components/TutorMode/GhostReference.tsx
13881
+ // src/components/TutorMode/BoxOverlay.tsx
13477
13882
  var import_framer_motion7 = require("framer-motion");
13478
13883
  var import_jsx_runtime47 = require("react/jsx-runtime");
13479
- var POSITIONS = {
13480
- "top-right": { top: 40, right: 40 },
13481
- "top-left": { top: 40, left: 40 },
13482
- "bottom-right": { bottom: 40, right: 40 },
13483
- "bottom-left": { bottom: 40, left: 40 }
13484
- };
13485
- function GhostReference({
13486
- page,
13487
- sourceBbox,
13488
- sourceBlockText,
13489
- sourcePageNumber,
13490
- action
13491
- }) {
13492
- const width = 360;
13493
- const [x1, y1, x2, y2] = sourceBbox;
13494
- return /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)(
13884
+ function BoxOverlay({ bbox, action }) {
13885
+ const [x1, y1, x2, y2] = bbox;
13886
+ const useSystem = !action.color || action.color === "#3B82F6" || action.color === "#3b82f6";
13887
+ const borderColor = useSystem ? ACCENT : action.color;
13888
+ const washColor = useSystem ? ACCENT_SOFT : void 0;
13889
+ return /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(
13495
13890
  import_framer_motion7.motion.div,
13496
13891
  {
13497
- initial: { opacity: 0, y: 20, scale: 0.95 },
13498
- animate: { opacity: 1, y: 0, scale: 1 },
13499
- exit: { opacity: 0, y: 20, scale: 0.95 },
13500
- transition: { duration: 0.4, ease: "easeOut" },
13892
+ initial: { opacity: 0, scale: 0.98 },
13893
+ animate: { opacity: 1, scale: 1 },
13894
+ exit: { opacity: 0 },
13895
+ transition: { duration: 0.45, ease: EASE_OUT_EXPO },
13501
13896
  style: {
13502
13897
  position: "absolute",
13503
- width,
13504
- background: "#111",
13505
- color: "white",
13506
- borderRadius: 12,
13507
- padding: 12,
13508
- boxShadow: "0 10px 40px rgba(0,0,0,0.5)",
13898
+ left: x1 - 3,
13899
+ top: y1 - 3,
13900
+ width: x2 - x1 + 6,
13901
+ height: y2 - y1 + 6,
13902
+ border: `${action.style === "dashed" ? "2px dashed" : "2px solid"} ${borderColor}`,
13903
+ borderRadius: 4,
13904
+ background: washColor,
13509
13905
  pointerEvents: "none",
13510
- fontFamily: "system-ui, sans-serif",
13511
- fontSize: 13,
13512
- ...POSITIONS[action.position]
13906
+ boxSizing: "border-box",
13907
+ // Subtle outer glow for the accent version, tinted to match.
13908
+ boxShadow: useSystem ? `0 0 0 1px rgba(176, 74, 26, 0.10), 0 8px 24px -10px rgba(176, 74, 26, 0.35)` : void 0
13513
13909
  },
13514
- "data-role": "ghost-reference",
13515
- children: [
13516
- /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)("div", { style: { opacity: 0.7, fontSize: 11, marginBottom: 6 }, children: [
13517
- "Page ",
13518
- sourcePageNumber,
13519
- " \u2014 ",
13520
- action.target_block
13521
- ] }),
13522
- /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)(
13523
- "svg",
13524
- {
13525
- width: width - 24,
13526
- height: 160,
13527
- viewBox: `0 0 ${page.width} ${page.height}`,
13528
- style: { background: "#1F2937", borderRadius: 6, display: "block" },
13529
- preserveAspectRatio: "xMidYMid meet",
13530
- children: [
13531
- /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(
13532
- "rect",
13533
- {
13534
- x: 0,
13535
- y: 0,
13536
- width: page.width,
13537
- height: page.height,
13538
- fill: "#1F2937"
13539
- }
13540
- ),
13541
- /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(
13542
- "rect",
13543
- {
13544
- x: x1,
13545
- y: y1,
13546
- width: x2 - x1,
13547
- height: y2 - y1,
13548
- fill: "rgba(250,204,21,0.45)",
13549
- stroke: "#FBBF24",
13550
- strokeWidth: 8
13551
- }
13552
- )
13553
- ]
13554
- }
13555
- ),
13556
- /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(
13557
- "div",
13558
- {
13559
- style: {
13560
- marginTop: 8,
13561
- fontSize: 12,
13562
- lineHeight: 1.4,
13563
- opacity: 0.9
13564
- },
13565
- children: sourceBlockText ?? "(figure)"
13566
- }
13567
- )
13568
- ]
13569
- }
13570
- );
13571
- }
13572
-
13573
- // src/components/TutorMode/BoxOverlay.tsx
13574
- var import_framer_motion8 = require("framer-motion");
13575
- var import_jsx_runtime48 = require("react/jsx-runtime");
13576
- function BoxOverlay({ bbox, action }) {
13577
- const [x1, y1, x2, y2] = bbox;
13578
- return /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
13579
- import_framer_motion8.motion.div,
13580
- {
13581
- initial: { opacity: 0, scale: 0.97 },
13582
- animate: { opacity: 1, scale: 1 },
13583
- exit: { opacity: 0 },
13584
- transition: { duration: 0.35, ease: "easeOut" },
13585
- style: {
13586
- position: "absolute",
13587
- left: x1,
13588
- top: y1,
13589
- width: x2 - x1,
13590
- height: y2 - y1,
13591
- border: `${action.style === "dashed" ? "3px dashed" : "3px solid"} ${action.color}`,
13592
- borderRadius: 6,
13593
- pointerEvents: "none",
13594
- boxSizing: "border-box"
13595
- },
13596
- "data-role": "box"
13597
- }
13598
- );
13599
- }
13600
-
13601
- // src/components/TutorMode/StickyLabel.tsx
13602
- var import_framer_motion9 = require("framer-motion");
13603
- var import_jsx_runtime49 = require("react/jsx-runtime");
13604
- function position(bbox, where) {
13605
- const [x1, y1, x2, y2] = bbox;
13606
- const cx = (x1 + x2) / 2;
13607
- const cy = (y1 + y2) / 2;
13608
- const PAD = 16;
13609
- switch (where) {
13610
- case "top":
13611
- return { left: cx, top: y1 - PAD, transform: "translate(-50%, -100%)" };
13612
- case "bottom":
13613
- return { left: cx, top: y2 + PAD, transform: "translate(-50%, 0)" };
13614
- case "left":
13615
- return { left: x1 - PAD, top: cy, transform: "translate(-100%, -50%)" };
13616
- case "right":
13617
- return { left: x2 + PAD, top: cy, transform: "translate(0, -50%)" };
13618
- default:
13619
- return { left: cx, top: y1, transform: "translate(-50%, -100%)" };
13620
- }
13621
- }
13622
- function StickyLabel({ bbox, action }) {
13623
- return /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
13624
- import_framer_motion9.motion.div,
13625
- {
13626
- initial: { opacity: 0, scale: 0.9 },
13627
- animate: { opacity: 1, scale: 1 },
13628
- exit: { opacity: 0 },
13629
- transition: { duration: 0.35, ease: "easeOut" },
13630
- style: {
13631
- position: "absolute",
13632
- padding: "6px 10px",
13633
- background: "#FEF3C7",
13634
- color: "#78350F",
13635
- borderRadius: 6,
13636
- boxShadow: "0 3px 10px rgba(0,0,0,0.2)",
13637
- fontSize: 14,
13638
- fontFamily: "system-ui, sans-serif",
13639
- maxWidth: 280,
13640
- pointerEvents: "none",
13641
- ...position(bbox, action.position)
13642
- },
13643
- "data-role": "label",
13644
- children: action.text
13910
+ "data-role": "box"
13645
13911
  }
13646
13912
  );
13647
13913
  }
13648
13914
 
13649
13915
  // src/components/TutorMode/CinemaLayer.tsx
13650
- var import_jsx_runtime50 = require("react/jsx-runtime");
13916
+ var import_jsx_runtime48 = require("react/jsx-runtime");
13651
13917
  function blockBbox(index, block_id) {
13652
13918
  return index.blockById.get(block_id)?.block.bbox;
13653
13919
  }
@@ -13657,7 +13923,7 @@ function CinemaLayer({
13657
13923
  overlays,
13658
13924
  scale
13659
13925
  }) {
13660
- return /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
13926
+ return /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
13661
13927
  "div",
13662
13928
  {
13663
13929
  "data-role": "cinema-layer",
@@ -13677,13 +13943,13 @@ function CinemaLayer({
13677
13943
  // reachable because it sits OUTSIDE this stacking context.
13678
13944
  zIndex: 100
13679
13945
  },
13680
- children: /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(import_framer_motion10.AnimatePresence, { children: overlays.map((overlay) => {
13946
+ children: /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_framer_motion8.AnimatePresence, { children: overlays.map((overlay) => {
13681
13947
  switch (overlay.kind) {
13682
13948
  case "spotlight": {
13683
13949
  const a = overlay.action;
13684
13950
  const b = blockBbox(index, a.target_block);
13685
13951
  if (!b) return null;
13686
- return /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
13952
+ return /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
13687
13953
  SpotlightMask,
13688
13954
  {
13689
13955
  page: page.page_dimensions,
@@ -13697,26 +13963,26 @@ function CinemaLayer({
13697
13963
  const a = overlay.action;
13698
13964
  const b = blockBbox(index, a.target_block);
13699
13965
  if (!b) return null;
13700
- return /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(AnimatedUnderline, { bbox: b, action: a }, overlay.id);
13966
+ return /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(AnimatedUnderline, { bbox: b, action: a }, overlay.id);
13701
13967
  }
13702
13968
  case "highlight": {
13703
13969
  const a = overlay.action;
13704
13970
  const b = blockBbox(index, a.target_block);
13705
13971
  if (!b) return null;
13706
- return /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(AnimatedHighlight, { bbox: b, action: a }, overlay.id);
13972
+ return /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(AnimatedHighlight, { bbox: b, action: a }, overlay.id);
13707
13973
  }
13708
13974
  case "pulse": {
13709
13975
  const a = overlay.action;
13710
13976
  const b = blockBbox(index, a.target_block);
13711
13977
  if (!b) return null;
13712
- return /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(PulseOverlay, { bbox: b, action: a }, overlay.id);
13978
+ return /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(PulseOverlay, { bbox: b, action: a }, overlay.id);
13713
13979
  }
13714
13980
  case "callout": {
13715
13981
  const a = overlay.action;
13716
13982
  const from = blockBbox(index, a.from_block);
13717
13983
  const to = blockBbox(index, a.to_block);
13718
13984
  if (!from || !to) return null;
13719
- return /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
13985
+ return /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
13720
13986
  CalloutArrow,
13721
13987
  {
13722
13988
  fromBbox: from,
@@ -13726,36 +13992,16 @@ function CinemaLayer({
13726
13992
  overlay.id
13727
13993
  );
13728
13994
  }
13729
- case "ghost_reference": {
13730
- const a = overlay.action;
13731
- const hit = index.blockById.get(a.target_block);
13732
- if (!hit) return null;
13733
- const targetPage = index.byPage.get(a.target_page);
13734
- if (!targetPage) return null;
13735
- return /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
13736
- GhostReference,
13737
- {
13738
- page: targetPage.page_dimensions,
13739
- sourceBbox: hit.block.bbox,
13740
- sourceBlockText: hit.block.text,
13741
- sourcePageNumber: hit.pageNumber,
13742
- action: a
13743
- },
13744
- overlay.id
13745
- );
13746
- }
13995
+ case "ghost_reference":
13996
+ return null;
13747
13997
  case "box": {
13748
13998
  const a = overlay.action;
13749
13999
  const b = blockBbox(index, a.target_block);
13750
14000
  if (!b) return null;
13751
- return /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(BoxOverlay, { bbox: b, action: a }, overlay.id);
13752
- }
13753
- case "label": {
13754
- const a = overlay.action;
13755
- const b = blockBbox(index, a.target_block);
13756
- if (!b) return null;
13757
- return /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(StickyLabel, { bbox: b, action: a }, overlay.id);
14001
+ return /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(BoxOverlay, { bbox: b, action: a }, overlay.id);
13758
14002
  }
14003
+ case "label":
14004
+ return null;
13759
14005
  case "clear":
13760
14006
  case "camera":
13761
14007
  return null;
@@ -13765,12 +14011,774 @@ function CinemaLayer({
13765
14011
  );
13766
14012
  }
13767
14013
 
13768
- // src/components/TutorMode/SubtitleBar.tsx
14014
+ // src/components/TutorMode/GhostReferenceOverlay.tsx
14015
+ var import_framer_motion10 = require("framer-motion");
14016
+
14017
+ // src/components/TutorMode/GhostReference.tsx
14018
+ var import_framer_motion9 = require("framer-motion");
14019
+ var import_jsx_runtime49 = require("react/jsx-runtime");
14020
+ var POSITIONS = {
14021
+ "top-right": {
14022
+ top: "clamp(12px, 4vw, 40px)",
14023
+ right: "clamp(12px, 4vw, 40px)"
14024
+ },
14025
+ "top-left": {
14026
+ top: "clamp(12px, 4vw, 40px)",
14027
+ left: "clamp(12px, 4vw, 40px)"
14028
+ },
14029
+ "bottom-right": {
14030
+ bottom: "clamp(12px, 4vw, 40px)",
14031
+ right: "clamp(12px, 4vw, 40px)"
14032
+ },
14033
+ "bottom-left": {
14034
+ bottom: "clamp(12px, 4vw, 40px)",
14035
+ left: "clamp(12px, 4vw, 40px)"
14036
+ }
14037
+ };
14038
+ var INK2 = "#2a2420";
14039
+ var PAPER2 = "#faf6ec";
14040
+ var PAPER_DEEP = "#f3ece0";
14041
+ var ACCENT2 = "#b04a1a";
14042
+ var RULE = "rgba(42, 36, 32, 0.10)";
14043
+ var SERIF2 = "'Iowan Old Style', 'Palatino Linotype', Palatino, 'Book Antiqua', 'EB Garamond', 'Hoefler Text', Georgia, serif";
14044
+ function GhostReference({
14045
+ page,
14046
+ sourceBbox,
14047
+ sourceBlockText,
14048
+ action
14049
+ }) {
14050
+ const [x1, y1, x2, y2] = sourceBbox;
14051
+ const text = sourceBlockText ?? "(figure)";
14052
+ return /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)(
14053
+ import_framer_motion9.motion.div,
14054
+ {
14055
+ initial: { opacity: 0, y: 24, scale: 0.97 },
14056
+ animate: { opacity: 1, y: 0, scale: 1 },
14057
+ exit: { opacity: 0, y: 20, scale: 0.97 },
14058
+ transition: {
14059
+ duration: 0.55,
14060
+ ease: [0.22, 1, 0.36, 1]
14061
+ // custom out-expo for an unhurried settle
14062
+ },
14063
+ style: {
14064
+ position: "absolute",
14065
+ width: "min(420px, calc(100vw - clamp(24px, 8vw, 80px)))",
14066
+ background: PAPER2,
14067
+ color: INK2,
14068
+ border: `1px solid ${RULE}`,
14069
+ // Barely-there corner radius — square-ish corners feel editorial,
14070
+ // 12px+ radius feels SaaS-notification. 3px is the sweet spot.
14071
+ borderRadius: 3,
14072
+ overflow: "hidden",
14073
+ // Warm, two-layer shadow: a tight contact shadow for definition,
14074
+ // a wider diffuse one tinted toward ink rather than pure grey.
14075
+ 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)",
14076
+ pointerEvents: "none",
14077
+ ...POSITIONS[action.position]
14078
+ },
14079
+ "data-role": "ghost-reference",
14080
+ children: [
14081
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
14082
+ import_framer_motion9.motion.span,
14083
+ {
14084
+ "aria-hidden": true,
14085
+ initial: { scaleY: 0 },
14086
+ animate: { scaleY: 1 },
14087
+ exit: { scaleY: 0 },
14088
+ transition: { duration: 0.55, delay: 0.06, ease: [0.22, 1, 0.36, 1] },
14089
+ style: {
14090
+ position: "absolute",
14091
+ left: 0,
14092
+ top: 0,
14093
+ bottom: 0,
14094
+ width: 3,
14095
+ background: ACCENT2,
14096
+ transformOrigin: "top"
14097
+ }
14098
+ }
14099
+ ),
14100
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
14101
+ "span",
14102
+ {
14103
+ "aria-hidden": true,
14104
+ style: {
14105
+ position: "absolute",
14106
+ inset: 0,
14107
+ pointerEvents: "none",
14108
+ background: `
14109
+ radial-gradient(120% 80% at 0% 0%, rgba(255, 244, 220, 0.5) 0%, transparent 55%),
14110
+ radial-gradient(100% 80% at 100% 100%, rgba(243, 229, 200, 0.35) 0%, transparent 60%)
14111
+ `
14112
+ }
14113
+ }
14114
+ ),
14115
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)(
14116
+ "div",
14117
+ {
14118
+ style: {
14119
+ position: "relative",
14120
+ padding: "20px 22px 20px 26px",
14121
+ display: "flex",
14122
+ gap: 16,
14123
+ alignItems: "flex-start"
14124
+ },
14125
+ children: [
14126
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
14127
+ import_framer_motion9.motion.div,
14128
+ {
14129
+ "aria-hidden": true,
14130
+ initial: { opacity: 0, scale: 0.92 },
14131
+ animate: { opacity: 1, scale: 1 },
14132
+ exit: { opacity: 0, scale: 0.92 },
14133
+ transition: { duration: 0.45, delay: 0.18, ease: [0.22, 1, 0.36, 1] },
14134
+ style: {
14135
+ flexShrink: 0,
14136
+ width: 46,
14137
+ aspectRatio: `${page.width} / ${page.height}`,
14138
+ background: PAPER_DEEP,
14139
+ borderRadius: 2,
14140
+ overflow: "hidden",
14141
+ boxShadow: "inset 0 0 0 1px rgba(42, 36, 32, 0.12), 0 1px 3px rgba(42, 36, 32, 0.10)"
14142
+ },
14143
+ children: /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)(
14144
+ "svg",
14145
+ {
14146
+ width: "100%",
14147
+ height: "100%",
14148
+ viewBox: `0 0 ${page.width} ${page.height}`,
14149
+ preserveAspectRatio: "xMidYMid meet",
14150
+ style: { display: "block" },
14151
+ children: [
14152
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
14153
+ "rect",
14154
+ {
14155
+ x: 0,
14156
+ y: 0,
14157
+ width: page.width,
14158
+ height: page.height,
14159
+ fill: PAPER_DEEP
14160
+ }
14161
+ ),
14162
+ TEXT_LINES.map((ln, i) => /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
14163
+ "rect",
14164
+ {
14165
+ x: page.width * ln.x,
14166
+ y: page.height * ln.y,
14167
+ width: page.width * ln.w,
14168
+ height: page.height * 0.012,
14169
+ fill: "rgba(42, 36, 32, 0.18)"
14170
+ },
14171
+ i
14172
+ )),
14173
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
14174
+ "rect",
14175
+ {
14176
+ x: x1,
14177
+ y: y1,
14178
+ width: x2 - x1,
14179
+ height: y2 - y1,
14180
+ fill: "rgba(176, 74, 26, 0.28)",
14181
+ stroke: ACCENT2,
14182
+ strokeWidth: 18
14183
+ }
14184
+ )
14185
+ ]
14186
+ }
14187
+ )
14188
+ }
14189
+ ),
14190
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)(
14191
+ import_framer_motion9.motion.div,
14192
+ {
14193
+ initial: { opacity: 0, y: 6 },
14194
+ animate: { opacity: 1, y: 0 },
14195
+ exit: { opacity: 0 },
14196
+ transition: { duration: 0.5, delay: 0.26, ease: [0.22, 1, 0.36, 1] },
14197
+ style: {
14198
+ flex: 1,
14199
+ minWidth: 0,
14200
+ fontFamily: SERIF2,
14201
+ fontSize: 15.5,
14202
+ lineHeight: 1.55,
14203
+ color: INK2,
14204
+ fontFeatureSettings: "'liga' 1, 'kern' 1, 'onum' 1",
14205
+ textRendering: "optimizeLegibility",
14206
+ letterSpacing: 0.05,
14207
+ // Hang an ornamental opening glyph outside the text column
14208
+ // so the reader's eye falls into the paragraph as if into a
14209
+ // well-set pull quote.
14210
+ position: "relative",
14211
+ paddingLeft: 2
14212
+ },
14213
+ children: [
14214
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
14215
+ "span",
14216
+ {
14217
+ "aria-hidden": true,
14218
+ style: {
14219
+ position: "absolute",
14220
+ left: -14,
14221
+ top: -2,
14222
+ color: ACCENT2,
14223
+ fontSize: 22,
14224
+ lineHeight: 1,
14225
+ fontWeight: 500
14226
+ // ornamental flourish anchoring the paragraph
14227
+ },
14228
+ children: "\u2767"
14229
+ }
14230
+ ),
14231
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
14232
+ "span",
14233
+ {
14234
+ style: {
14235
+ display: "-webkit-box",
14236
+ WebkitLineClamp: 8,
14237
+ WebkitBoxOrient: "vertical",
14238
+ overflow: "hidden"
14239
+ },
14240
+ children: text
14241
+ }
14242
+ )
14243
+ ]
14244
+ }
14245
+ )
14246
+ ]
14247
+ }
14248
+ ),
14249
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
14250
+ import_framer_motion9.motion.span,
14251
+ {
14252
+ "aria-hidden": true,
14253
+ initial: { scaleX: 0, opacity: 0 },
14254
+ animate: { scaleX: 1, opacity: 0.6 },
14255
+ exit: { opacity: 0 },
14256
+ transition: { duration: 0.5, delay: 0.42, ease: [0.22, 1, 0.36, 1] },
14257
+ style: {
14258
+ position: "absolute",
14259
+ right: 22,
14260
+ bottom: 10,
14261
+ height: 1,
14262
+ width: 28,
14263
+ background: ACCENT2,
14264
+ transformOrigin: "right"
14265
+ }
14266
+ }
14267
+ )
14268
+ ]
14269
+ }
14270
+ );
14271
+ }
14272
+ var TEXT_LINES = [
14273
+ { x: 0.1, y: 0.12, w: 0.54 },
14274
+ { x: 0.1, y: 0.18, w: 0.68 },
14275
+ { x: 0.1, y: 0.24, w: 0.48 },
14276
+ { x: 0.1, y: 0.34, w: 0.72 },
14277
+ { x: 0.1, y: 0.4, w: 0.62 },
14278
+ { x: 0.1, y: 0.46, w: 0.66 },
14279
+ { x: 0.1, y: 0.56, w: 0.56 },
14280
+ { x: 0.1, y: 0.62, w: 0.7 },
14281
+ { x: 0.1, y: 0.68, w: 0.44 },
14282
+ { x: 0.1, y: 0.78, w: 0.64 },
14283
+ { x: 0.1, y: 0.84, w: 0.58 }
14284
+ ];
14285
+
14286
+ // src/components/TutorMode/GhostReferenceOverlay.tsx
14287
+ var import_jsx_runtime50 = require("react/jsx-runtime");
14288
+ function GhostReferenceOverlay({
14289
+ overlays,
14290
+ index
14291
+ }) {
14292
+ const ghosts = overlays.filter((o) => o.kind === "ghost_reference");
14293
+ return /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
14294
+ "div",
14295
+ {
14296
+ "data-role": "ghost-reference-overlay",
14297
+ style: {
14298
+ position: "absolute",
14299
+ inset: 0,
14300
+ pointerEvents: "none",
14301
+ // Sits above the Reset view button (z-60) so a ghost card doesn't
14302
+ // disappear behind it if the host also renders the Reset button.
14303
+ zIndex: 70
14304
+ },
14305
+ children: /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(import_framer_motion10.AnimatePresence, { children: ghosts.map((overlay) => {
14306
+ const a = overlay.action;
14307
+ const hit = index.blockById.get(a.target_block);
14308
+ if (!hit) return null;
14309
+ const targetPage = index.byPage.get(a.target_page);
14310
+ if (!targetPage) return null;
14311
+ return /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
14312
+ GhostReference,
14313
+ {
14314
+ page: targetPage.page_dimensions,
14315
+ sourceBbox: hit.block.bbox,
14316
+ sourceBlockText: hit.block.text,
14317
+ sourcePageNumber: hit.pageNumber,
14318
+ action: a
14319
+ },
14320
+ overlay.id
14321
+ );
14322
+ }) })
14323
+ }
14324
+ );
14325
+ }
14326
+
14327
+ // src/components/TutorMode/LabelOverlay.tsx
14328
+ var import_framer_motion12 = require("framer-motion");
14329
+
14330
+ // src/components/TutorMode/StickyLabel.tsx
13769
14331
  var import_framer_motion11 = require("framer-motion");
13770
14332
  var import_jsx_runtime51 = require("react/jsx-runtime");
13771
- function SubtitleBar({ text }) {
13772
- return /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(import_framer_motion11.AnimatePresence, { children: text ? /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(
14333
+ var INK3 = "#2a2420";
14334
+ var PAPER3 = "#faf6ec";
14335
+ var ACCENT3 = "#b04a1a";
14336
+ var SERIF3 = "'Iowan Old Style', 'Palatino Linotype', Palatino, 'Book Antiqua', 'EB Garamond', 'Hoefler Text', Georgia, serif";
14337
+ var STEM = 18;
14338
+ function StickyLabel({ screenAnchor, action }) {
14339
+ const { x, y } = screenAnchor;
14340
+ const layout = LAYOUTS[action.position] ?? LAYOUTS.top;
14341
+ return /* @__PURE__ */ (0, import_jsx_runtime51.jsxs)(
13773
14342
  import_framer_motion11.motion.div,
14343
+ {
14344
+ initial: { opacity: 0, scale: 0.88 },
14345
+ animate: { opacity: 1, scale: 1 },
14346
+ exit: { opacity: 0, scale: 0.92 },
14347
+ transition: {
14348
+ duration: 0.4,
14349
+ ease: [0.22, 1, 0.36, 1]
14350
+ },
14351
+ style: {
14352
+ position: "absolute",
14353
+ left: x,
14354
+ top: y,
14355
+ // Wrapper positions at the anchor; the body's transform places
14356
+ // it on the correct side via `layout.containerTransform`.
14357
+ transform: layout.containerTransform,
14358
+ pointerEvents: "none",
14359
+ // Compose transform-origin toward the anchor so scale-in feels
14360
+ // tethered to the block rather than free-floating.
14361
+ transformOrigin: layout.origin
14362
+ },
14363
+ "data-role": "label",
14364
+ children: [
14365
+ /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(
14366
+ import_framer_motion11.motion.span,
14367
+ {
14368
+ "aria-hidden": true,
14369
+ initial: { scaleX: 0, scaleY: 0, opacity: 0 },
14370
+ animate: { scaleX: 1, scaleY: 1, opacity: 1 },
14371
+ exit: { opacity: 0 },
14372
+ transition: { duration: 0.35, ease: [0.22, 1, 0.36, 1] },
14373
+ style: {
14374
+ position: "absolute",
14375
+ background: ACCENT3,
14376
+ transformOrigin: layout.stemOrigin,
14377
+ ...layout.stem
14378
+ }
14379
+ }
14380
+ ),
14381
+ /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(
14382
+ import_framer_motion11.motion.span,
14383
+ {
14384
+ "aria-hidden": true,
14385
+ initial: { scale: 0 },
14386
+ animate: { scale: 1 },
14387
+ exit: { scale: 0 },
14388
+ transition: { duration: 0.3, delay: 0.15, ease: [0.22, 1, 0.36, 1] },
14389
+ style: {
14390
+ position: "absolute",
14391
+ width: 6,
14392
+ height: 6,
14393
+ borderRadius: "50%",
14394
+ background: ACCENT3,
14395
+ boxShadow: `0 0 0 2px ${PAPER3}, 0 0 0 3px rgba(176, 74, 26, 0.25)`,
14396
+ ...layout.dot
14397
+ }
14398
+ }
14399
+ ),
14400
+ /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(
14401
+ import_framer_motion11.motion.div,
14402
+ {
14403
+ initial: { y: layout.bodyIn.y, x: layout.bodyIn.x, opacity: 0 },
14404
+ animate: { y: 0, x: 0, opacity: 1 },
14405
+ exit: { opacity: 0 },
14406
+ transition: { duration: 0.4, delay: 0.08, ease: [0.22, 1, 0.36, 1] },
14407
+ style: {
14408
+ position: "absolute",
14409
+ ...layout.bodyAnchor,
14410
+ background: PAPER3,
14411
+ color: INK3,
14412
+ border: "1px solid rgba(42, 36, 32, 0.10)",
14413
+ borderRadius: 3,
14414
+ padding: "6px 12px 6px 14px",
14415
+ fontFamily: SERIF3,
14416
+ fontSize: 12.5,
14417
+ lineHeight: 1.25,
14418
+ letterSpacing: 0.6,
14419
+ textTransform: "uppercase",
14420
+ fontWeight: 500,
14421
+ whiteSpace: "nowrap",
14422
+ maxWidth: 240,
14423
+ overflow: "hidden",
14424
+ textOverflow: "ellipsis",
14425
+ // Warm two-layer shadow (matches GhostReference's palette).
14426
+ boxShadow: "0 1px 2px rgba(42, 36, 32, 0.12), 0 8px 18px -6px rgba(42, 36, 32, 0.22)",
14427
+ // Internal left accent rule — a 2px terracotta stripe.
14428
+ backgroundImage: `linear-gradient(to right, ${ACCENT3} 0, ${ACCENT3} 2px, transparent 2px)`,
14429
+ backgroundRepeat: "no-repeat",
14430
+ backgroundSize: "2px 100%",
14431
+ backgroundPosition: "left top"
14432
+ },
14433
+ children: action.text
14434
+ }
14435
+ )
14436
+ ]
14437
+ }
14438
+ );
14439
+ }
14440
+ var LAYOUTS = {
14441
+ top: {
14442
+ containerTransform: "translate(-50%, -100%)",
14443
+ origin: "50% 100%",
14444
+ stem: {
14445
+ left: "50%",
14446
+ bottom: -STEM,
14447
+ width: 1,
14448
+ height: STEM - 4,
14449
+ transform: "translateX(-50%)"
14450
+ },
14451
+ stemOrigin: "bottom",
14452
+ dot: {
14453
+ left: "50%",
14454
+ bottom: -STEM + 1,
14455
+ transform: "translate(-50%, 100%)"
14456
+ },
14457
+ bodyAnchor: { bottom: 0, left: "50%", transform: "translateX(-50%)" },
14458
+ bodyIn: { x: 0, y: -4 }
14459
+ },
14460
+ bottom: {
14461
+ containerTransform: "translate(-50%, 0%)",
14462
+ origin: "50% 0%",
14463
+ stem: {
14464
+ left: "50%",
14465
+ top: STEM - (STEM - 4),
14466
+ width: 1,
14467
+ height: STEM - 4,
14468
+ transform: "translateX(-50%)"
14469
+ },
14470
+ stemOrigin: "top",
14471
+ dot: {
14472
+ left: "50%",
14473
+ top: -1,
14474
+ transform: "translate(-50%, -100%)"
14475
+ },
14476
+ bodyAnchor: { top: STEM, left: "50%", transform: "translateX(-50%)" },
14477
+ bodyIn: { x: 0, y: 4 }
14478
+ },
14479
+ left: {
14480
+ containerTransform: "translate(-100%, -50%)",
14481
+ origin: "100% 50%",
14482
+ stem: {
14483
+ top: "50%",
14484
+ right: -STEM,
14485
+ width: STEM - 4,
14486
+ height: 1,
14487
+ transform: "translateY(-50%)"
14488
+ },
14489
+ stemOrigin: "right",
14490
+ dot: {
14491
+ right: -STEM + 1,
14492
+ top: "50%",
14493
+ transform: "translate(100%, -50%)"
14494
+ },
14495
+ bodyAnchor: { right: 0, top: "50%", transform: "translateY(-50%)" },
14496
+ bodyIn: { x: -4, y: 0 }
14497
+ },
14498
+ right: {
14499
+ containerTransform: "translate(0%, -50%)",
14500
+ origin: "0% 50%",
14501
+ stem: {
14502
+ top: "50%",
14503
+ left: STEM - (STEM - 4),
14504
+ width: STEM - 4,
14505
+ height: 1,
14506
+ transform: "translateY(-50%)"
14507
+ },
14508
+ stemOrigin: "left",
14509
+ dot: {
14510
+ left: -1,
14511
+ top: "50%",
14512
+ transform: "translate(-100%, -50%)"
14513
+ },
14514
+ bodyAnchor: { left: STEM, top: "50%", transform: "translateY(-50%)" },
14515
+ bodyIn: { x: 4, y: 0 }
14516
+ }
14517
+ };
14518
+
14519
+ // src/components/TutorMode/LabelOverlay.tsx
14520
+ var import_jsx_runtime52 = require("react/jsx-runtime");
14521
+ function LabelOverlay({
14522
+ overlays,
14523
+ index,
14524
+ currentPage,
14525
+ camera,
14526
+ viewport
14527
+ }) {
14528
+ const labels = overlays.filter((o) => o.kind === "label");
14529
+ const page = index.byPage.get(currentPage);
14530
+ return /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(
14531
+ "div",
14532
+ {
14533
+ "data-role": "label-overlay",
14534
+ style: {
14535
+ position: "absolute",
14536
+ inset: 0,
14537
+ pointerEvents: "none",
14538
+ overflow: "hidden",
14539
+ // Above CameraView and the Reset button but not above
14540
+ // GhostReferenceOverlay (z:70) — labels are block-attached and
14541
+ // should not cover a cross-page reference card.
14542
+ zIndex: 65
14543
+ },
14544
+ children: /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(import_framer_motion12.AnimatePresence, { children: page ? labels.map((overlay) => {
14545
+ const a = overlay.action;
14546
+ const hit = index.blockById.get(a.target_block);
14547
+ if (!hit) return null;
14548
+ const anchor = computeScreenAnchor(
14549
+ hit.block.bbox,
14550
+ a.position,
14551
+ page,
14552
+ camera,
14553
+ viewport
14554
+ );
14555
+ return /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(
14556
+ StickyLabel,
14557
+ {
14558
+ screenAnchor: anchor,
14559
+ action: a
14560
+ },
14561
+ overlay.id
14562
+ );
14563
+ }) : null })
14564
+ }
14565
+ );
14566
+ }
14567
+ function computeScreenAnchor(bbox, where, page, camera, viewport) {
14568
+ const [x1, y1, x2, y2] = bbox;
14569
+ const pageCX = page.page_dimensions.width / 2;
14570
+ const pageCY = page.page_dimensions.height / 2;
14571
+ let px, py;
14572
+ switch (where) {
14573
+ case "top":
14574
+ px = (x1 + x2) / 2;
14575
+ py = y1;
14576
+ break;
14577
+ case "bottom":
14578
+ px = (x1 + x2) / 2;
14579
+ py = y2;
14580
+ break;
14581
+ case "left":
14582
+ px = x1;
14583
+ py = (y1 + y2) / 2;
14584
+ break;
14585
+ case "right":
14586
+ px = x2;
14587
+ py = (y1 + y2) / 2;
14588
+ break;
14589
+ default:
14590
+ px = (x1 + x2) / 2;
14591
+ py = y1;
14592
+ }
14593
+ const screenX = viewport.width / 2 + camera.x + (px - pageCX) * camera.scale;
14594
+ const screenY = viewport.height / 2 + camera.y + (py - pageCY) * camera.scale;
14595
+ return { x: screenX, y: screenY };
14596
+ }
14597
+
14598
+ // src/components/TutorMode/CalloutLabelOverlay.tsx
14599
+ var import_framer_motion13 = require("framer-motion");
14600
+ var import_jsx_runtime53 = require("react/jsx-runtime");
14601
+ function CalloutLabelOverlay({
14602
+ overlays,
14603
+ index,
14604
+ currentPage,
14605
+ camera,
14606
+ viewport
14607
+ }) {
14608
+ const callouts = overlays.filter(
14609
+ (o) => o.kind === "callout" && o.action.label
14610
+ );
14611
+ const page = index.byPage.get(currentPage);
14612
+ return /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(
14613
+ "div",
14614
+ {
14615
+ "data-role": "callout-label-overlay",
14616
+ style: {
14617
+ position: "absolute",
14618
+ inset: 0,
14619
+ pointerEvents: "none",
14620
+ overflow: "hidden",
14621
+ // Above the arrow stroke (which is inside CameraView) and the
14622
+ // reset button, below the ghost card.
14623
+ zIndex: 68
14624
+ },
14625
+ children: /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(import_framer_motion13.AnimatePresence, { children: page ? callouts.map((overlay) => {
14626
+ const a = overlay.action;
14627
+ const fromHit = index.blockById.get(a.from_block);
14628
+ const toHit = index.blockById.get(a.to_block);
14629
+ if (!fromHit || !toHit || !a.label) return null;
14630
+ const pos = computePillAnchor(
14631
+ fromHit.block.bbox,
14632
+ toHit.block.bbox,
14633
+ page,
14634
+ camera,
14635
+ viewport
14636
+ );
14637
+ return /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(
14638
+ CalloutLabelPill,
14639
+ {
14640
+ label: a.label,
14641
+ anchor: pos,
14642
+ side: pos.side
14643
+ },
14644
+ overlay.id
14645
+ );
14646
+ }) : null })
14647
+ }
14648
+ );
14649
+ }
14650
+ function computePillAnchor(fromBbox, toBbox, page, camera, viewport) {
14651
+ const aCX = (fromBbox[0] + fromBbox[2]) / 2;
14652
+ const aCY = (fromBbox[1] + fromBbox[3]) / 2;
14653
+ const bCX = (toBbox[0] + toBbox[2]) / 2;
14654
+ const bCY = (toBbox[1] + toBbox[3]) / 2;
14655
+ const dx = bCX - aCX;
14656
+ const dy = bCY - aCY;
14657
+ const len = Math.hypot(dx, dy) || 1;
14658
+ const ux = dx / len;
14659
+ const uy = dy / len;
14660
+ const bHalfW = (toBbox[2] - toBbox[0]) / 2;
14661
+ const bHalfH = (toBbox[3] - toBbox[1]) / 2;
14662
+ const bOff = Math.min(Math.max(bHalfW, bHalfH), 60);
14663
+ const toX = bCX - ux * bOff;
14664
+ const toY = bCY - uy * bOff;
14665
+ const pageCX = page.page_dimensions.width / 2;
14666
+ const pageCY = page.page_dimensions.height / 2;
14667
+ const tipScreenX = viewport.width / 2 + camera.x + (toX - pageCX) * camera.scale;
14668
+ const tipScreenY = viewport.height / 2 + camera.y + (toY - pageCY) * camera.scale;
14669
+ const isVertical = Math.abs(dy) >= Math.abs(dx);
14670
+ const OFFSET = 32;
14671
+ const MAX_PILL_W = 220;
14672
+ const MAX_PILL_H = 30;
14673
+ const SAFE = 16;
14674
+ if (isVertical) {
14675
+ const canFitRight = tipScreenX + OFFSET + MAX_PILL_W < viewport.width - SAFE;
14676
+ const side2 = canFitRight ? "right" : "left";
14677
+ return {
14678
+ x: tipScreenX + (side2 === "right" ? OFFSET : -OFFSET),
14679
+ y: tipScreenY,
14680
+ side: side2
14681
+ };
14682
+ }
14683
+ const canFitBelow = tipScreenY + OFFSET + MAX_PILL_H < viewport.height - SAFE;
14684
+ const side = canFitBelow ? "below" : "above";
14685
+ return {
14686
+ x: tipScreenX,
14687
+ y: tipScreenY + (side === "below" ? OFFSET : -OFFSET),
14688
+ side
14689
+ };
14690
+ }
14691
+ function CalloutLabelPill({
14692
+ label,
14693
+ anchor,
14694
+ side
14695
+ }) {
14696
+ const spec = PILL_SIDE_SPECS[side];
14697
+ return /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(
14698
+ import_framer_motion13.motion.div,
14699
+ {
14700
+ initial: { opacity: 0, scale: 0.92, ...spec.slideIn },
14701
+ animate: { opacity: 1, scale: 1, x: 0, y: 0 },
14702
+ exit: { opacity: 0, scale: 0.94 },
14703
+ transition: { duration: 0.45, delay: 0.5, ease: EASE_OUT_EXPO },
14704
+ style: {
14705
+ position: "absolute",
14706
+ left: anchor.x,
14707
+ top: anchor.y,
14708
+ transform: spec.transform,
14709
+ pointerEvents: "none",
14710
+ background: PAPER,
14711
+ color: INK,
14712
+ border: "1px solid rgba(42, 36, 32, 0.10)",
14713
+ borderRadius: 3,
14714
+ padding: spec.padding,
14715
+ fontFamily: SERIF,
14716
+ fontSize: 11.5,
14717
+ lineHeight: 1.2,
14718
+ letterSpacing: 0.6,
14719
+ textTransform: "uppercase",
14720
+ fontWeight: 500,
14721
+ whiteSpace: "nowrap",
14722
+ maxWidth: 220,
14723
+ overflow: "hidden",
14724
+ textOverflow: "ellipsis",
14725
+ boxShadow: "0 1px 2px rgba(42, 36, 32, 0.12), 0 8px 18px -6px rgba(42, 36, 32, 0.22)",
14726
+ // Accent rule on the "inward" edge (the one closest to the arrow).
14727
+ backgroundImage: spec.accentGradient,
14728
+ backgroundRepeat: "no-repeat",
14729
+ backgroundSize: spec.accentSize,
14730
+ backgroundPosition: spec.accentPosition
14731
+ },
14732
+ "data-role": "callout-label",
14733
+ children: label
14734
+ }
14735
+ );
14736
+ }
14737
+ var PILL_SIDE_SPECS = {
14738
+ // Pill sits to the RIGHT of a vertical arrow → left edge anchors at
14739
+ // offset point, accent rule on the left (pointing back toward arrow).
14740
+ right: {
14741
+ transform: "translate(0, -50%)",
14742
+ slideIn: { x: -6 },
14743
+ padding: "5px 12px 5px 14px",
14744
+ accentGradient: `linear-gradient(to right, ${ACCENT} 0, ${ACCENT} 2px, transparent 2px)`,
14745
+ accentSize: "2px 100%",
14746
+ accentPosition: "left top"
14747
+ },
14748
+ left: {
14749
+ transform: "translate(-100%, -50%)",
14750
+ slideIn: { x: 6 },
14751
+ padding: "5px 14px 5px 12px",
14752
+ accentGradient: `linear-gradient(to left, ${ACCENT} 0, ${ACCENT} 2px, transparent 2px)`,
14753
+ accentSize: "2px 100%",
14754
+ accentPosition: "right top"
14755
+ },
14756
+ // Pill sits BELOW a horizontal arrow → top edge anchors at offset
14757
+ // point, accent rule on the top (pointing back up toward arrow).
14758
+ below: {
14759
+ transform: "translate(-50%, 0)",
14760
+ slideIn: { y: -6 },
14761
+ padding: "7px 12px 5px 12px",
14762
+ accentGradient: `linear-gradient(to bottom, ${ACCENT} 0, ${ACCENT} 2px, transparent 2px)`,
14763
+ accentSize: "100% 2px",
14764
+ accentPosition: "left top"
14765
+ },
14766
+ above: {
14767
+ transform: "translate(-50%, -100%)",
14768
+ slideIn: { y: 6 },
14769
+ padding: "5px 12px 7px 12px",
14770
+ accentGradient: `linear-gradient(to top, ${ACCENT} 0, ${ACCENT} 2px, transparent 2px)`,
14771
+ accentSize: "100% 2px",
14772
+ accentPosition: "left bottom"
14773
+ }
14774
+ };
14775
+
14776
+ // src/components/TutorMode/SubtitleBar.tsx
14777
+ var import_framer_motion14 = require("framer-motion");
14778
+ var import_jsx_runtime54 = require("react/jsx-runtime");
14779
+ function SubtitleBar({ text }) {
14780
+ return /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(import_framer_motion14.AnimatePresence, { children: text ? /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(
14781
+ import_framer_motion14.motion.div,
13774
14782
  {
13775
14783
  initial: { opacity: 0, y: 20 },
13776
14784
  animate: { opacity: 1, y: 0 },
@@ -13996,12 +15004,17 @@ var StoryboardEngine = class {
13996
15004
  const blockCY = (y1 + y2) / 2;
13997
15005
  const pageCX = pageDims.page_dimensions.width / 2;
13998
15006
  const pageCY = pageDims.page_dimensions.height / 2;
13999
- const x = (pageCX - blockCX) * finalScale;
14000
- const y = (pageCY - blockCY) * finalScale;
15007
+ const rawX = (pageCX - blockCX) * finalScale;
15008
+ const rawY = (pageCY - blockCY) * finalScale;
15009
+ const clamped = clampCamera(
15010
+ { scale: finalScale, x: rawX, y: rawY },
15011
+ pageDims.page_dimensions,
15012
+ viewport
15013
+ );
14001
15014
  const camera = {
14002
- scale: finalScale,
14003
- x,
14004
- y,
15015
+ scale: clamped.scale,
15016
+ x: clamped.x,
15017
+ y: clamped.y,
14005
15018
  easing: action.easing
14006
15019
  };
14007
15020
  narrationStore.getState().setCamera(camera);
@@ -14986,7 +15999,7 @@ function storyboardFromMatch(match, page) {
14986
15999
  }
14987
16000
 
14988
16001
  // src/components/TutorMode/TutorModeContainer.tsx
14989
- var import_jsx_runtime52 = require("react/jsx-runtime");
16002
+ var import_jsx_runtime55 = require("react/jsx-runtime");
14990
16003
  function buildBBoxIndex(bboxData) {
14991
16004
  const byPage = /* @__PURE__ */ new Map();
14992
16005
  const blockById = /* @__PURE__ */ new Map();
@@ -15024,16 +16037,34 @@ function TutorModeContainer({
15024
16037
  minOverlayDurationMs,
15025
16038
  backgroundColor = "#ffffff",
15026
16039
  loadingComponent,
16040
+ onPageChange,
15027
16041
  className
15028
16042
  }) {
15029
- const containerRef = (0, import_react56.useRef)(null);
15030
- const index = (0, import_react56.useMemo)(() => buildBBoxIndex(bboxData), [bboxData]);
15031
- const { document: document2 } = usePDFViewer();
15032
- const [pageProxy, setPageProxy] = (0, import_react56.useState)(null);
15033
- const [viewport, setViewport] = (0, import_react56.useState)({ width: 800, height: 1e3 });
16043
+ const containerRef = (0, import_react58.useRef)(null);
16044
+ const index = (0, import_react58.useMemo)(() => buildBBoxIndex(bboxData), [bboxData]);
16045
+ const {
16046
+ document: document2,
16047
+ currentPage: viewerCurrentPage,
16048
+ numPages,
16049
+ goToPage: viewerGoToPage
16050
+ } = usePDFViewer();
16051
+ const [pageProxy, setPageProxy] = (0, import_react58.useState)(null);
16052
+ const [viewport, setViewport] = (0, import_react58.useState)({ width: 800, height: 1e3 });
15034
16053
  const camera = (0, import_zustand2.useStore)(narrationStore, (s) => s.camera);
15035
16054
  const activeOverlays = (0, import_zustand2.useStore)(narrationStore, (s) => s.activeOverlays);
15036
- (0, import_react56.useEffect)(() => {
16055
+ (0, import_react58.useEffect)(() => {
16056
+ if (numPages <= 0) return;
16057
+ if (pageNumber < 1 || pageNumber > numPages) return;
16058
+ if (viewerCurrentPage === pageNumber) return;
16059
+ viewerGoToPage(pageNumber);
16060
+ }, [pageNumber, numPages, viewerCurrentPage, viewerGoToPage]);
16061
+ (0, import_react58.useEffect)(() => {
16062
+ if (!onPageChange) return;
16063
+ if (viewerCurrentPage === pageNumber) return;
16064
+ if (viewerCurrentPage < 1) return;
16065
+ onPageChange(viewerCurrentPage);
16066
+ }, [viewerCurrentPage, pageNumber, onPageChange]);
16067
+ (0, import_react58.useEffect)(() => {
15037
16068
  if (!containerRef.current) return;
15038
16069
  const el = containerRef.current;
15039
16070
  const update = () => setViewport({ width: el.clientWidth, height: el.clientHeight });
@@ -15042,7 +16073,7 @@ function TutorModeContainer({
15042
16073
  ro.observe(el);
15043
16074
  return () => ro.disconnect();
15044
16075
  }, []);
15045
- (0, import_react56.useEffect)(() => {
16076
+ (0, import_react58.useEffect)(() => {
15046
16077
  if (!document2) {
15047
16078
  setPageProxy(null);
15048
16079
  return;
@@ -15057,10 +16088,10 @@ function TutorModeContainer({
15057
16088
  cancelled = true;
15058
16089
  };
15059
16090
  }, [document2, pageNumber]);
15060
- (0, import_react56.useEffect)(() => {
16091
+ (0, import_react58.useEffect)(() => {
15061
16092
  narrationStore.getState().setCurrentPage(pageNumber);
15062
16093
  }, [pageNumber, narrationStore]);
15063
- (0, import_react56.useEffect)(() => {
16094
+ (0, import_react58.useEffect)(() => {
15064
16095
  const page2 = index.byPage.get(pageNumber);
15065
16096
  if (!page2) return;
15066
16097
  if (viewport.width === 0 || viewport.height === 0) return;
@@ -15071,8 +16102,8 @@ function TutorModeContainer({
15071
16102
  ) * 0.95;
15072
16103
  narrationStore.getState().setCamera({ scale: fit, x: 0, y: 0 });
15073
16104
  }, [pageNumber, viewport, index, narrationStore]);
15074
- const engineRef = (0, import_react56.useRef)(null);
15075
- (0, import_react56.useEffect)(() => {
16105
+ const engineRef = (0, import_react58.useRef)(null);
16106
+ (0, import_react58.useEffect)(() => {
15076
16107
  engineRef.current = new StoryboardEngine({
15077
16108
  narrationStore,
15078
16109
  bboxIndex: index,
@@ -15081,10 +16112,10 @@ function TutorModeContainer({
15081
16112
  });
15082
16113
  return () => engineRef.current?.cancelPending();
15083
16114
  }, [narrationStore, index, viewport, minOverlayDurationMs]);
15084
- const abortRef = (0, import_react56.useRef)(null);
15085
- const debounceRef = (0, import_react56.useRef)(null);
15086
- const lastChunkRef = (0, import_react56.useRef)(null);
15087
- (0, import_react56.useEffect)(() => {
16115
+ const abortRef = (0, import_react58.useRef)(null);
16116
+ const debounceRef = (0, import_react58.useRef)(null);
16117
+ const lastChunkRef = (0, import_react58.useRef)(null);
16118
+ (0, import_react58.useEffect)(() => {
15088
16119
  if (!llm) return;
15089
16120
  if (!currentChunk || currentChunk === lastChunkRef.current) return;
15090
16121
  if (debounceRef.current) clearTimeout(debounceRef.current);
@@ -15171,7 +16202,7 @@ function TutorModeContainer({
15171
16202
  if (debounceRef.current) clearTimeout(debounceRef.current);
15172
16203
  };
15173
16204
  }, [currentChunk, llm, index, pageNumber, narrationStore, embeddingProvider, llmTimeoutMs]);
15174
- (0, import_react56.useEffect)(() => {
16205
+ (0, import_react58.useEffect)(() => {
15175
16206
  if (!currentChunk) return;
15176
16207
  const t = setTimeout(() => {
15177
16208
  if (!engineRef.current) return;
@@ -15189,7 +16220,7 @@ function TutorModeContainer({
15189
16220
  const baseW = page ? page.page_dimensions.width * (scale || 1) : 0;
15190
16221
  const baseH = page ? page.page_dimensions.height * (scale || 1) : 0;
15191
16222
  const isReady = !!page && !!pageProxy;
15192
- return /* @__PURE__ */ (0, import_jsx_runtime52.jsxs)(
16223
+ return /* @__PURE__ */ (0, import_jsx_runtime55.jsxs)(
15193
16224
  "div",
15194
16225
  {
15195
16226
  ref: containerRef,
@@ -15204,7 +16235,7 @@ function TutorModeContainer({
15204
16235
  "data-role": "tutor-mode-container",
15205
16236
  "data-page-loaded": isReady ? "true" : "false",
15206
16237
  children: [
15207
- showExitButton && isReady ? /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(
16238
+ showExitButton && isReady ? /* @__PURE__ */ (0, import_jsx_runtime55.jsx)(
15208
16239
  "button",
15209
16240
  {
15210
16241
  onClick: () => {
@@ -15235,43 +16266,66 @@ function TutorModeContainer({
15235
16266
  children: "Reset view"
15236
16267
  }
15237
16268
  ) : null,
15238
- isReady ? /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(CameraView, { camera, children: /* @__PURE__ */ (0, import_jsx_runtime52.jsxs)(
15239
- "div",
15240
- {
15241
- style: {
15242
- position: "absolute",
15243
- top: "50%",
15244
- left: "50%",
15245
- width: baseW,
15246
- height: baseH,
15247
- transform: "translate(-50%, -50%)"
15248
- },
15249
- children: [
15250
- /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(
15251
- PDFPage,
15252
- {
15253
- pageNumber,
15254
- page: pageProxy,
15255
- scale: rasterScale,
15256
- rotation,
15257
- showTextLayer: false,
15258
- showHighlightLayer: false,
15259
- showAnnotationLayer: false
15260
- }
15261
- ),
15262
- /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(
15263
- CinemaLayer,
15264
- {
15265
- page,
15266
- index,
15267
- overlays: activeOverlays,
15268
- scale: scale || 1
15269
- }
15270
- )
15271
- ]
15272
- }
15273
- ) }) : /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(TutorLoadingState, { custom: loadingComponent }),
15274
- showSubtitles ? /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(SubtitleBar, { text: currentChunk ?? null }) : null
16269
+ isReady ? /* @__PURE__ */ (0, import_jsx_runtime55.jsxs)(import_jsx_runtime55.Fragment, { children: [
16270
+ /* @__PURE__ */ (0, import_jsx_runtime55.jsx)(CameraView, { camera, children: /* @__PURE__ */ (0, import_jsx_runtime55.jsxs)(
16271
+ "div",
16272
+ {
16273
+ style: {
16274
+ position: "absolute",
16275
+ top: "50%",
16276
+ left: "50%",
16277
+ width: baseW,
16278
+ height: baseH,
16279
+ transform: "translate(-50%, -50%)"
16280
+ },
16281
+ children: [
16282
+ /* @__PURE__ */ (0, import_jsx_runtime55.jsx)(
16283
+ PDFPage,
16284
+ {
16285
+ pageNumber,
16286
+ page: pageProxy,
16287
+ scale: rasterScale,
16288
+ rotation,
16289
+ showTextLayer: false,
16290
+ showHighlightLayer: false,
16291
+ showAnnotationLayer: false
16292
+ }
16293
+ ),
16294
+ /* @__PURE__ */ (0, import_jsx_runtime55.jsx)(
16295
+ CinemaLayer,
16296
+ {
16297
+ page,
16298
+ index,
16299
+ overlays: activeOverlays,
16300
+ scale: scale || 1
16301
+ }
16302
+ )
16303
+ ]
16304
+ }
16305
+ ) }),
16306
+ /* @__PURE__ */ (0, import_jsx_runtime55.jsx)(
16307
+ LabelOverlay,
16308
+ {
16309
+ overlays: activeOverlays,
16310
+ index,
16311
+ currentPage: pageNumber,
16312
+ camera,
16313
+ viewport
16314
+ }
16315
+ ),
16316
+ /* @__PURE__ */ (0, import_jsx_runtime55.jsx)(
16317
+ CalloutLabelOverlay,
16318
+ {
16319
+ overlays: activeOverlays,
16320
+ index,
16321
+ currentPage: pageNumber,
16322
+ camera,
16323
+ viewport
16324
+ }
16325
+ ),
16326
+ /* @__PURE__ */ (0, import_jsx_runtime55.jsx)(GhostReferenceOverlay, { overlays: activeOverlays, index })
16327
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime55.jsx)(TutorLoadingState, { custom: loadingComponent }),
16328
+ showSubtitles ? /* @__PURE__ */ (0, import_jsx_runtime55.jsx)(SubtitleBar, { text: currentChunk ?? null }) : null
15275
16329
  ]
15276
16330
  }
15277
16331
  );
@@ -15280,7 +16334,7 @@ function TutorLoadingState({
15280
16334
  custom
15281
16335
  }) {
15282
16336
  if (custom) {
15283
- return /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(
16337
+ return /* @__PURE__ */ (0, import_jsx_runtime55.jsx)(
15284
16338
  "div",
15285
16339
  {
15286
16340
  style: {
@@ -15295,7 +16349,7 @@ function TutorLoadingState({
15295
16349
  }
15296
16350
  );
15297
16351
  }
15298
- return /* @__PURE__ */ (0, import_jsx_runtime52.jsxs)(
16352
+ return /* @__PURE__ */ (0, import_jsx_runtime55.jsxs)(
15299
16353
  "div",
15300
16354
  {
15301
16355
  style: {
@@ -15312,7 +16366,7 @@ function TutorLoadingState({
15312
16366
  },
15313
16367
  "data-role": "tutor-loading",
15314
16368
  children: [
15315
- /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(
16369
+ /* @__PURE__ */ (0, import_jsx_runtime55.jsx)(
15316
16370
  "div",
15317
16371
  {
15318
16372
  "aria-hidden": true,
@@ -15326,8 +16380,8 @@ function TutorLoadingState({
15326
16380
  }
15327
16381
  }
15328
16382
  ),
15329
- /* @__PURE__ */ (0, import_jsx_runtime52.jsx)("span", { children: "Loading document\u2026" }),
15330
- /* @__PURE__ */ (0, import_jsx_runtime52.jsx)("style", { children: `
16383
+ /* @__PURE__ */ (0, import_jsx_runtime55.jsx)("span", { children: "Loading document\u2026" }),
16384
+ /* @__PURE__ */ (0, import_jsx_runtime55.jsx)("style", { children: `
15331
16385
  @keyframes pdf-tutor-spin {
15332
16386
  from { transform: rotate(0deg); }
15333
16387
  to { transform: rotate(360deg); }