@bendyline/squisq-react 1.4.2 → 2.0.0

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.
Files changed (45) hide show
  1. package/README.md +30 -3
  2. package/dist/index.d.ts +174 -27
  3. package/dist/index.js +1244 -603
  4. package/dist/index.js.map +1 -1
  5. package/dist/squisq-player.global.js +54 -37
  6. package/dist/squisq-player.global.js.map +1 -1
  7. package/dist/standalone-source.js +1 -1
  8. package/package.json +2 -2
  9. package/src/BlockRenderer.tsx +53 -17
  10. package/src/DocControlsSlideshow.tsx +222 -5
  11. package/src/DocPlayer.tsx +367 -183
  12. package/src/DocPlayerWithSidebar.tsx +4 -0
  13. package/src/DocProgressBar.tsx +40 -1
  14. package/src/LinearDocView.tsx +135 -62
  15. package/src/MarkdownRenderer.tsx +40 -97
  16. package/src/MediaClipLayer.tsx +12 -2
  17. package/src/__tests__/BlockRenderer.test.tsx +79 -8
  18. package/src/__tests__/DocControlsSlideshow.test.tsx +94 -1
  19. package/src/__tests__/DocPlayer.test.tsx +505 -0
  20. package/src/__tests__/DocProgressBar.test.tsx +28 -2
  21. package/src/__tests__/LinearDocView.test.tsx +91 -11
  22. package/src/__tests__/MapLayer.test.tsx +63 -0
  23. package/src/__tests__/MarkdownRenderer.test.tsx +13 -2
  24. package/src/__tests__/MediaClipLayer.test.tsx +70 -0
  25. package/src/__tests__/MediaContext.test.tsx +51 -0
  26. package/src/__tests__/PathLayer.test.tsx +12 -1
  27. package/src/__tests__/VideoLayer.test.tsx +94 -0
  28. package/src/__tests__/fillStyle.test.tsx +3 -2
  29. package/src/__tests__/standaloneEntry.test.tsx +103 -0
  30. package/src/__tests__/useAudioSync.test.ts +49 -0
  31. package/src/__tests__/useDocPlayback.transition.test.ts +48 -5
  32. package/src/__tests__/useViewportOrientation.test.ts +22 -0
  33. package/src/hooks/MediaContext.tsx +12 -3
  34. package/src/hooks/useAudioSync.ts +61 -12
  35. package/src/hooks/useDocPlayback.ts +40 -12
  36. package/src/hooks/useViewportOrientation.ts +2 -4
  37. package/src/index.ts +5 -2
  38. package/src/layers/MapLayer.tsx +7 -6
  39. package/src/layers/PathLayer.tsx +20 -11
  40. package/src/layers/ShapeLayer.tsx +4 -2
  41. package/src/layers/TextLayer.tsx +4 -3
  42. package/src/layers/TreeLayer.tsx +167 -0
  43. package/src/layers/VideoLayer.tsx +20 -6
  44. package/src/standalone-entry.tsx +91 -14
  45. package/src/types.ts +13 -13
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // src/DocPlayer.tsx
2
- import { Fragment as Fragment3, useRef as useRef7, useState as useState7, useEffect as useEffect8, useCallback as useCallback6, useMemo as useMemo9 } from "react";
2
+ import { Fragment as Fragment3, useId as useId7, useRef as useRef9, useState as useState9, useEffect as useEffect10, useCallback as useCallback7, useMemo as useMemo9 } from "react";
3
3
  import {
4
- isTemplateBlock as isTemplateBlock2,
4
+ isTemplateBlock as isTemplateBlock3,
5
5
  getCaptionAtTime as getCaptionAtTime2,
6
6
  resolveMediaSchedule,
7
7
  getDocPlaybackDuration
@@ -32,9 +32,15 @@ function useMediaUrl(relativePath, basePath) {
32
32
  return;
33
33
  }
34
34
  let cancelled = false;
35
- provider.resolveUrl(safePath).then((resolved) => {
36
- if (!cancelled) setUrl(resolved);
37
- });
35
+ setUrl(fallback);
36
+ provider.resolveUrl(safePath).then(
37
+ (resolved) => {
38
+ if (!cancelled) setUrl(resolved);
39
+ },
40
+ () => {
41
+ if (!cancelled) setUrl(fallback);
42
+ }
43
+ );
38
44
  return () => {
39
45
  cancelled = true;
40
46
  };
@@ -63,7 +69,8 @@ function MediaClipLayer({
63
69
  currentTime,
64
70
  isPlaying,
65
71
  basePath,
66
- renderMode = false
72
+ renderMode = false,
73
+ muted = false
67
74
  }) {
68
75
  const { renderClips, activeIds } = useMediaSchedule(schedule, currentTime);
69
76
  if (renderClips.length === 0) return null;
@@ -75,7 +82,8 @@ function MediaClipLayer({
75
82
  currentTime,
76
83
  isPlaying,
77
84
  basePath,
78
- renderMode
85
+ renderMode,
86
+ muted
79
87
  },
80
88
  clip.id
81
89
  )) });
@@ -86,7 +94,8 @@ function MediaClipElement({
86
94
  currentTime,
87
95
  isPlaying,
88
96
  basePath,
89
- renderMode
97
+ renderMode,
98
+ muted
90
99
  }) {
91
100
  const ref = useRef(null);
92
101
  const src = useMediaUrl(clip.src, basePath);
@@ -111,7 +120,7 @@ function MediaClipElement({
111
120
  } else {
112
121
  el.pause();
113
122
  }
114
- }, [active, currentTime, isPlaying, renderMode, clip.sourceIn, clip.absoluteStart]);
123
+ }, [active, currentTime, isPlaying, renderMode, clip.sourceIn, clip.absoluteStart, src]);
115
124
  const isVideo = clip.kind === "video";
116
125
  const common = {
117
126
  ref,
@@ -141,13 +150,21 @@ function MediaClipElement({
141
150
  }
142
151
  );
143
152
  }
144
- return /* @__PURE__ */ jsx("audio", { ...common, muted: renderMode, style: { position: "absolute", width: 0, height: 0 } });
153
+ return /* @__PURE__ */ jsx(
154
+ "audio",
155
+ {
156
+ ...common,
157
+ muted: renderMode || muted,
158
+ style: { position: "absolute", width: 0, height: 0 }
159
+ }
160
+ );
145
161
  }
146
162
 
147
163
  // src/DocPlayer.tsx
148
164
  import { applySurface as applySurface2 } from "@bendyline/squisq/schemas";
149
165
 
150
166
  // src/BlockRenderer.tsx
167
+ import { useId as useId5 } from "react";
151
168
  import { resolveTransitionDuration } from "@bendyline/squisq/schemas";
152
169
 
153
170
  // src/layers/ImageLayer.tsx
@@ -316,7 +333,7 @@ function remapToKenBurns(anim) {
316
333
  }
317
334
 
318
335
  // src/layers/TextLayer.tsx
319
- import { useMemo as useMemo3 } from "react";
336
+ import { useId, useMemo as useMemo3 } from "react";
320
337
  import { DEFAULT_DOC_FONT } from "@bendyline/squisq/schemas";
321
338
  import {
322
339
  parseHtmlToNodes,
@@ -505,6 +522,7 @@ function IconTextLayer({ layer, viewport, blockTime }) {
505
522
  ) });
506
523
  }
507
524
  function PlainTextLayer({ layer, viewport, blockTime }) {
525
+ const defsId = `${useId().replace(/:/g, "")}-${layer.id}`;
508
526
  const { content, position, animation } = layer;
509
527
  const { text, style } = content;
510
528
  const rawX = resolveValue(position.x, viewport.width);
@@ -538,13 +556,13 @@ function PlainTextLayer({ layer, viewport, blockTime }) {
538
556
  fill: style.color,
539
557
  ...animStyle.style
540
558
  };
541
- const filterId = style.shadow ? `shadow-${layer.id}` : void 0;
559
+ const filterId = style.shadow ? `shadow-${defsId}` : void 0;
542
560
  return /* @__PURE__ */ jsxs2("g", { className: `block-layer block-layer--text ${animStyle.className}`, "data-layer-id": layer.id, children: [
543
561
  style.shadow && /* @__PURE__ */ jsx4("defs", { children: /* @__PURE__ */ jsx4("filter", { id: filterId, x: "-20%", y: "-20%", width: "140%", height: "140%", children: /* @__PURE__ */ jsx4("feDropShadow", { dx: "0", dy: "2", stdDeviation: "3", floodColor: "rgba(0,0,0,0.7)" }) }) }),
544
562
  /* @__PURE__ */ jsx4(
545
563
  TextBox,
546
564
  {
547
- layerId: layer.id,
565
+ layerId: defsId,
548
566
  style,
549
567
  box: boxWidth != null && boxHeight != null ? {
550
568
  x: rawX - anchorAxis(anchor, boxWidth, "x"),
@@ -746,9 +764,11 @@ function wrapText(text, fontSize, maxWidth) {
746
764
  }
747
765
 
748
766
  // src/layers/ShapeLayer.tsx
767
+ import { useId as useId2 } from "react";
749
768
  import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
750
769
  function ShapeLayer({ layer, viewport, blockTime }) {
751
770
  const { content, position, animation } = layer;
771
+ const defsId = `${useId2().replace(/:/g, "")}-${layer.id}`;
752
772
  const rawX = resolveValue(position.x, viewport.width);
753
773
  const rawY = resolveValue(position.y, viewport.height);
754
774
  const width = position.width ? resolveValue(position.width, viewport.width) : 100;
@@ -782,12 +802,12 @@ function ShapeLayer({ layer, viewport, blockTime }) {
782
802
  );
783
803
  }
784
804
  const { fill: fillValue, def: fillDef } = resolveFill(
785
- layer.id,
805
+ defsId,
786
806
  fill,
787
807
  content.gradient,
788
808
  content.pattern
789
809
  );
790
- const { filterAttr, def: filterDef } = resolveShapeFilter(layer.id, content.filter);
810
+ const { filterAttr, def: filterDef } = resolveShapeFilter(defsId, content.filter);
791
811
  const dash = borderDashArray(content.borderStyle, content.strokeWidth);
792
812
  const shapeProps = {
793
813
  fill: fillValue,
@@ -847,6 +867,7 @@ function ShapeLayer({ layer, viewport, blockTime }) {
847
867
  }
848
868
 
849
869
  // src/layers/PathLayer.tsx
870
+ import { useId as useId3 } from "react";
850
871
  import { markerPath, shapePath } from "@bendyline/squisq/doc";
851
872
  import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
852
873
  function effectivePath(layer, viewport) {
@@ -860,23 +881,28 @@ function effectivePath(layer, viewport) {
860
881
  const derived = shapePath(content.shapeKind, rawX + anchor.x, rawY + anchor.y, w, h);
861
882
  return derived ?? content.d;
862
883
  }
863
- function effectiveMarker(explicit, arrow, end) {
884
+ function readLegacyArrow(content) {
885
+ return content.arrow;
886
+ }
887
+ function effectiveMarker(explicit, legacyArrow, end) {
864
888
  if (explicit) return explicit;
865
- const wants = arrow === "both" || arrow === end;
889
+ const wants = legacyArrow === "both" || legacyArrow === end;
866
890
  return wants ? "arrow" : "none";
867
891
  }
868
892
  function PathLayer({ layer, viewport, blockTime }) {
869
893
  const { content, animation, id } = layer;
894
+ const defsId = `${useId3().replace(/:/g, "")}-${id}`;
870
895
  const d = effectivePath(layer, viewport);
871
896
  const stroke = content.stroke ?? "#1e293b";
872
897
  const strokeWidth = content.strokeWidth ?? 2;
873
- const { fill, def: fillDef } = resolveFill(id, content.fill ?? "none", content.gradient);
898
+ const { fill, def: fillDef } = resolveFill(defsId, content.fill ?? "none", content.gradient);
874
899
  const dash = content.borderStyle ? borderDashArray(content.borderStyle, strokeWidth) : content.dasharray;
875
900
  const animStyle = getAnimationStyle(animation, blockTime);
876
- const startId = `marker-start-${id}`;
877
- const endId = `marker-end-${id}`;
878
- const start = markerPath(effectiveMarker(content.startMarker, content.arrow, "start"), "start");
879
- const end = markerPath(effectiveMarker(content.endMarker, content.arrow, "end"), "end");
901
+ const startId = `marker-start-${defsId}`;
902
+ const endId = `marker-end-${defsId}`;
903
+ const legacyArrow = readLegacyArrow(content);
904
+ const start = markerPath(effectiveMarker(content.startMarker, legacyArrow, "start"), "start");
905
+ const end = markerPath(effectiveMarker(content.endMarker, legacyArrow, "end"), "end");
880
906
  return /* @__PURE__ */ jsxs4(
881
907
  "g",
882
908
  {
@@ -938,7 +964,7 @@ function MarkerDef({
938
964
  }
939
965
 
940
966
  // src/layers/MapLayer.tsx
941
- import { useState as useState2, useEffect as useEffect3 } from "react";
967
+ import { useId as useId4, useState as useState2, useEffect as useEffect3 } from "react";
942
968
 
943
969
  // src/utils/mapTileUtils.ts
944
970
  var TILE_PROVIDERS = {
@@ -1124,6 +1150,7 @@ function drawAttribution(ctx, text, width, height) {
1124
1150
  import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
1125
1151
  function MapLayer({ layer, basePath, viewport, blockTime }) {
1126
1152
  const { content, position, animation } = layer;
1153
+ const clipId = `map-clip-${useId4().replace(/:/g, "")}-${layer.id}`;
1127
1154
  const [mapImageUrl, setMapImageUrl] = useState2(null);
1128
1155
  const [isLoading, setIsLoading] = useState2(true);
1129
1156
  const [error, setError] = useState2(null);
@@ -1168,11 +1195,12 @@ function MapLayer({ layer, basePath, viewport, blockTime }) {
1168
1195
  cancelled = true;
1169
1196
  };
1170
1197
  }, [
1171
- content.center.lat,
1172
- content.center.lng,
1198
+ content.center,
1173
1199
  content.zoom,
1174
1200
  content.style,
1175
1201
  content.staticSrc,
1202
+ content.markers,
1203
+ content.showAttribution,
1176
1204
  width,
1177
1205
  height,
1178
1206
  basePath
@@ -1237,8 +1265,8 @@ function MapLayer({ layer, basePath, viewport, blockTime }) {
1237
1265
  style: animStyle.style,
1238
1266
  "data-layer-id": layer.id,
1239
1267
  children: [
1240
- /* @__PURE__ */ jsx7("defs", { children: /* @__PURE__ */ jsx7("clipPath", { id: `clip-${layer.id}`, children: /* @__PURE__ */ jsx7("rect", { x: finalX, y: finalY, width, height }) }) }),
1241
- /* @__PURE__ */ jsx7("g", { clipPath: `url(#clip-${layer.id})`, children: /* @__PURE__ */ jsx7(
1268
+ /* @__PURE__ */ jsx7("defs", { children: /* @__PURE__ */ jsx7("clipPath", { id: clipId, children: /* @__PURE__ */ jsx7("rect", { x: finalX, y: finalY, width, height }) }) }),
1269
+ /* @__PURE__ */ jsx7("g", { clipPath: `url(#${clipId})`, children: /* @__PURE__ */ jsx7(
1242
1270
  "image",
1243
1271
  {
1244
1272
  href: mapImageUrl,
@@ -1258,6 +1286,7 @@ function MapLayer({ layer, basePath, viewport, blockTime }) {
1258
1286
  // src/layers/VideoLayer.tsx
1259
1287
  import { useRef as useRef2, useEffect as useEffect4 } from "react";
1260
1288
  import { jsx as jsx8 } from "react/jsx-runtime";
1289
+ var VIDEO_SYNC_DRIFT_SECONDS = 0.2;
1261
1290
  function VideoLayer({ layer, basePath, viewport, blockTime, isPlaying }) {
1262
1291
  const { content, position } = layer;
1263
1292
  const videoRef = useRef2(null);
@@ -1297,16 +1326,22 @@ function VideoLayer({ layer, basePath, viewport, blockTime, isPlaying }) {
1297
1326
  video.removeEventListener("timeupdate", handleTimeUpdate);
1298
1327
  video.pause();
1299
1328
  };
1300
- }, [content.src, content.clipStart, content.clipEnd]);
1329
+ }, [src, content.clipStart, content.clipEnd]);
1301
1330
  useEffect4(() => {
1302
1331
  const video = videoRef.current;
1303
1332
  if (!video || !hasStartedRef.current) return;
1333
+ const targetTime = gated ? content.clipStart : Math.min(content.clipEnd, content.clipStart + Math.max(0, blockTime - startAt));
1334
+ if (Math.abs(video.currentTime - targetTime) > VIDEO_SYNC_DRIFT_SECONDS) {
1335
+ video.currentTime = targetTime;
1336
+ }
1304
1337
  if (gated) {
1305
1338
  video.pause();
1306
- video.currentTime = content.clipStart;
1307
1339
  return;
1308
1340
  }
1309
- if (video.currentTime >= content.clipEnd) return;
1341
+ if (targetTime >= content.clipEnd) {
1342
+ video.pause();
1343
+ return;
1344
+ }
1310
1345
  if (isPlaying) {
1311
1346
  const playPromise = video.play();
1312
1347
  if (playPromise) {
@@ -1316,7 +1351,7 @@ function VideoLayer({ layer, basePath, viewport, blockTime, isPlaying }) {
1316
1351
  } else {
1317
1352
  video.pause();
1318
1353
  }
1319
- }, [isPlaying, gated, content.clipStart, content.clipEnd]);
1354
+ }, [isPlaying, gated, blockTime, startAt, src, content.clipStart, content.clipEnd]);
1320
1355
  return /* @__PURE__ */ jsx8("g", { className: "block-layer block-layer--video", "data-layer-id": layer.id, children: /* @__PURE__ */ jsx8("foreignObject", { x: finalX, y: finalY, width, height, children: /* @__PURE__ */ jsx8(
1321
1356
  "video",
1322
1357
  {
@@ -1425,9 +1460,137 @@ function TableLayer({ layer, viewport, blockTime }) {
1425
1460
  ) });
1426
1461
  }
1427
1462
 
1428
- // src/BlockRenderer.tsx
1463
+ // src/layers/TreeLayer.tsx
1464
+ import { useState as useState3 } from "react";
1429
1465
  import { jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
1430
- var VIEWPORT = {
1466
+ function faClass(token, fallback) {
1467
+ const name = token && token.trim() ? token.trim() : fallback;
1468
+ const colon = name.indexOf(":");
1469
+ if (colon > 0) {
1470
+ const family = name.slice(0, colon).replace(/^fa-/, "");
1471
+ return `fa-${family} fa-${name.slice(colon + 1)}`;
1472
+ }
1473
+ return `fa-solid fa-${name}`;
1474
+ }
1475
+ function TreeLayer({ layer, viewport, blockTime }) {
1476
+ const { content, position, animation } = layer;
1477
+ const { items, style } = content;
1478
+ const x = resolveValue(position.x, viewport.width);
1479
+ const y = resolveValue(position.y, viewport.height);
1480
+ const width = position.width ? resolveValue(position.width, viewport.width) : viewport.width;
1481
+ const height = position.height ? resolveValue(position.height, viewport.height) : viewport.height;
1482
+ const offset = getAnchorOffset(position.anchor, width, height);
1483
+ const animStyle = animation ? getAnimationStyle(animation, blockTime) : {};
1484
+ return /* @__PURE__ */ jsx10(
1485
+ "foreignObject",
1486
+ {
1487
+ x: x + offset.x,
1488
+ y: y + offset.y,
1489
+ width,
1490
+ height,
1491
+ style: animStyle,
1492
+ children: /* @__PURE__ */ jsx10(
1493
+ "div",
1494
+ {
1495
+ ...{ xmlns: "http://www.w3.org/1999/xhtml" },
1496
+ className: "squisq-treelayer",
1497
+ style: {
1498
+ width: `${width}px`,
1499
+ height: `${height}px`,
1500
+ display: "flex",
1501
+ flexDirection: "column",
1502
+ justifyContent: "center",
1503
+ padding: "24px 32px",
1504
+ boxSizing: "border-box",
1505
+ fontFamily: style.fontFamily ?? "system-ui, sans-serif",
1506
+ fontSize: `${style.fontSize}px`,
1507
+ lineHeight: 1.7,
1508
+ overflow: "hidden"
1509
+ },
1510
+ children: /* @__PURE__ */ jsx10(TreeList, { items, depth: 0, style })
1511
+ }
1512
+ )
1513
+ }
1514
+ );
1515
+ }
1516
+ function TreeList({
1517
+ items,
1518
+ depth,
1519
+ style
1520
+ }) {
1521
+ return /* @__PURE__ */ jsx10(
1522
+ "ul",
1523
+ {
1524
+ style: {
1525
+ listStyle: "none",
1526
+ margin: 0,
1527
+ padding: 0,
1528
+ paddingLeft: depth === 0 ? 0 : `${style.indentPx}px`,
1529
+ borderLeft: depth === 0 ? "none" : `1px solid ${style.connectorColor}`
1530
+ },
1531
+ children: items.map((item) => /* @__PURE__ */ jsx10(TreeRow, { item, style }, item.id))
1532
+ }
1533
+ );
1534
+ }
1535
+ function TreeRow({
1536
+ item,
1537
+ style
1538
+ }) {
1539
+ const hasChildren = item.children.length > 0;
1540
+ const [collapsed, setCollapsed] = useState3(false);
1541
+ const isDir = item.isDir || hasChildren;
1542
+ const iconCls = isDir ? faClass(style.folderIcon, collapsed ? "folder" : "folder-open") : faClass(style.fileIcon, "file");
1543
+ return /* @__PURE__ */ jsxs7("li", { style: { position: "relative" }, children: [
1544
+ /* @__PURE__ */ jsxs7("div", { style: { display: "flex", alignItems: "baseline", gap: "8px", padding: "1px 0" }, children: [
1545
+ hasChildren ? /* @__PURE__ */ jsx10(
1546
+ "button",
1547
+ {
1548
+ type: "button",
1549
+ "aria-label": collapsed ? "Expand" : "Collapse",
1550
+ onClick: () => setCollapsed((c) => !c),
1551
+ style: {
1552
+ flex: "0 0 auto",
1553
+ width: "1em",
1554
+ border: "none",
1555
+ background: "transparent",
1556
+ cursor: "pointer",
1557
+ color: style.connectorColor,
1558
+ padding: 0,
1559
+ fontSize: "0.8em"
1560
+ },
1561
+ children: /* @__PURE__ */ jsx10(
1562
+ "i",
1563
+ {
1564
+ className: `fa-solid ${collapsed ? "fa-chevron-right" : "fa-chevron-down"}`,
1565
+ "aria-hidden": "true"
1566
+ }
1567
+ )
1568
+ }
1569
+ ) : /* @__PURE__ */ jsx10("span", { style: { flex: "0 0 auto", width: "1em" } }),
1570
+ /* @__PURE__ */ jsx10(
1571
+ "i",
1572
+ {
1573
+ className: iconCls,
1574
+ "aria-hidden": "true",
1575
+ style: { flex: "0 0 auto", color: style.iconColor, width: "1.2em", textAlign: "center" }
1576
+ }
1577
+ ),
1578
+ /* @__PURE__ */ jsx10(
1579
+ "span",
1580
+ {
1581
+ style: { color: isDir ? style.dirColor : style.rowColor, fontWeight: isDir ? 600 : 400 },
1582
+ children: item.label
1583
+ }
1584
+ ),
1585
+ item.comment ? /* @__PURE__ */ jsx10("span", { style: { color: style.commentColor, fontSize: "0.85em", fontStyle: "italic" }, children: item.comment }) : null
1586
+ ] }),
1587
+ hasChildren && !collapsed ? /* @__PURE__ */ jsx10(TreeList, { items: item.children, depth: 1, style }) : null
1588
+ ] });
1589
+ }
1590
+
1591
+ // src/BlockRenderer.tsx
1592
+ import { jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
1593
+ var DEFAULT_VIEWPORT = {
1431
1594
  width: 1920,
1432
1595
  height: 1080
1433
1596
  };
@@ -1438,12 +1601,13 @@ function BlockRenderer({
1438
1601
  isEntering = false,
1439
1602
  isExiting = false,
1440
1603
  transition,
1441
- viewport = VIEWPORT,
1442
- isPlaying
1604
+ viewport = DEFAULT_VIEWPORT,
1605
+ isPlaying,
1606
+ animationsEnabled = true
1443
1607
  }) {
1444
1608
  let transitionClass = "";
1445
1609
  const transitionStyle = {};
1446
- const activeTransition = transition ?? block.transition;
1610
+ const activeTransition = animationsEnabled ? transition ?? block.transition : void 0;
1447
1611
  if (activeTransition && isEntering) {
1448
1612
  transitionClass = getTransitionClass(activeTransition.type, true, activeTransition.direction);
1449
1613
  transitionStyle["--transition-duration"] = `${resolveTransitionDuration(activeTransition)}s`;
@@ -1451,8 +1615,9 @@ function BlockRenderer({
1451
1615
  transitionClass = getTransitionClass(activeTransition.type, false, activeTransition.direction);
1452
1616
  transitionStyle["--transition-duration"] = `${resolveTransitionDuration(activeTransition)}s`;
1453
1617
  }
1454
- const clipId = `vb-clip-${block.id}`;
1455
- return /* @__PURE__ */ jsxs7(
1618
+ const instanceId = useId5().replace(/:/g, "");
1619
+ const clipId = `vb-clip-${instanceId}-${block.id}`;
1620
+ return /* @__PURE__ */ jsxs8(
1456
1621
  "svg",
1457
1622
  {
1458
1623
  className: `block-svg ${transitionClass}`,
@@ -1462,15 +1627,16 @@ function BlockRenderer({
1462
1627
  overflow: "hidden",
1463
1628
  "data-block-id": block.id,
1464
1629
  children: [
1465
- /* @__PURE__ */ jsx10("defs", { children: /* @__PURE__ */ jsx10("clipPath", { id: clipId, children: /* @__PURE__ */ jsx10("rect", { x: "0", y: "0", width: viewport.width, height: viewport.height }) }) }),
1466
- /* @__PURE__ */ jsx10("g", { clipPath: `url(#${clipId})`, children: (block.layers ?? []).map((layer) => /* @__PURE__ */ jsx10(
1630
+ /* @__PURE__ */ jsx11("defs", { children: /* @__PURE__ */ jsx11("clipPath", { id: clipId, children: /* @__PURE__ */ jsx11("rect", { x: "0", y: "0", width: viewport.width, height: viewport.height }) }) }),
1631
+ /* @__PURE__ */ jsx11("g", { clipPath: `url(#${clipId})`, children: (block.layers ?? []).map((layer) => /* @__PURE__ */ jsx11(
1467
1632
  LayerRenderer,
1468
1633
  {
1469
1634
  layer,
1470
1635
  basePath,
1471
1636
  viewport,
1472
1637
  blockTime,
1473
- isPlaying
1638
+ isPlaying,
1639
+ animationsEnabled
1474
1640
  },
1475
1641
  layer.id
1476
1642
  )) })
@@ -1478,23 +1644,47 @@ function BlockRenderer({
1478
1644
  }
1479
1645
  );
1480
1646
  }
1481
- function LayerRenderer({ layer, basePath, viewport, blockTime, isPlaying }) {
1482
- switch (layer.type) {
1647
+ function LayerRenderer({
1648
+ layer,
1649
+ basePath,
1650
+ viewport,
1651
+ blockTime,
1652
+ isPlaying,
1653
+ animationsEnabled
1654
+ }) {
1655
+ const renderedLayer = animationsEnabled || !layer.animation ? layer : { ...layer, animation: void 0 };
1656
+ switch (renderedLayer.type) {
1483
1657
  case "image":
1484
- return /* @__PURE__ */ jsx10(ImageLayer, { layer, basePath, viewport, blockTime });
1658
+ return /* @__PURE__ */ jsx11(
1659
+ ImageLayer,
1660
+ {
1661
+ layer: renderedLayer,
1662
+ basePath,
1663
+ viewport,
1664
+ blockTime
1665
+ }
1666
+ );
1485
1667
  case "text":
1486
- return /* @__PURE__ */ jsx10(TextLayer, { layer, viewport, blockTime });
1668
+ return /* @__PURE__ */ jsx11(TextLayer, { layer: renderedLayer, viewport, blockTime });
1487
1669
  case "shape":
1488
- return /* @__PURE__ */ jsx10(ShapeLayer, { layer, viewport, blockTime });
1670
+ return /* @__PURE__ */ jsx11(ShapeLayer, { layer: renderedLayer, viewport, blockTime });
1489
1671
  case "path":
1490
- return /* @__PURE__ */ jsx10(PathLayer, { layer, viewport, blockTime });
1672
+ return /* @__PURE__ */ jsx11(PathLayer, { layer: renderedLayer, viewport, blockTime });
1491
1673
  case "map":
1492
- return /* @__PURE__ */ jsx10(MapLayer, { layer, basePath, viewport, blockTime });
1674
+ return /* @__PURE__ */ jsx11(
1675
+ MapLayer,
1676
+ {
1677
+ layer: renderedLayer,
1678
+ basePath,
1679
+ viewport,
1680
+ blockTime
1681
+ }
1682
+ );
1493
1683
  case "video":
1494
- return /* @__PURE__ */ jsx10(
1684
+ return /* @__PURE__ */ jsx11(
1495
1685
  VideoLayer,
1496
1686
  {
1497
- layer,
1687
+ layer: renderedLayer,
1498
1688
  basePath,
1499
1689
  viewport,
1500
1690
  blockTime,
@@ -1502,9 +1692,11 @@ function LayerRenderer({ layer, basePath, viewport, blockTime, isPlaying }) {
1502
1692
  }
1503
1693
  );
1504
1694
  case "table":
1505
- return /* @__PURE__ */ jsx10(TableLayer, { layer, viewport, blockTime });
1695
+ return /* @__PURE__ */ jsx11(TableLayer, { layer: renderedLayer, viewport, blockTime });
1696
+ case "tree":
1697
+ return /* @__PURE__ */ jsx11(TreeLayer, { layer: renderedLayer, viewport, blockTime });
1506
1698
  default:
1507
- console.warn(`Unknown layer type: ${layer.type}`);
1699
+ console.warn(`Unknown layer type: ${renderedLayer.type}`);
1508
1700
  return null;
1509
1701
  }
1510
1702
  }
@@ -1515,7 +1707,7 @@ import { getCaptionAtTime } from "@bendyline/squisq/schemas";
1515
1707
  // src/SocialCaptionOverlay.tsx
1516
1708
  import { useMemo as useMemo4 } from "react";
1517
1709
  import { resolveFontFamily } from "@bendyline/squisq/schemas";
1518
- import { jsx as jsx11 } from "react/jsx-runtime";
1710
+ import { jsx as jsx12 } from "react/jsx-runtime";
1519
1711
  var TARGET_CHUNK_SIZE = 4;
1520
1712
  var MIN_CHUNK_SIZE = 2;
1521
1713
  var MAX_CHUNK_SIZE = 6;
@@ -1574,7 +1766,7 @@ function SocialCaptionOverlay({
1574
1766
  [captions]
1575
1767
  );
1576
1768
  if (!enabled || chunks.length === 0) {
1577
- return /* @__PURE__ */ jsx11(
1769
+ return /* @__PURE__ */ jsx12(
1578
1770
  "div",
1579
1771
  {
1580
1772
  className: "social-caption-overlay",
@@ -1636,7 +1828,7 @@ function SocialCaptionOverlay({
1636
1828
  const viewportHeight = viewport?.height ?? 720;
1637
1829
  const baseFontSize = Math.round(viewportHeight * 0.055);
1638
1830
  const fontSize = Math.max(24, Math.min(72, baseFontSize));
1639
- return /* @__PURE__ */ jsx11(
1831
+ return /* @__PURE__ */ jsx12(
1640
1832
  "div",
1641
1833
  {
1642
1834
  className: "social-caption-overlay",
@@ -1653,7 +1845,7 @@ function SocialCaptionOverlay({
1653
1845
  opacity: 1,
1654
1846
  transition: "opacity 0.15s ease-in-out"
1655
1847
  },
1656
- children: /* @__PURE__ */ jsx11(
1848
+ children: /* @__PURE__ */ jsx12(
1657
1849
  "div",
1658
1850
  {
1659
1851
  style: {
@@ -1662,7 +1854,7 @@ function SocialCaptionOverlay({
1662
1854
  },
1663
1855
  children: activeChunk.words.map((word, i) => {
1664
1856
  const isActive = i === activeWordIndex;
1665
- return /* @__PURE__ */ jsx11(
1857
+ return /* @__PURE__ */ jsx12(
1666
1858
  "span",
1667
1859
  {
1668
1860
  style: {
@@ -1687,7 +1879,7 @@ function SocialCaptionOverlay({
1687
1879
  }
1688
1880
 
1689
1881
  // src/CaptionOverlay.tsx
1690
- import { jsx as jsx12 } from "react/jsx-runtime";
1882
+ import { jsx as jsx13 } from "react/jsx-runtime";
1691
1883
  function CaptionOverlay({
1692
1884
  captions,
1693
1885
  currentTime,
@@ -1698,7 +1890,7 @@ function CaptionOverlay({
1698
1890
  viewport
1699
1891
  }) {
1700
1892
  if (captionStyle === "social") {
1701
- return /* @__PURE__ */ jsx12(
1893
+ return /* @__PURE__ */ jsx13(
1702
1894
  SocialCaptionOverlay,
1703
1895
  {
1704
1896
  captions,
@@ -1711,7 +1903,7 @@ function CaptionOverlay({
1711
1903
  }
1712
1904
  const phrase = enabled && captions ? getCaptionAtTime(captions, currentTime) : null;
1713
1905
  const captionText = phrase?.text ?? null;
1714
- return /* @__PURE__ */ jsx12(
1906
+ return /* @__PURE__ */ jsx13(
1715
1907
  "div",
1716
1908
  {
1717
1909
  className: "caption-overlay",
@@ -1730,7 +1922,7 @@ function CaptionOverlay({
1730
1922
  padding: "0 4px",
1731
1923
  boxSizing: "border-box"
1732
1924
  },
1733
- children: captionText && /* @__PURE__ */ jsx12(
1925
+ children: captionText && /* @__PURE__ */ jsx13(
1734
1926
  "div",
1735
1927
  {
1736
1928
  style: {
@@ -1740,7 +1932,7 @@ function CaptionOverlay({
1740
1932
  borderRadius: "4px",
1741
1933
  backdropFilter: "blur(4px)"
1742
1934
  },
1743
- children: /* @__PURE__ */ jsx12(
1935
+ children: /* @__PURE__ */ jsx13(
1744
1936
  "span",
1745
1937
  {
1746
1938
  style: {
@@ -1786,22 +1978,40 @@ function useAutoSurface(enabled) {
1786
1978
  }
1787
1979
 
1788
1980
  // src/hooks/useAudioSync.ts
1789
- import { useState as useState3, useEffect as useEffect5, useRef as useRef3, useCallback as useCallback2 } from "react";
1790
- function useAudioSync(audioRef, audioTrack, basePath = "") {
1791
- const [currentTime, setCurrentTime] = useState3(0);
1792
- const [isPlaying, setIsPlaying] = useState3(false);
1793
- const [currentSegment, setCurrentSegment] = useState3(0);
1794
- const [isEnded, setIsEnded] = useState3(false);
1795
- const [isAudioReady, setIsAudioReady] = useState3(false);
1796
- const [totalDuration, setTotalDuration] = useState3(0);
1981
+ import { useState as useState4, useEffect as useEffect5, useRef as useRef3, useCallback as useCallback2 } from "react";
1982
+ function resolveAudioUrl(src, basePath) {
1983
+ if (!src || /^(?:[a-z][a-z0-9+.-]*:|\/\/|\/)/i.test(src)) return src;
1984
+ if (!basePath) return src;
1985
+ return `${basePath.replace(/\/$/, "")}/${src.replace(/^\//, "")}`;
1986
+ }
1987
+ function useAudioSync(audioRef, audioTrack, basePath = "", enabled = true) {
1988
+ const [currentTime, setCurrentTime] = useState4(0);
1989
+ const [isPlaying, setIsPlaying] = useState4(false);
1990
+ const [currentSegment, setCurrentSegment] = useState4(0);
1991
+ const [isEnded, setIsEnded] = useState4(false);
1992
+ const [isAudioReady, setIsAudioReady] = useState4(false);
1993
+ const [totalDuration, setTotalDuration] = useState4(0);
1797
1994
  const segmentStarts = useRef3([]);
1798
1995
  const pendingSeekTime = useRef3(null);
1799
1996
  const shouldPlayAfterLoad = useRef3(false);
1800
1997
  const blobUrls = useRef3(/* @__PURE__ */ new Map());
1801
1998
  const loadingPromises = useRef3(/* @__PURE__ */ new Map());
1999
+ const abortControllers = useRef3(/* @__PURE__ */ new Set());
2000
+ const loadGeneration = useRef3(0);
1802
2001
  const fallbackMode = useRef3(false);
1803
2002
  useEffect5(() => {
1804
- if (!audioTrack?.segments) {
2003
+ loadGeneration.current += 1;
2004
+ pendingSeekTime.current = null;
2005
+ shouldPlayAfterLoad.current = false;
2006
+ fallbackMode.current = false;
2007
+ setCurrentTime(0);
2008
+ setCurrentSegment(0);
2009
+ setIsPlaying(false);
2010
+ setIsEnded(false);
2011
+ setIsAudioReady(false);
2012
+ if (!enabled || !audioTrack?.segments) {
2013
+ segmentStarts.current = [];
2014
+ setTotalDuration(0);
1805
2015
  return;
1806
2016
  }
1807
2017
  let time = 0;
@@ -1811,27 +2021,35 @@ function useAudioSync(audioRef, audioTrack, basePath = "") {
1811
2021
  return start;
1812
2022
  });
1813
2023
  setTotalDuration(time);
1814
- }, [audioTrack]);
2024
+ }, [audioTrack, enabled]);
1815
2025
  const preloadAudio = useCallback2(
1816
2026
  async (src) => {
1817
- const audioUrl = basePath ? `${basePath}/${src}` : src;
2027
+ const audioUrl = resolveAudioUrl(src, basePath);
1818
2028
  if (blobUrls.current.has(src)) {
1819
2029
  return blobUrls.current.get(src);
1820
2030
  }
1821
2031
  if (loadingPromises.current.has(src)) {
1822
2032
  return loadingPromises.current.get(src);
1823
2033
  }
2034
+ const controller = new AbortController();
2035
+ abortControllers.current.add(controller);
2036
+ const generation = loadGeneration.current;
1824
2037
  const loadPromise = (async () => {
1825
2038
  try {
1826
- const response = await fetch(audioUrl);
2039
+ const response = await fetch(audioUrl, { signal: controller.signal });
1827
2040
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
1828
2041
  const blob = await response.blob();
1829
2042
  const blobUrl = URL.createObjectURL(blob);
2043
+ if (controller.signal.aborted || generation !== loadGeneration.current) {
2044
+ URL.revokeObjectURL(blobUrl);
2045
+ return audioUrl;
2046
+ }
1830
2047
  blobUrls.current.set(src, blobUrl);
1831
2048
  return blobUrl;
1832
2049
  } catch {
1833
2050
  return audioUrl;
1834
2051
  } finally {
2052
+ abortControllers.current.delete(controller);
1835
2053
  loadingPromises.current.delete(src);
1836
2054
  }
1837
2055
  })();
@@ -1841,19 +2059,26 @@ function useAudioSync(audioRef, audioTrack, basePath = "") {
1841
2059
  [basePath]
1842
2060
  );
1843
2061
  useEffect5(() => {
1844
- if (!audioTrack?.segments) return;
2062
+ if (!enabled || !audioTrack?.segments) return;
1845
2063
  audioTrack.segments.forEach((segment) => {
1846
2064
  preloadAudio(segment.src);
1847
2065
  });
1848
2066
  const currentBlobUrls = blobUrls.current;
2067
+ const currentAbortControllers = abortControllers.current;
2068
+ const currentLoadingPromises = loadingPromises.current;
1849
2069
  return () => {
2070
+ loadGeneration.current += 1;
2071
+ currentAbortControllers.forEach((controller) => controller.abort());
2072
+ currentAbortControllers.clear();
2073
+ currentLoadingPromises.clear();
1850
2074
  currentBlobUrls.forEach((url) => {
1851
2075
  URL.revokeObjectURL(url);
1852
2076
  });
1853
2077
  currentBlobUrls.clear();
1854
2078
  };
1855
- }, [audioTrack, preloadAudio]);
2079
+ }, [audioTrack, preloadAudio, enabled]);
1856
2080
  useEffect5(() => {
2081
+ if (!enabled) return;
1857
2082
  const audio = audioRef.current;
1858
2083
  if (!audio) return;
1859
2084
  const handleTimeUpdate = () => {
@@ -1890,8 +2115,9 @@ function useAudioSync(audioRef, audioTrack, basePath = "") {
1890
2115
  audio.removeEventListener("ended", handleEnded);
1891
2116
  audio.removeEventListener("error", handleError);
1892
2117
  };
1893
- }, [audioRef, currentSegment, audioTrack]);
2118
+ }, [audioRef, currentSegment, audioTrack, enabled]);
1894
2119
  useEffect5(() => {
2120
+ if (!enabled) return;
1895
2121
  const audio = audioRef.current;
1896
2122
  if (!audio || !audioTrack?.segments) return;
1897
2123
  const segment = audioTrack.segments[currentSegment];
@@ -1913,29 +2139,37 @@ function useAudioSync(audioRef, audioTrack, basePath = "") {
1913
2139
  const currentSrc = audio.src;
1914
2140
  const cachedBlobUrl = blobUrls.current.get(segment.src);
1915
2141
  const isSameSource = currentSrc && (currentSrc === cachedBlobUrl || currentSrc.endsWith(segment.src));
2142
+ let cancelled = false;
2143
+ let handleCanPlay = null;
1916
2144
  if (!isSameSource) {
1917
2145
  const loadAndPlay = async () => {
1918
2146
  const blobUrl = await preloadAudio(segment.src);
1919
- const handleCanPlay = () => {
2147
+ if (cancelled) return;
2148
+ handleCanPlay = () => {
2149
+ if (cancelled) return;
1920
2150
  setIsAudioReady(true);
1921
2151
  applyPendingSeek();
1922
- audio.removeEventListener("canplay", handleCanPlay);
2152
+ if (handleCanPlay) audio.removeEventListener("canplay", handleCanPlay);
1923
2153
  };
1924
2154
  audio.addEventListener("canplay", handleCanPlay);
1925
2155
  audio.src = blobUrl;
1926
2156
  audio.load();
1927
2157
  await Promise.resolve();
1928
2158
  if (audio.readyState >= 3) {
1929
- audio.removeEventListener("canplay", handleCanPlay);
2159
+ if (handleCanPlay) audio.removeEventListener("canplay", handleCanPlay);
1930
2160
  setIsAudioReady(true);
1931
2161
  applyPendingSeek();
1932
2162
  }
1933
2163
  };
1934
- loadAndPlay();
2164
+ void loadAndPlay();
1935
2165
  } else {
1936
2166
  applyPendingSeek();
1937
2167
  }
1938
- }, [audioRef, currentSegment, audioTrack, preloadAudio]);
2168
+ return () => {
2169
+ cancelled = true;
2170
+ if (handleCanPlay) audio.removeEventListener("canplay", handleCanPlay);
2171
+ };
2172
+ }, [audioRef, currentSegment, audioTrack, preloadAudio, enabled]);
1939
2173
  const play = useCallback2(() => {
1940
2174
  const audio = audioRef.current;
1941
2175
  if (audio) {
@@ -2072,8 +2306,8 @@ import {
2072
2306
  resolvePersistentLayers,
2073
2307
  VIEWPORT_PRESETS
2074
2308
  } from "@bendyline/squisq/doc";
2075
- function useDocPlayback(script, currentTime, viewport = VIEWPORT_PRESETS.landscape, renderMode = false, theme) {
2076
- void renderMode;
2309
+ function useDocPlayback(script, currentTime, options = {}) {
2310
+ const { viewport = VIEWPORT_PRESETS.landscape, theme, onSeek } = options;
2077
2311
  const blocks = useMemo6(() => {
2078
2312
  if (!script?.blocks) {
2079
2313
  return [];
@@ -2136,8 +2370,19 @@ function useDocPlayback(script, currentTime, viewport = VIEWPORT_PRESETS.landsca
2136
2370
  const outgoingBlockRef = useRef4(null);
2137
2371
  const activeBlockIdRef = useRef4(null);
2138
2372
  const lastRenderedBlockRef = useRef4(null);
2373
+ const suppressOutgoingTargetRef = useRef4(null);
2374
+ const suppressOutgoingForNextBlock = useCallback3((blockId) => {
2375
+ if (activeBlockIdRef.current === blockId) {
2376
+ outgoingBlockRef.current = null;
2377
+ suppressOutgoingTargetRef.current = null;
2378
+ return;
2379
+ }
2380
+ suppressOutgoingTargetRef.current = blockId;
2381
+ }, []);
2139
2382
  if (currentBlock && currentBlock.id !== activeBlockIdRef.current) {
2140
- outgoingBlockRef.current = lastRenderedBlockRef.current;
2383
+ const suppressOutgoing = suppressOutgoingTargetRef.current === currentBlock.id;
2384
+ outgoingBlockRef.current = suppressOutgoing ? null : lastRenderedBlockRef.current;
2385
+ suppressOutgoingTargetRef.current = null;
2141
2386
  activeBlockIdRef.current = currentBlock.id;
2142
2387
  }
2143
2388
  lastRenderedBlockRef.current = currentBlock;
@@ -2151,10 +2396,10 @@ function useDocPlayback(script, currentTime, viewport = VIEWPORT_PRESETS.landsca
2151
2396
  if (!script || index < 0 || index >= blocks.length) return;
2152
2397
  const targetBlock = blocks[index];
2153
2398
  if (targetBlock) {
2154
- return targetBlock.startTime;
2399
+ onSeek?.(targetBlock.startTime);
2155
2400
  }
2156
2401
  },
2157
- [script, blocks]
2402
+ [script, blocks, onSeek]
2158
2403
  );
2159
2404
  const nextBlock = useCallback3(() => {
2160
2405
  if (currentBlockIndex < blocks.length - 1) {
@@ -2178,13 +2423,14 @@ function useDocPlayback(script, currentTime, viewport = VIEWPORT_PRESETS.landsca
2178
2423
  nextBlock,
2179
2424
  prevBlock,
2180
2425
  goToBlock,
2426
+ suppressOutgoingForNextBlock,
2181
2427
  /** Expanded blocks (templates converted to full blocks with layers) */
2182
2428
  blocks
2183
2429
  };
2184
2430
  }
2185
2431
 
2186
2432
  // src/hooks/useViewportOrientation.ts
2187
- import { useState as useState4, useEffect as useEffect6, useMemo as useMemo7 } from "react";
2433
+ import { useState as useState5, useEffect as useEffect6, useMemo as useMemo7 } from "react";
2188
2434
  import {
2189
2435
  VIEWPORT_PRESETS as VIEWPORT_PRESETS2
2190
2436
  } from "@bendyline/squisq/doc";
@@ -2195,7 +2441,7 @@ function getOrientationFromWindow(width, height) {
2195
2441
  } else if (ratio < 0.83) {
2196
2442
  return "portrait";
2197
2443
  } else {
2198
- return "landscape";
2444
+ return "square";
2199
2445
  }
2200
2446
  }
2201
2447
  function getViewportForOrientation(orientation) {
@@ -2210,7 +2456,7 @@ function getViewportForOrientation(orientation) {
2210
2456
  }
2211
2457
  }
2212
2458
  function useViewportOrientation() {
2213
- const [windowSize, setWindowSize] = useState4(() => ({
2459
+ const [windowSize, setWindowSize] = useState5(() => ({
2214
2460
  width: typeof window !== "undefined" ? window.innerWidth : 1920,
2215
2461
  height: typeof window !== "undefined" ? window.innerHeight : 1080
2216
2462
  }));
@@ -2246,7 +2492,7 @@ function useViewportOrientation() {
2246
2492
  }
2247
2493
 
2248
2494
  // src/hooks/useSlideSwipe.ts
2249
- import { useCallback as useCallback4, useEffect as useEffect7, useRef as useRef5, useState as useState5 } from "react";
2495
+ import { useCallback as useCallback4, useEffect as useEffect7, useRef as useRef5, useState as useState6 } from "react";
2250
2496
  var DISTANCE_RATIO = 0.3;
2251
2497
  var FLICK_VELOCITY = 0.5;
2252
2498
  var MIN_FLICK_DISTANCE = 12;
@@ -2270,8 +2516,8 @@ function decideSwipe({
2270
2516
  }
2271
2517
  function useSlideSwipe(opts) {
2272
2518
  const settleMs = opts.settleMs ?? DEFAULT_SETTLE_MS;
2273
- const [offsetPx, setOffsetPx] = useState5(0);
2274
- const [phase, setPhase] = useState5("idle");
2519
+ const [offsetPx, setOffsetPx] = useState6(0);
2520
+ const [phase, setPhase] = useState6("idle");
2275
2521
  const optsRef = useRef5(opts);
2276
2522
  optsRef.current = opts;
2277
2523
  const dragRef = useRef5(null);
@@ -2402,7 +2648,7 @@ import {
2402
2648
  import { parseMarkdown as parseMarkdown2 } from "@bendyline/squisq/markdown";
2403
2649
 
2404
2650
  // src/DocProgressBar.tsx
2405
- import { useRef as useRef6, useState as useState6, useCallback as useCallback5 } from "react";
2651
+ import { useRef as useRef6, useState as useState7, useCallback as useCallback5 } from "react";
2406
2652
 
2407
2653
  // src/types.ts
2408
2654
  function formatTime(seconds) {
@@ -2412,7 +2658,7 @@ function formatTime(seconds) {
2412
2658
  }
2413
2659
 
2414
2660
  // src/DocProgressBar.tsx
2415
- import { jsx as jsx13, jsxs as jsxs8 } from "react/jsx-runtime";
2661
+ import { jsx as jsx14, jsxs as jsxs9 } from "react/jsx-runtime";
2416
2662
  function DocProgressBar({
2417
2663
  state,
2418
2664
  actions,
@@ -2421,7 +2667,7 @@ function DocProgressBar({
2421
2667
  getBlockTitle
2422
2668
  }) {
2423
2669
  const progressBarRef = useRef6(null);
2424
- const [hoverPosition, setHoverPosition] = useState6(null);
2670
+ const [hoverPosition, setHoverPosition] = useState7(null);
2425
2671
  const playProgress = state.totalDuration > 0 ? Math.max(0, Math.min(1, state.currentTime / state.totalDuration)) : 0;
2426
2672
  const handleProgressHover = useCallback5((e) => {
2427
2673
  const bar = progressBarRef.current;
@@ -2434,6 +2680,31 @@ function DocProgressBar({
2434
2680
  const handleProgressLeave = useCallback5(() => {
2435
2681
  setHoverPosition(null);
2436
2682
  }, []);
2683
+ const handleProgressKeyDown = useCallback5(
2684
+ (e) => {
2685
+ let next = null;
2686
+ switch (e.key) {
2687
+ case "ArrowLeft":
2688
+ case "ArrowDown":
2689
+ next = state.currentTime - 5;
2690
+ break;
2691
+ case "ArrowRight":
2692
+ case "ArrowUp":
2693
+ next = state.currentTime + 5;
2694
+ break;
2695
+ case "Home":
2696
+ next = 0;
2697
+ break;
2698
+ case "End":
2699
+ next = state.totalDuration;
2700
+ break;
2701
+ }
2702
+ if (next == null) return;
2703
+ e.preventDefault();
2704
+ actions.seekTo(Math.max(0, Math.min(state.totalDuration, next)));
2705
+ },
2706
+ [actions, state.currentTime, state.totalDuration]
2707
+ );
2437
2708
  const getBlockAtTimeLocal = useCallback5(
2438
2709
  (time) => {
2439
2710
  for (let i = expandedBlocks.length - 1; i >= 0; i--) {
@@ -2446,10 +2717,12 @@ function DocProgressBar({
2446
2717
  },
2447
2718
  [expandedBlocks]
2448
2719
  );
2449
- return /* @__PURE__ */ jsxs8(
2720
+ return /* @__PURE__ */ jsxs9(
2450
2721
  "div",
2451
2722
  {
2452
2723
  ref: progressBarRef,
2724
+ role: "group",
2725
+ "aria-label": "Playback timeline",
2453
2726
  style: {
2454
2727
  flex: 1,
2455
2728
  height: "24px",
@@ -2467,9 +2740,17 @@ function DocProgressBar({
2467
2740
  onMouseMove: handleProgressHover,
2468
2741
  onMouseLeave: handleProgressLeave,
2469
2742
  children: [
2470
- /* @__PURE__ */ jsx13(
2743
+ /* @__PURE__ */ jsx14(
2471
2744
  "div",
2472
2745
  {
2746
+ role: "slider",
2747
+ tabIndex: 0,
2748
+ "aria-label": "Playback position",
2749
+ "aria-valuemin": 0,
2750
+ "aria-valuemax": state.totalDuration,
2751
+ "aria-valuenow": Math.max(0, Math.min(state.totalDuration, state.currentTime)),
2752
+ "aria-valuetext": `${formatTime(state.currentTime)} of ${formatTime(state.totalDuration)}`,
2753
+ onKeyDown: handleProgressKeyDown,
2473
2754
  style: {
2474
2755
  position: "absolute",
2475
2756
  left: 0,
@@ -2480,7 +2761,7 @@ function DocProgressBar({
2480
2761
  }
2481
2762
  }
2482
2763
  ),
2483
- /* @__PURE__ */ jsx13(
2764
+ /* @__PURE__ */ jsx14(
2484
2765
  "div",
2485
2766
  {
2486
2767
  "data-testid": "doc-progress-fill",
@@ -2494,9 +2775,10 @@ function DocProgressBar({
2494
2775
  }
2495
2776
  }
2496
2777
  ),
2497
- blockMarkers.map((marker, i) => /* @__PURE__ */ jsx13(
2498
- "div",
2778
+ blockMarkers.map((marker, i) => /* @__PURE__ */ jsx14(
2779
+ "button",
2499
2780
  {
2781
+ type: "button",
2500
2782
  style: {
2501
2783
  position: "absolute",
2502
2784
  left: `${marker.position}%`,
@@ -2506,11 +2788,13 @@ function DocProgressBar({
2506
2788
  borderRadius: "50%",
2507
2789
  background: marker.index === state.currentBlockIndex ? "#ffffff" : "rgba(255,255,255,0.5)",
2508
2790
  border: "2px solid #5b9bd5",
2791
+ padding: 0,
2509
2792
  cursor: "pointer",
2510
2793
  zIndex: 2,
2511
2794
  transition: "transform 0.15s, background 0.15s"
2512
2795
  },
2513
2796
  title: marker.title,
2797
+ "aria-label": `Seek to ${marker.title}`,
2514
2798
  onClick: (e) => {
2515
2799
  e.stopPropagation();
2516
2800
  actions.seekTo(marker.block.startTime);
@@ -2524,7 +2808,7 @@ function DocProgressBar({
2524
2808
  },
2525
2809
  `${marker.block.id}-${i}`
2526
2810
  )),
2527
- hoverPosition !== null && /* @__PURE__ */ jsxs8(
2811
+ hoverPosition !== null && /* @__PURE__ */ jsxs9(
2528
2812
  "div",
2529
2813
  {
2530
2814
  style: {
@@ -2541,19 +2825,19 @@ function DocProgressBar({
2541
2825
  zIndex: 10
2542
2826
  },
2543
2827
  children: [
2544
- /* @__PURE__ */ jsx13("div", { style: { color: "white", fontSize: "12px", fontFamily: "monospace" }, children: formatTime(hoverPosition * state.totalDuration) }),
2828
+ /* @__PURE__ */ jsx14("div", { style: { color: "white", fontSize: "12px", fontFamily: "monospace" }, children: formatTime(hoverPosition * state.totalDuration) }),
2545
2829
  (() => {
2546
2830
  const hoverTime = hoverPosition * state.totalDuration;
2547
2831
  const slideInfo = getBlockAtTimeLocal(hoverTime);
2548
2832
  if (slideInfo && getBlockTitle) {
2549
- return /* @__PURE__ */ jsx13("div", { style: { color: "rgba(255,255,255,0.7)", fontSize: "11px", marginTop: "2px" }, children: getBlockTitle(slideInfo.block) });
2833
+ return /* @__PURE__ */ jsx14("div", { style: { color: "rgba(255,255,255,0.7)", fontSize: "11px", marginTop: "2px" }, children: getBlockTitle(slideInfo.block) });
2550
2834
  }
2551
2835
  return null;
2552
2836
  })()
2553
2837
  ]
2554
2838
  }
2555
2839
  ),
2556
- hoverPosition !== null && /* @__PURE__ */ jsx13(
2840
+ hoverPosition !== null && /* @__PURE__ */ jsx14(
2557
2841
  "div",
2558
2842
  {
2559
2843
  style: {
@@ -2575,7 +2859,7 @@ function DocProgressBar({
2575
2859
  }
2576
2860
 
2577
2861
  // src/DocControlsOverlay.tsx
2578
- import { jsx as jsx14, jsxs as jsxs9 } from "react/jsx-runtime";
2862
+ import { jsx as jsx15, jsxs as jsxs10 } from "react/jsx-runtime";
2579
2863
  function DocControlsOverlay({
2580
2864
  state,
2581
2865
  actions,
@@ -2583,7 +2867,7 @@ function DocControlsOverlay({
2583
2867
  expandedBlocks,
2584
2868
  getBlockTitle
2585
2869
  }) {
2586
- return /* @__PURE__ */ jsxs9(
2870
+ return /* @__PURE__ */ jsxs10(
2587
2871
  "div",
2588
2872
  {
2589
2873
  className: "doc-player__controls",
@@ -2600,7 +2884,7 @@ function DocControlsOverlay({
2600
2884
  zIndex: 100
2601
2885
  },
2602
2886
  children: [
2603
- /* @__PURE__ */ jsx14(
2887
+ /* @__PURE__ */ jsx15(
2604
2888
  "button",
2605
2889
  {
2606
2890
  onClick: actions.restart,
@@ -2616,10 +2900,10 @@ function DocControlsOverlay({
2616
2900
  },
2617
2901
  title: "Restart",
2618
2902
  "aria-label": "Restart from beginning",
2619
- children: /* @__PURE__ */ jsx14("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx14("path", { d: "M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z" }) })
2903
+ children: /* @__PURE__ */ jsx15("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx15("path", { d: "M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z" }) })
2620
2904
  }
2621
2905
  ),
2622
- /* @__PURE__ */ jsx14(
2906
+ /* @__PURE__ */ jsx15(
2623
2907
  "button",
2624
2908
  {
2625
2909
  onClick: actions.toggle,
@@ -2638,15 +2922,15 @@ function DocControlsOverlay({
2638
2922
  height: "40px"
2639
2923
  },
2640
2924
  "aria-label": state.isPlaying ? "Pause" : "Play",
2641
- children: state.isPlaying ? /* @__PURE__ */ jsx14("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx14("path", { d: "M6 19h4V5H6v14zm8-14v14h4V5h-4z" }) }) : /* @__PURE__ */ jsx14("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx14("path", { d: "M8 5v14l11-7z" }) })
2925
+ children: state.isPlaying ? /* @__PURE__ */ jsx15("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx15("path", { d: "M6 19h4V5H6v14zm8-14v14h4V5h-4z" }) }) : /* @__PURE__ */ jsx15("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx15("path", { d: "M8 5v14l11-7z" }) })
2642
2926
  }
2643
2927
  ),
2644
- /* @__PURE__ */ jsxs9("span", { style: { color: "white", fontSize: "12px", minWidth: "80px", fontFamily: "monospace" }, children: [
2928
+ /* @__PURE__ */ jsxs10("span", { style: { color: "white", fontSize: "12px", minWidth: "80px", fontFamily: "monospace" }, children: [
2645
2929
  formatTime(state.currentTime),
2646
2930
  " / ",
2647
2931
  formatTime(state.totalDuration)
2648
2932
  ] }),
2649
- /* @__PURE__ */ jsx14(
2933
+ /* @__PURE__ */ jsx15(
2650
2934
  DocProgressBar,
2651
2935
  {
2652
2936
  state,
@@ -2656,12 +2940,12 @@ function DocControlsOverlay({
2656
2940
  getBlockTitle
2657
2941
  }
2658
2942
  ),
2659
- /* @__PURE__ */ jsxs9("span", { style: { color: "rgba(255,255,255,0.5)", fontSize: "11px", whiteSpace: "nowrap" }, children: [
2943
+ /* @__PURE__ */ jsxs10("span", { style: { color: "rgba(255,255,255,0.5)", fontSize: "11px", whiteSpace: "nowrap" }, children: [
2660
2944
  state.currentBlockIndex + 1,
2661
2945
  "/",
2662
2946
  state.totalBlocks
2663
2947
  ] }),
2664
- state.hasCaptions && /* @__PURE__ */ jsxs9(
2948
+ state.hasCaptions && /* @__PURE__ */ jsxs10(
2665
2949
  "button",
2666
2950
  {
2667
2951
  onClick: () => actions.cycleCaptionMode(),
@@ -2680,12 +2964,12 @@ function DocControlsOverlay({
2680
2964
  title: state.captionMode === "off" ? "Captions: Off (click for Standard)" : state.captionMode === "standard" ? "Captions: Standard (click for Social)" : "Captions: Social (click to turn off)",
2681
2965
  "aria-label": "Cycle caption style",
2682
2966
  children: [
2683
- /* @__PURE__ */ jsx14("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx14("path", { d: "M19 4H5a2 2 0 00-2 2v12a2 2 0 002 2h14a2 2 0 002-2V6a2 2 0 00-2-2zm-8 7H9.5v-.5h-2v3h2V13H11v1a1 1 0 01-1 1H7a1 1 0 01-1-1v-4a1 1 0 011-1h3a1 1 0 011 1v1zm7 0h-1.5v-.5h-2v3h2V13H18v1a1 1 0 01-1 1h-3a1 1 0 01-1-1v-4a1 1 0 011-1h3a1 1 0 011 1v1z" }) }),
2684
- state.captionMode !== "off" && /* @__PURE__ */ jsx14("span", { style: { fontSize: "10px", textTransform: "uppercase", letterSpacing: "0.5px" }, children: state.captionMode === "standard" ? "CC" : "SM" })
2967
+ /* @__PURE__ */ jsx15("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx15("path", { d: "M19 4H5a2 2 0 00-2 2v12a2 2 0 002 2h14a2 2 0 002-2V6a2 2 0 00-2-2zm-8 7H9.5v-.5h-2v3h2V13H11v1a1 1 0 01-1 1H7a1 1 0 01-1-1v-4a1 1 0 011-1h3a1 1 0 011 1v1zm7 0h-1.5v-.5h-2v3h2V13H18v1a1 1 0 01-1 1h-3a1 1 0 01-1-1v-4a1 1 0 011-1h3a1 1 0 011 1v1z" }) }),
2968
+ state.captionMode !== "off" && /* @__PURE__ */ jsx15("span", { style: { fontSize: "10px", textTransform: "uppercase", letterSpacing: "0.5px" }, children: state.captionMode === "standard" ? "CC" : "SM" })
2685
2969
  ]
2686
2970
  }
2687
2971
  ),
2688
- actions.toggleFullscreen && /* @__PURE__ */ jsx14(
2972
+ actions.toggleFullscreen && /* @__PURE__ */ jsx15(
2689
2973
  "button",
2690
2974
  {
2691
2975
  onClick: actions.toggleFullscreen,
@@ -2701,7 +2985,7 @@ function DocControlsOverlay({
2701
2985
  },
2702
2986
  title: state.isFullscreen ? "Exit fullscreen (F)" : "Fullscreen (F)",
2703
2987
  "aria-label": state.isFullscreen ? "Exit fullscreen" : "Enter fullscreen",
2704
- children: state.isFullscreen ? /* @__PURE__ */ jsx14("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx14("path", { d: "M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z" }) }) : /* @__PURE__ */ jsx14("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx14("path", { d: "M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" }) })
2988
+ children: state.isFullscreen ? /* @__PURE__ */ jsx15("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx15("path", { d: "M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z" }) }) : /* @__PURE__ */ jsx15("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx15("path", { d: "M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" }) })
2705
2989
  }
2706
2990
  )
2707
2991
  ]
@@ -2710,8 +2994,29 @@ function DocControlsOverlay({
2710
2994
  }
2711
2995
 
2712
2996
  // src/DocControlsSlideshow.tsx
2713
- import { jsx as jsx15, jsxs as jsxs10 } from "react/jsx-runtime";
2714
- function DocControlsSlideshow({ state, slideNav }) {
2997
+ import { useCallback as useCallback6, useEffect as useEffect8, useId as useId6, useLayoutEffect, useRef as useRef7, useState as useState8 } from "react";
2998
+ import { jsx as jsx16, jsxs as jsxs11 } from "react/jsx-runtime";
2999
+ function DocControlsSlideshow({
3000
+ state,
3001
+ slideNav,
3002
+ slides = [],
3003
+ pickerOpen,
3004
+ onPickerOpenChange
3005
+ }) {
3006
+ const [uncontrolledPickerOpen, setUncontrolledPickerOpen] = useState8(false);
3007
+ const isPickerOpen = pickerOpen ?? uncontrolledPickerOpen;
3008
+ const setPickerOpen = useCallback6(
3009
+ (open) => {
3010
+ if (pickerOpen === void 0) setUncontrolledPickerOpen(open);
3011
+ onPickerOpenChange?.(open);
3012
+ },
3013
+ [pickerOpen, onPickerOpenChange]
3014
+ );
3015
+ const [pickerMaxHeight, setPickerMaxHeight] = useState8(280);
3016
+ const controlsRef = useRef7(null);
3017
+ const triggerRef = useRef7(null);
3018
+ const menuRef = useRef7(null);
3019
+ const menuId = useId6();
2715
3020
  const {
2716
3021
  currentBlockIndex,
2717
3022
  currentSlideLabel,
@@ -2722,9 +3027,68 @@ function DocControlsSlideshow({ state, slideNav }) {
2722
3027
  const isFirst = currentBlockIndex <= 0;
2723
3028
  const isLast = currentBlockIndex >= totalBlocks - 1;
2724
3029
  const counterText = totalBlocks > 0 ? currentSlideLabel ?? `${currentSlideNumber ?? currentBlockIndex + 1} / ${totalSlideNumber ?? totalBlocks}` : "\u2014";
2725
- return /* @__PURE__ */ jsxs10(
3030
+ useEffect8(() => {
3031
+ if (!isPickerOpen) return;
3032
+ const handlePointerDown = (event) => {
3033
+ if (!controlsRef.current?.contains(event.target)) setPickerOpen(false);
3034
+ };
3035
+ const handleKeyDown = (event) => {
3036
+ if (event.key !== "Escape") return;
3037
+ event.preventDefault();
3038
+ setPickerOpen(false);
3039
+ triggerRef.current?.focus();
3040
+ };
3041
+ document.addEventListener("pointerdown", handlePointerDown);
3042
+ document.addEventListener("keydown", handleKeyDown);
3043
+ return () => {
3044
+ document.removeEventListener("pointerdown", handlePointerDown);
3045
+ document.removeEventListener("keydown", handleKeyDown);
3046
+ };
3047
+ }, [isPickerOpen, setPickerOpen]);
3048
+ useLayoutEffect(() => {
3049
+ if (!isPickerOpen) return;
3050
+ const controls = controlsRef.current;
3051
+ const player = controls?.closest(".doc-player") ?? controls?.parentElement;
3052
+ if (!controls || !player) return;
3053
+ const updateMaxHeight = () => {
3054
+ const controlsBounds = controls.getBoundingClientRect();
3055
+ const playerBounds = player.getBoundingClientRect();
3056
+ const availableHeight = Math.floor(controlsBounds.top - playerBounds.top - 8 - 16);
3057
+ setPickerMaxHeight(Math.max(72, availableHeight));
3058
+ };
3059
+ updateMaxHeight();
3060
+ const resizeObserver = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(updateMaxHeight);
3061
+ resizeObserver?.observe(player);
3062
+ resizeObserver?.observe(controls);
3063
+ window.addEventListener("resize", updateMaxHeight);
3064
+ return () => {
3065
+ resizeObserver?.disconnect();
3066
+ window.removeEventListener("resize", updateMaxHeight);
3067
+ };
3068
+ }, [isPickerOpen]);
3069
+ useEffect8(() => {
3070
+ if (!isPickerOpen) return;
3071
+ menuRef.current?.querySelector('[aria-current="true"]')?.focus();
3072
+ }, [isPickerOpen]);
3073
+ const handleMenuKeyDown = (event) => {
3074
+ if (!["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) return;
3075
+ const items = Array.from(
3076
+ menuRef.current?.querySelectorAll('[role="menuitem"]') ?? []
3077
+ );
3078
+ if (items.length === 0) return;
3079
+ event.preventDefault();
3080
+ const focusedIndex = items.indexOf(document.activeElement);
3081
+ let nextIndex = focusedIndex;
3082
+ if (event.key === "Home") nextIndex = 0;
3083
+ else if (event.key === "End") nextIndex = items.length - 1;
3084
+ else if (event.key === "ArrowDown") nextIndex = (focusedIndex + 1) % items.length;
3085
+ else nextIndex = (focusedIndex - 1 + items.length) % items.length;
3086
+ items[nextIndex]?.focus();
3087
+ };
3088
+ return /* @__PURE__ */ jsxs11(
2726
3089
  "div",
2727
3090
  {
3091
+ ref: controlsRef,
2728
3092
  className: "doc-controls-slideshow",
2729
3093
  "data-testid": "slideshow-controls",
2730
3094
  style: {
@@ -2743,7 +3107,7 @@ function DocControlsSlideshow({ state, slideNav }) {
2743
3107
  WebkitBackdropFilter: "blur(8px)"
2744
3108
  },
2745
3109
  children: [
2746
- /* @__PURE__ */ jsx15(
3110
+ /* @__PURE__ */ jsx16(
2747
3111
  "button",
2748
3112
  {
2749
3113
  onClick: (e) => {
@@ -2772,27 +3136,138 @@ function DocControlsSlideshow({ state, slideNav }) {
2772
3136
  onMouseLeave: (e) => {
2773
3137
  e.currentTarget.style.background = "none";
2774
3138
  },
2775
- children: /* @__PURE__ */ jsx15("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx15("path", { d: "M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z" }) })
3139
+ children: /* @__PURE__ */ jsx16("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx16("path", { d: "M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z" }) })
2776
3140
  }
2777
3141
  ),
2778
- /* @__PURE__ */ jsx15(
2779
- "span",
3142
+ /* @__PURE__ */ jsx16(
3143
+ "button",
2780
3144
  {
3145
+ ref: triggerRef,
3146
+ type: "button",
2781
3147
  "data-testid": "slide-counter",
3148
+ "aria-label": `Choose slide, current ${counterText}`,
3149
+ "aria-haspopup": "menu",
3150
+ "aria-expanded": isPickerOpen,
3151
+ "aria-controls": isPickerOpen ? menuId : void 0,
3152
+ title: "Choose slide",
3153
+ disabled: slides.length === 0,
3154
+ onClick: (event) => {
3155
+ event.stopPropagation();
3156
+ setPickerOpen(!isPickerOpen);
3157
+ },
2782
3158
  style: {
3159
+ background: isPickerOpen ? "rgba(255,255,255,0.12)" : "none",
3160
+ border: "none",
2783
3161
  color: "rgba(255,255,255,0.9)",
3162
+ cursor: slides.length > 0 ? "pointer" : "default",
2784
3163
  fontSize: "13px",
2785
3164
  fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
2786
3165
  fontVariantNumeric: "tabular-nums",
2787
3166
  minWidth: "48px",
2788
3167
  textAlign: "center",
2789
- padding: "0 4px",
2790
- letterSpacing: "0.02em"
3168
+ padding: "6px 4px",
3169
+ letterSpacing: "0.02em",
3170
+ borderRadius: "4px",
3171
+ transition: "background 0.15s"
2791
3172
  },
2792
3173
  children: counterText
2793
3174
  }
2794
3175
  ),
2795
- /* @__PURE__ */ jsx15(
3176
+ isPickerOpen && /* @__PURE__ */ jsx16(
3177
+ "div",
3178
+ {
3179
+ ref: menuRef,
3180
+ id: menuId,
3181
+ role: "menu",
3182
+ "aria-label": "Choose a slide",
3183
+ "data-testid": "slide-picker",
3184
+ onClick: (event) => event.stopPropagation(),
3185
+ onKeyDown: handleMenuKeyDown,
3186
+ style: {
3187
+ position: "absolute",
3188
+ right: 0,
3189
+ bottom: "calc(100% + 8px)",
3190
+ width: "min(280px, calc(100vw - 32px))",
3191
+ maxHeight: `${pickerMaxHeight}px`,
3192
+ overflowY: "auto",
3193
+ padding: "6px",
3194
+ background: "rgba(20, 20, 20, 0.94)",
3195
+ border: "1px solid rgba(255,255,255,0.14)",
3196
+ borderRadius: "8px",
3197
+ boxShadow: "0 10px 30px rgba(0,0,0,0.38)",
3198
+ backdropFilter: "blur(12px)",
3199
+ WebkitBackdropFilter: "blur(12px)"
3200
+ },
3201
+ children: slides.map((slide, index) => {
3202
+ const isCurrent = index === currentBlockIndex;
3203
+ return /* @__PURE__ */ jsxs11(
3204
+ "button",
3205
+ {
3206
+ type: "button",
3207
+ role: "menuitem",
3208
+ "aria-current": isCurrent ? "true" : void 0,
3209
+ "data-testid": `slide-picker-item-${index}`,
3210
+ onClick: () => {
3211
+ slideNav.goToSlide(index);
3212
+ setPickerOpen(false);
3213
+ },
3214
+ style: {
3215
+ display: "grid",
3216
+ gridTemplateColumns: "42px minmax(0, 1fr)",
3217
+ alignItems: "center",
3218
+ gap: "8px",
3219
+ width: "100%",
3220
+ padding: "8px 10px",
3221
+ background: isCurrent ? "rgba(255,255,255,0.14)" : "transparent",
3222
+ border: "none",
3223
+ borderRadius: "5px",
3224
+ color: "rgba(255,255,255,0.94)",
3225
+ cursor: "pointer",
3226
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
3227
+ textAlign: "left"
3228
+ },
3229
+ onMouseEnter: (event) => {
3230
+ event.currentTarget.style.background = "rgba(255,255,255,0.1)";
3231
+ },
3232
+ onMouseLeave: (event) => {
3233
+ event.currentTarget.style.background = isCurrent ? "rgba(255,255,255,0.14)" : "transparent";
3234
+ },
3235
+ children: [
3236
+ /* @__PURE__ */ jsx16(
3237
+ "span",
3238
+ {
3239
+ style: {
3240
+ color: isCurrent ? "#fff" : "rgba(255,255,255,0.58)",
3241
+ fontSize: "12px",
3242
+ fontVariantNumeric: "tabular-nums",
3243
+ textAlign: "right"
3244
+ },
3245
+ children: slide.label
3246
+ }
3247
+ ),
3248
+ /* @__PURE__ */ jsx16(
3249
+ "span",
3250
+ {
3251
+ style: {
3252
+ minWidth: 0,
3253
+ overflow: "hidden",
3254
+ color: isCurrent ? "#fff" : "rgba(255,255,255,0.82)",
3255
+ fontSize: "13px",
3256
+ lineHeight: 1.3,
3257
+ textOverflow: "ellipsis",
3258
+ whiteSpace: "nowrap"
3259
+ },
3260
+ children: slide.summary
3261
+ }
3262
+ )
3263
+ ]
3264
+ },
3265
+ `${slide.id}-${index}`
3266
+ );
3267
+ })
3268
+ }
3269
+ ),
3270
+ /* @__PURE__ */ jsx16(
2796
3271
  "button",
2797
3272
  {
2798
3273
  onClick: (e) => {
@@ -2821,7 +3296,7 @@ function DocControlsSlideshow({ state, slideNav }) {
2821
3296
  onMouseLeave: (e) => {
2822
3297
  e.currentTarget.style.background = "none";
2823
3298
  },
2824
- children: /* @__PURE__ */ jsx15("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx15("path", { d: "M8.59 16.59L10 18l6-6-6-6-1.41 1.41L13.17 12z" }) })
3299
+ children: /* @__PURE__ */ jsx16("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx16("path", { d: "M8.59 16.59L10 18l6-6-6-6-1.41 1.41L13.17 12z" }) })
2825
3300
  }
2826
3301
  )
2827
3302
  ]
@@ -2830,18 +3305,18 @@ function DocControlsSlideshow({ state, slideNav }) {
2830
3305
  }
2831
3306
 
2832
3307
  // src/LinearDocView.tsx
2833
- import { useMemo as useMemo8 } from "react";
3308
+ import { useEffect as useEffect9, useMemo as useMemo8, useRef as useRef8 } from "react";
2834
3309
  import {
2835
3310
  applySurface,
2836
3311
  resolveFontFamily as resolveFontFamily2
2837
3312
  } from "@bendyline/squisq/schemas";
2838
3313
  import { VIEWPORT_PRESETS as VIEWPORT_PRESETS3 } from "@bendyline/squisq/schemas";
2839
3314
  import {
2840
- getLayers,
2841
- hasTemplate,
3315
+ materializeBlockLayers,
2842
3316
  markdownToDoc,
2843
3317
  DEFAULT_THEME as DEFAULT_THEME2,
2844
- deriveTemplateInputs
3318
+ deriveTemplateInputs,
3319
+ isTemplateBlock as isTemplateBlock2
2845
3320
  } from "@bendyline/squisq/doc";
2846
3321
  import { extractPlainText, parseMarkdown } from "@bendyline/squisq/markdown";
2847
3322
 
@@ -2853,7 +3328,7 @@ import {
2853
3328
  } from "@bendyline/squisq/markdown";
2854
3329
 
2855
3330
  // src/InlineVideoPlayer.tsx
2856
- import { jsx as jsx16 } from "react/jsx-runtime";
3331
+ import { jsx as jsx17 } from "react/jsx-runtime";
2857
3332
  function InlineVideoPlayer({
2858
3333
  src,
2859
3334
  basePath = "",
@@ -2868,7 +3343,7 @@ function InlineVideoPlayer({
2868
3343
  const resolvedPoster = useMediaUrl(poster ?? "", basePath);
2869
3344
  const posterUrl = poster ? resolvedPoster : void 0;
2870
3345
  if (!resolvedSrc) return null;
2871
- return /* @__PURE__ */ jsx16("span", { className: `squisq-inline-video-player ${className ?? ""}`.trim(), children: /* @__PURE__ */ jsx16(
3346
+ return /* @__PURE__ */ jsx17("span", { className: `squisq-inline-video-player ${className ?? ""}`.trim(), children: /* @__PURE__ */ jsx17(
2872
3347
  "video",
2873
3348
  {
2874
3349
  src: resolvedSrc,
@@ -2883,7 +3358,7 @@ function InlineVideoPlayer({
2883
3358
  }
2884
3359
 
2885
3360
  // src/InlineAudioPlayer.tsx
2886
- import { jsx as jsx17 } from "react/jsx-runtime";
3361
+ import { jsx as jsx18 } from "react/jsx-runtime";
2887
3362
  function InlineAudioPlayer({
2888
3363
  src,
2889
3364
  basePath = "",
@@ -2893,11 +3368,11 @@ function InlineAudioPlayer({
2893
3368
  }) {
2894
3369
  const resolvedSrc = useMediaUrl(src, basePath);
2895
3370
  if (!resolvedSrc) return null;
2896
- return /* @__PURE__ */ jsx17("span", { className: `squisq-inline-audio-player ${className ?? ""}`.trim(), children: /* @__PURE__ */ jsx17("audio", { src: resolvedSrc, controls, preload }) });
3371
+ return /* @__PURE__ */ jsx18("span", { className: `squisq-inline-audio-player ${className ?? ""}`.trim(), children: /* @__PURE__ */ jsx18("audio", { src: resolvedSrc, controls, preload }) });
2897
3372
  }
2898
3373
 
2899
3374
  // src/MarkdownRenderer.tsx
2900
- import { jsx as jsx18, jsxs as jsxs11 } from "react/jsx-runtime";
3375
+ import { jsx as jsx19, jsxs as jsxs12 } from "react/jsx-runtime";
2901
3376
  var DEFAULT_CTX = { htmlPolicy: "sanitize" };
2902
3377
  function renderInline(nodes, keyPrefix = "", ctx = DEFAULT_CTX) {
2903
3378
  return nodes.map((node, i) => {
@@ -2905,28 +3380,28 @@ function renderInline(nodes, keyPrefix = "", ctx = DEFAULT_CTX) {
2905
3380
  switch (node.type) {
2906
3381
  case "text": {
2907
3382
  if (!node.value.includes("\n")) {
2908
- return /* @__PURE__ */ jsx18(Fragment2, { children: node.value }, key);
3383
+ return /* @__PURE__ */ jsx19(Fragment2, { children: node.value }, key);
2909
3384
  }
2910
3385
  const parts = node.value.split("\n");
2911
- return /* @__PURE__ */ jsx18(Fragment2, { children: parts.map((part, j) => /* @__PURE__ */ jsxs11(Fragment2, { children: [
2912
- j > 0 && /* @__PURE__ */ jsx18("br", {}),
3386
+ return /* @__PURE__ */ jsx19(Fragment2, { children: parts.map((part, j) => /* @__PURE__ */ jsxs12(Fragment2, { children: [
3387
+ j > 0 && /* @__PURE__ */ jsx19("br", {}),
2913
3388
  part
2914
3389
  ] }, j)) }, key);
2915
3390
  }
2916
3391
  case "emphasis":
2917
- return /* @__PURE__ */ jsx18("em", { className: "squisq-md-em", children: renderInline(node.children, key, ctx) }, key);
3392
+ return /* @__PURE__ */ jsx19("em", { className: "squisq-md-em", children: renderInline(node.children, key, ctx) }, key);
2918
3393
  case "strong":
2919
- return /* @__PURE__ */ jsx18("strong", { className: "squisq-md-strong", children: renderInline(node.children, key, ctx) }, key);
3394
+ return /* @__PURE__ */ jsx19("strong", { className: "squisq-md-strong", children: renderInline(node.children, key, ctx) }, key);
2920
3395
  case "delete":
2921
- return /* @__PURE__ */ jsx18("del", { className: "squisq-md-del", children: renderInline(node.children, key, ctx) }, key);
3396
+ return /* @__PURE__ */ jsx19("del", { className: "squisq-md-del", children: renderInline(node.children, key, ctx) }, key);
2922
3397
  case "inlineCode":
2923
- return /* @__PURE__ */ jsx18("code", { className: "squisq-md-inline-code", children: node.value }, key);
3398
+ return /* @__PURE__ */ jsx19("code", { className: "squisq-md-inline-code", children: node.value }, key);
2924
3399
  case "link": {
2925
3400
  const href = sanitizeUrl(node.url, "link", { extraLinkSchemes: ctx.linkSchemes });
2926
3401
  if (!href) {
2927
- return /* @__PURE__ */ jsx18("span", { className: "squisq-md-link squisq-md-link--blocked", children: renderInline(node.children, key, ctx) }, key);
3402
+ return /* @__PURE__ */ jsx19("span", { className: "squisq-md-link squisq-md-link--blocked", children: renderInline(node.children, key, ctx) }, key);
2928
3403
  }
2929
- return /* @__PURE__ */ jsx18(
3404
+ return /* @__PURE__ */ jsx19(
2930
3405
  "a",
2931
3406
  {
2932
3407
  className: "squisq-md-link",
@@ -2940,42 +3415,32 @@ function renderInline(nodes, keyPrefix = "", ctx = DEFAULT_CTX) {
2940
3415
  );
2941
3416
  }
2942
3417
  case "image":
2943
- return /* @__PURE__ */ jsx18(MdImage, { src: node.url, alt: node.alt ?? "", title: node.title ?? void 0 }, key);
3418
+ return /* @__PURE__ */ jsx19(MdImage, { src: node.url, alt: node.alt ?? "", title: node.title ?? void 0 }, key);
2944
3419
  case "break":
2945
- return /* @__PURE__ */ jsx18("br", {}, key);
3420
+ return /* @__PURE__ */ jsx19("br", {}, key);
2946
3421
  case "inlineMath":
2947
- return /* @__PURE__ */ jsx18("code", { className: "squisq-md-inline-math", children: node.value }, key);
3422
+ return /* @__PURE__ */ jsx19("code", { className: "squisq-md-inline-math", children: node.value }, key);
2948
3423
  case "htmlInline":
2949
3424
  if (ctx.htmlPolicy === "strip") return null;
2950
- if (ctx.htmlPolicy === "trusted" && !containsMediaTag(node.htmlChildren) && !containsDangerousTag(node.htmlChildren) && !hasDangerousRawHtml(node.rawHtml)) {
2951
- return /* @__PURE__ */ jsx18(
2952
- "span",
2953
- {
2954
- className: "squisq-md-html-inline",
2955
- dangerouslySetInnerHTML: { __html: node.rawHtml }
2956
- },
2957
- key
2958
- );
2959
- }
2960
- return /* @__PURE__ */ jsx18("span", { className: "squisq-md-html-inline", children: renderHtmlNodes(resolveHtmlNodes(node.htmlChildren, ctx.htmlPolicy), `${key}h`) }, key);
3425
+ return /* @__PURE__ */ jsx19("span", { className: "squisq-md-html-inline", children: renderHtmlNodes(resolveHtmlNodes(node.htmlChildren, ctx.htmlPolicy), `${key}h`, ctx) }, key);
2961
3426
  case "footnoteReference":
2962
- return /* @__PURE__ */ jsx18("sup", { className: "squisq-md-footnote-ref", children: /* @__PURE__ */ jsxs11("a", { href: `#fn-${node.identifier}`, children: [
3427
+ return /* @__PURE__ */ jsx19("sup", { className: "squisq-md-footnote-ref", children: /* @__PURE__ */ jsxs12("a", { href: `#fn-${node.identifier}`, children: [
2963
3428
  "[",
2964
3429
  node.label ?? node.identifier,
2965
3430
  "]"
2966
3431
  ] }) }, key);
2967
3432
  case "linkReference":
2968
- return /* @__PURE__ */ jsx18("span", { className: "squisq-md-link-ref", children: renderInline(node.children, key, ctx) }, key);
3433
+ return /* @__PURE__ */ jsx19("span", { className: "squisq-md-link-ref", children: renderInline(node.children, key, ctx) }, key);
2969
3434
  case "imageReference":
2970
- return /* @__PURE__ */ jsxs11("span", { className: "squisq-md-image-ref", children: [
3435
+ return /* @__PURE__ */ jsxs12("span", { className: "squisq-md-image-ref", children: [
2971
3436
  "[",
2972
3437
  node.alt ?? node.identifier,
2973
3438
  "]"
2974
3439
  ] }, key);
2975
3440
  case "textDirective":
2976
- return /* @__PURE__ */ jsx18("span", { className: "squisq-md-text-directive", "data-directive": node.name, children: renderInline(node.children, key, ctx) }, key);
3441
+ return /* @__PURE__ */ jsx19("span", { className: "squisq-md-text-directive", "data-directive": node.name, children: renderInline(node.children, key, ctx) }, key);
2977
3442
  case "mention":
2978
- return /* @__PURE__ */ jsxs11(
3443
+ return /* @__PURE__ */ jsxs12(
2979
3444
  "span",
2980
3445
  {
2981
3446
  className: "squisq-md-mention mention",
@@ -2998,61 +3463,51 @@ function renderInline(nodes, keyPrefix = "", ctx = DEFAULT_CTX) {
2998
3463
  function renderBlock(node, key, ctx = DEFAULT_CTX) {
2999
3464
  switch (node.type) {
3000
3465
  case "paragraph":
3001
- return /* @__PURE__ */ jsx18("p", { className: "squisq-md-p", children: renderInline(node.children, key, ctx) }, key);
3466
+ return /* @__PURE__ */ jsx19("p", { className: "squisq-md-p", children: renderInline(node.children, key, ctx) }, key);
3002
3467
  case "heading": {
3003
3468
  const Tag = `h${node.depth}`;
3004
- return /* @__PURE__ */ jsx18(Tag, { className: `squisq-md-heading squisq-md-h${node.depth}`, children: renderInline(node.children, key, ctx) }, key);
3469
+ return /* @__PURE__ */ jsx19(Tag, { className: `squisq-md-heading squisq-md-h${node.depth}`, children: renderInline(node.children, key, ctx) }, key);
3005
3470
  }
3006
3471
  case "blockquote":
3007
- return /* @__PURE__ */ jsx18("blockquote", { className: "squisq-md-blockquote", children: renderBlocks(node.children, key, ctx) }, key);
3472
+ return /* @__PURE__ */ jsx19("blockquote", { className: "squisq-md-blockquote", children: renderBlocks(node.children, key, ctx) }, key);
3008
3473
  case "list":
3009
3474
  if (node.ordered) {
3010
- return /* @__PURE__ */ jsx18("ol", { className: "squisq-md-list squisq-md-ol", start: node.start ?? void 0, children: node.children.map((item, i) => renderListItem(item, `${key}li${i}`, ctx)) }, key);
3475
+ return /* @__PURE__ */ jsx19("ol", { className: "squisq-md-list squisq-md-ol", start: node.start ?? void 0, children: node.children.map((item, i) => renderListItem(item, `${key}li${i}`, ctx)) }, key);
3011
3476
  }
3012
- return /* @__PURE__ */ jsx18("ul", { className: "squisq-md-list squisq-md-ul", children: node.children.map((item, i) => renderListItem(item, `${key}li${i}`, ctx)) }, key);
3477
+ return /* @__PURE__ */ jsx19("ul", { className: "squisq-md-list squisq-md-ul", children: node.children.map((item, i) => renderListItem(item, `${key}li${i}`, ctx)) }, key);
3013
3478
  case "code":
3014
- return /* @__PURE__ */ jsx18("pre", { className: "squisq-md-code-block", children: /* @__PURE__ */ jsx18("code", { className: node.lang ? `language-${node.lang}` : void 0, children: node.value }) }, key);
3479
+ return /* @__PURE__ */ jsx19("pre", { className: "squisq-md-code-block", children: /* @__PURE__ */ jsx19("code", { className: node.lang ? `language-${node.lang}` : void 0, children: node.value }) }, key);
3015
3480
  case "thematicBreak":
3016
- return /* @__PURE__ */ jsx18("hr", { className: "squisq-md-hr" }, key);
3481
+ return /* @__PURE__ */ jsx19("hr", { className: "squisq-md-hr" }, key);
3017
3482
  case "table":
3018
3483
  return renderTable(node.children, node.align, key, ctx);
3019
3484
  case "htmlBlock":
3020
3485
  if (ctx.htmlPolicy === "strip") return null;
3021
- if (ctx.htmlPolicy === "trusted" && !containsMediaTag(node.htmlChildren) && !containsDangerousTag(node.htmlChildren) && !hasDangerousRawHtml(node.rawHtml)) {
3022
- return /* @__PURE__ */ jsx18(
3023
- "div",
3024
- {
3025
- className: "squisq-md-html-block",
3026
- dangerouslySetInnerHTML: { __html: node.rawHtml }
3027
- },
3028
- key
3029
- );
3030
- }
3031
- return /* @__PURE__ */ jsx18("div", { className: "squisq-md-html-block", children: renderHtmlNodes(resolveHtmlNodes(node.htmlChildren, ctx.htmlPolicy), `${key}h`) }, key);
3486
+ return /* @__PURE__ */ jsx19("div", { className: "squisq-md-html-block", children: renderHtmlNodes(resolveHtmlNodes(node.htmlChildren, ctx.htmlPolicy), `${key}h`, ctx) }, key);
3032
3487
  case "math":
3033
- return /* @__PURE__ */ jsx18("pre", { className: "squisq-md-math-block", children: /* @__PURE__ */ jsx18("code", { children: node.value }) }, key);
3488
+ return /* @__PURE__ */ jsx19("pre", { className: "squisq-md-math-block", children: /* @__PURE__ */ jsx19("code", { children: node.value }) }, key);
3034
3489
  case "definition":
3035
3490
  return null;
3036
3491
  case "footnoteDefinition":
3037
- return /* @__PURE__ */ jsxs11("div", { className: "squisq-md-footnote-def", id: `fn-${node.identifier}`, children: [
3038
- /* @__PURE__ */ jsx18("sup", { children: node.label ?? node.identifier }),
3492
+ return /* @__PURE__ */ jsxs12("div", { className: "squisq-md-footnote-def", id: `fn-${node.identifier}`, children: [
3493
+ /* @__PURE__ */ jsx19("sup", { children: node.label ?? node.identifier }),
3039
3494
  renderBlocks(node.children, key, ctx)
3040
3495
  ] }, key);
3041
3496
  case "containerDirective":
3042
- return /* @__PURE__ */ jsxs11(
3497
+ return /* @__PURE__ */ jsxs12(
3043
3498
  "div",
3044
3499
  {
3045
3500
  className: `squisq-md-directive squisq-md-directive-${node.name}`,
3046
3501
  "data-directive": node.name,
3047
3502
  children: [
3048
- node.label && /* @__PURE__ */ jsx18("div", { className: "squisq-md-directive-label", children: node.label }),
3503
+ node.label && /* @__PURE__ */ jsx19("div", { className: "squisq-md-directive-label", children: node.label }),
3049
3504
  renderBlocks(node.children, key, ctx)
3050
3505
  ]
3051
3506
  },
3052
3507
  key
3053
3508
  );
3054
3509
  case "leafDirective":
3055
- return /* @__PURE__ */ jsx18(
3510
+ return /* @__PURE__ */ jsx19(
3056
3511
  "div",
3057
3512
  {
3058
3513
  className: `squisq-md-directive squisq-md-directive-${node.name}`,
@@ -3062,11 +3517,11 @@ function renderBlock(node, key, ctx = DEFAULT_CTX) {
3062
3517
  key
3063
3518
  );
3064
3519
  case "definitionList":
3065
- return /* @__PURE__ */ jsx18("dl", { className: "squisq-md-dl", children: node.children.map((child, i) => {
3520
+ return /* @__PURE__ */ jsx19("dl", { className: "squisq-md-dl", children: node.children.map((child, i) => {
3066
3521
  if (child.type === "definitionTerm") {
3067
- return /* @__PURE__ */ jsx18("dt", { className: "squisq-md-dt", children: renderInline(child.children, `${key}dt${i}`, ctx) }, `${key}dt${i}`);
3522
+ return /* @__PURE__ */ jsx19("dt", { className: "squisq-md-dt", children: renderInline(child.children, `${key}dt${i}`, ctx) }, `${key}dt${i}`);
3068
3523
  }
3069
- return /* @__PURE__ */ jsx18("dd", { className: "squisq-md-dd", children: renderBlocks(child.children, `${key}dd${i}`, ctx) }, `${key}dd${i}`);
3524
+ return /* @__PURE__ */ jsx19("dd", { className: "squisq-md-dd", children: renderBlocks(child.children, `${key}dd${i}`, ctx) }, `${key}dd${i}`);
3070
3525
  }) }, key);
3071
3526
  default:
3072
3527
  return null;
@@ -3074,15 +3529,15 @@ function renderBlock(node, key, ctx = DEFAULT_CTX) {
3074
3529
  }
3075
3530
  function renderListItem(item, key, ctx = DEFAULT_CTX) {
3076
3531
  const isTask = item.checked !== null && item.checked !== void 0;
3077
- return /* @__PURE__ */ jsxs11("li", { className: `squisq-md-li${isTask ? " squisq-md-task" : ""}`, children: [
3078
- isTask && /* @__PURE__ */ jsx18("input", { type: "checkbox", checked: !!item.checked, readOnly: true, className: "squisq-md-checkbox" }),
3532
+ return /* @__PURE__ */ jsxs12("li", { className: `squisq-md-li${isTask ? " squisq-md-task" : ""}`, children: [
3533
+ isTask && /* @__PURE__ */ jsx19("input", { type: "checkbox", checked: !!item.checked, readOnly: true, className: "squisq-md-checkbox" }),
3079
3534
  renderBlocks(item.children, key, ctx)
3080
3535
  ] }, key);
3081
3536
  }
3082
3537
  function renderTable(rows, align, key, ctx = DEFAULT_CTX) {
3083
3538
  const [headerRow, ...bodyRows] = rows;
3084
- return /* @__PURE__ */ jsxs11("table", { className: "squisq-md-table", children: [
3085
- headerRow && /* @__PURE__ */ jsx18("thead", { children: /* @__PURE__ */ jsx18("tr", { children: headerRow.children.map((cell, ci) => /* @__PURE__ */ jsx18(
3539
+ return /* @__PURE__ */ jsxs12("table", { className: "squisq-md-table", children: [
3540
+ headerRow && /* @__PURE__ */ jsx19("thead", { children: /* @__PURE__ */ jsx19("tr", { children: headerRow.children.map((cell, ci) => /* @__PURE__ */ jsx19(
3086
3541
  "th",
3087
3542
  {
3088
3543
  className: "squisq-md-th",
@@ -3091,7 +3546,7 @@ function renderTable(rows, align, key, ctx = DEFAULT_CTX) {
3091
3546
  },
3092
3547
  `${key}th${ci}`
3093
3548
  )) }) }),
3094
- bodyRows.length > 0 && /* @__PURE__ */ jsx18("tbody", { children: bodyRows.map((row, ri) => /* @__PURE__ */ jsx18("tr", { children: row.children.map((cell, ci) => /* @__PURE__ */ jsx18(
3549
+ bodyRows.length > 0 && /* @__PURE__ */ jsx19("tbody", { children: bodyRows.map((row, ri) => /* @__PURE__ */ jsx19("tr", { children: row.children.map((cell, ci) => /* @__PURE__ */ jsx19(
3095
3550
  "td",
3096
3551
  {
3097
3552
  className: "squisq-md-td",
@@ -3109,22 +3564,13 @@ function MdImage({ src, alt, title }) {
3109
3564
  const safeSrc = sanitizeUrl(src, "media");
3110
3565
  const resolved = useMediaUrl(safeSrc ?? "", ".");
3111
3566
  if (!safeSrc) return null;
3112
- return /* @__PURE__ */ jsx18("img", { className: "squisq-md-image", src: resolved, alt, title });
3567
+ return /* @__PURE__ */ jsx19("img", { className: "squisq-md-image", src: resolved, alt, title });
3113
3568
  }
3114
3569
  function resolveHtmlNodes(nodes, htmlPolicy) {
3115
3570
  if (htmlPolicy === "strip") return [];
3116
3571
  if (htmlPolicy === "trusted") return nodes;
3117
3572
  return sanitizeHtmlNodes2(nodes);
3118
3573
  }
3119
- function containsMediaTag(nodes) {
3120
- for (const node of nodes) {
3121
- if (node.type !== "htmlElement") continue;
3122
- const tagName = node.tagName.toLowerCase();
3123
- if (tagName === "video" || tagName === "audio") return true;
3124
- if (containsMediaTag(node.children)) return true;
3125
- }
3126
- return false;
3127
- }
3128
3574
  var DANGEROUS_HTML_TAGS = /* @__PURE__ */ new Set([
3129
3575
  "base",
3130
3576
  "embed",
@@ -3136,18 +3582,6 @@ var DANGEROUS_HTML_TAGS = /* @__PURE__ */ new Set([
3136
3582
  "style",
3137
3583
  "title"
3138
3584
  ]);
3139
- function containsDangerousTag(nodes) {
3140
- for (const node of nodes) {
3141
- if (node.type !== "htmlElement") continue;
3142
- if (DANGEROUS_HTML_TAGS.has(node.tagName.toLowerCase())) return true;
3143
- if (containsDangerousTag(node.children)) return true;
3144
- }
3145
- return false;
3146
- }
3147
- var DANGEROUS_RAW_HTML_RE = /<\s*\/?\s*(?:base|embed|iframe|link|meta|object|script|style|title)\b/i;
3148
- function hasDangerousRawHtml(rawHtml) {
3149
- return DANGEROUS_RAW_HTML_RE.test(rawHtml);
3150
- }
3151
3585
  var PASSTHROUGH_ATTRS = {
3152
3586
  // common
3153
3587
  class: "className",
@@ -3166,7 +3600,7 @@ var PASSTHROUGH_ATTRS = {
3166
3600
  target: "target",
3167
3601
  rel: "rel"
3168
3602
  };
3169
- function reactPropsFromAttrs(attrs) {
3603
+ function reactPropsFromAttrs(attrs, ctx) {
3170
3604
  const out = {};
3171
3605
  for (const [name, value] of Object.entries(attrs)) {
3172
3606
  const propName = PASSTHROUGH_ATTRS[name];
@@ -3175,21 +3609,33 @@ function reactPropsFromAttrs(attrs) {
3175
3609
  out["data-style"] = value;
3176
3610
  continue;
3177
3611
  }
3612
+ if (propName === "href") {
3613
+ const href = sanitizeUrl(value, "link", { extraLinkSchemes: ctx.linkSchemes });
3614
+ if (href) out.href = href;
3615
+ continue;
3616
+ }
3617
+ if (propName === "src") {
3618
+ const src = sanitizeUrl(value, "media");
3619
+ if (src) out.src = src;
3620
+ continue;
3621
+ }
3178
3622
  out[propName] = value;
3179
3623
  }
3180
3624
  return out;
3181
3625
  }
3182
- function renderHtmlElement(el, key) {
3626
+ function renderHtmlElement(el, key, ctx) {
3183
3627
  const tagName = el.tagName.toLowerCase();
3184
3628
  if (DANGEROUS_HTML_TAGS.has(tagName)) return null;
3185
3629
  if (tagName === "video") {
3186
- return /* @__PURE__ */ jsx18(
3630
+ const src = sanitizeUrl(el.attributes.src ?? "", "media") ?? "";
3631
+ const poster = sanitizeUrl(el.attributes.poster ?? "", "media") ?? void 0;
3632
+ return /* @__PURE__ */ jsx19(
3187
3633
  InlineVideoPlayer,
3188
3634
  {
3189
- src: el.attributes.src ?? "",
3635
+ src,
3190
3636
  width: el.attributes.width,
3191
3637
  height: el.attributes.height,
3192
- poster: el.attributes.poster,
3638
+ poster,
3193
3639
  controls: "controls" in el.attributes,
3194
3640
  preload: el.attributes.preload === "none" || el.attributes.preload === "metadata" || el.attributes.preload === "auto" ? el.attributes.preload : void 0
3195
3641
  },
@@ -3197,10 +3643,11 @@ function renderHtmlElement(el, key) {
3197
3643
  );
3198
3644
  }
3199
3645
  if (tagName === "audio") {
3200
- return /* @__PURE__ */ jsx18(
3646
+ const src = sanitizeUrl(el.attributes.src ?? "", "media") ?? "";
3647
+ return /* @__PURE__ */ jsx19(
3201
3648
  InlineAudioPlayer,
3202
3649
  {
3203
- src: el.attributes.src ?? "",
3650
+ src,
3204
3651
  controls: "controls" in el.attributes,
3205
3652
  preload: el.attributes.preload === "none" || el.attributes.preload === "metadata" || el.attributes.preload === "auto" ? el.attributes.preload : void 0
3206
3653
  },
@@ -3208,20 +3655,20 @@ function renderHtmlElement(el, key) {
3208
3655
  );
3209
3656
  }
3210
3657
  const Tag = tagName;
3211
- const props = reactPropsFromAttrs(el.attributes);
3658
+ const props = reactPropsFromAttrs(el.attributes, ctx);
3212
3659
  if (el.selfClosing) {
3213
- return /* @__PURE__ */ jsx18(Tag, { ...props }, key);
3660
+ return /* @__PURE__ */ jsx19(Tag, { ...props }, key);
3214
3661
  }
3215
- return /* @__PURE__ */ jsx18(Tag, { ...props, children: renderHtmlNodes(el.children, `${key}c`) }, key);
3662
+ return /* @__PURE__ */ jsx19(Tag, { ...props, children: renderHtmlNodes(el.children, `${key}c`, ctx) }, key);
3216
3663
  }
3217
- function renderHtmlNodes(nodes, keyPrefix) {
3664
+ function renderHtmlNodes(nodes, keyPrefix, ctx = DEFAULT_CTX) {
3218
3665
  return nodes.map((node, i) => {
3219
3666
  const key = `${keyPrefix}${i}`;
3220
3667
  switch (node.type) {
3221
3668
  case "htmlElement":
3222
- return renderHtmlElement(node, key);
3669
+ return renderHtmlElement(node, key, ctx);
3223
3670
  case "htmlText":
3224
- return /* @__PURE__ */ jsx18(Fragment2, { children: node.value }, key);
3671
+ return /* @__PURE__ */ jsx19(Fragment2, { children: node.value }, key);
3225
3672
  case "htmlComment":
3226
3673
  return null;
3227
3674
  default:
@@ -3236,25 +3683,16 @@ function MarkdownRenderer({
3236
3683
  linkSchemes
3237
3684
  }) {
3238
3685
  if (!nodes || nodes.length === 0) return null;
3239
- return /* @__PURE__ */ jsx18("div", { className: `squisq-md ${className || ""}`, children: renderBlocks(nodes, "", { htmlPolicy, linkSchemes }) });
3686
+ return /* @__PURE__ */ jsx19("div", { className: `squisq-md ${className || ""}`, children: renderBlocks(nodes, "", { htmlPolicy, linkSchemes }) });
3240
3687
  }
3241
3688
 
3242
3689
  // src/LinearDocView.tsx
3243
- import { jsx as jsx19, jsxs as jsxs12 } from "react/jsx-runtime";
3244
- var warnedUnknownTemplates = /* @__PURE__ */ new Set();
3690
+ import { jsx as jsx20, jsxs as jsxs13 } from "react/jsx-runtime";
3245
3691
  function isAnnotatedBlock(block) {
3246
- const annotation = block.sourceHeading?.templateAnnotation;
3247
- if (!annotation?.template) return false;
3248
- if (!hasTemplate(annotation.template)) {
3249
- if (!warnedUnknownTemplates.has(annotation.template)) {
3250
- warnedUnknownTemplates.add(annotation.template);
3251
- console.warn(
3252
- `[squisq] Unknown template "${annotation.template}" \u2014 rendering the block as plain markdown.`
3253
- );
3254
- }
3255
- return false;
3256
- }
3257
- return true;
3692
+ return !!block.sourceHeading?.templateAnnotation?.template || !block.sourceHeading && isTemplateBlock2(block);
3693
+ }
3694
+ function visualTemplateName(block) {
3695
+ return block.sourceHeading?.templateAnnotation?.template ?? block.template;
3258
3696
  }
3259
3697
  function countAll(blocks) {
3260
3698
  let count = 0;
@@ -3264,50 +3702,65 @@ function countAll(blocks) {
3264
3702
  }
3265
3703
  return count;
3266
3704
  }
3267
- function BlockSection({ block, basePath, viewport, renderContext, blockIndex }) {
3705
+ function BlockSection({
3706
+ block,
3707
+ basePath,
3708
+ viewport,
3709
+ renderContext,
3710
+ blockIndex,
3711
+ blockIndices,
3712
+ animationsEnabled
3713
+ }) {
3268
3714
  const isAnnotated = isAnnotatedBlock(block);
3269
3715
  const visualBlock = useMemo8(() => {
3270
3716
  if (!isAnnotated) return null;
3271
- const annotation = block.sourceHeading.templateAnnotation;
3272
- const headingText = extractPlainText(block.sourceHeading);
3273
- const templateBlock = {
3274
- id: block.id,
3275
- template: annotation.template,
3276
- startTime: 0,
3277
- duration: 1,
3278
- audioSegment: 0,
3279
- title: headingText,
3280
- ...deriveTemplateInputs(
3281
- annotation.template ?? "sectionHeader",
3282
- headingText,
3283
- block.contents,
3284
- {
3717
+ const annotation = block.sourceHeading?.templateAnnotation;
3718
+ const templateName = visualTemplateName(block) ?? "sectionHeader";
3719
+ const templateBlock = annotation ? (() => {
3720
+ const headingText = extractPlainText(block.sourceHeading);
3721
+ return {
3722
+ id: block.id,
3723
+ template: templateName,
3724
+ startTime: 0,
3725
+ duration: 1,
3726
+ audioSegment: 0,
3727
+ title: headingText,
3728
+ contents: block.contents,
3729
+ children: block.children,
3730
+ ...deriveTemplateInputs(templateName, headingText, block.contents, {
3285
3731
  placeholders: true
3286
- }
3287
- ) ?? {},
3288
- ...annotation.params,
3289
- ...block.templateOverrides
3732
+ }) ?? {},
3733
+ ...annotation.params,
3734
+ ...block.templateOverrides
3735
+ };
3736
+ })() : {
3737
+ ...block,
3738
+ startTime: block.startTime ?? 0,
3739
+ duration: block.duration ?? 1,
3740
+ audioSegment: block.audioSegment ?? 0,
3741
+ template: templateName
3290
3742
  };
3291
3743
  const ctx = {
3292
3744
  ...renderContext,
3293
3745
  blockIndex
3294
3746
  };
3295
- const layers = getLayers(templateBlock, ctx);
3747
+ const { layers } = materializeBlockLayers(templateBlock, ctx);
3296
3748
  return {
3297
3749
  ...block,
3298
3750
  layers,
3299
- template: annotation.template
3751
+ template: templateName
3300
3752
  };
3301
3753
  }, [block, isAnnotated, renderContext, blockIndex]);
3302
- return /* @__PURE__ */ jsxs12(
3754
+ return /* @__PURE__ */ jsxs13(
3303
3755
  "div",
3304
3756
  {
3305
3757
  className: "squisq-linear-section",
3306
3758
  "data-block-id": block.id,
3307
- "data-template": isAnnotated ? block.sourceHeading?.templateAnnotation?.template : void 0,
3759
+ "data-block-index": blockIndex,
3760
+ "data-template": isAnnotated ? visualTemplateName(block) : void 0,
3308
3761
  children: [
3309
- block.sourceHeading && !isAnnotated && /* @__PURE__ */ jsx19(MarkdownRenderer, { nodes: [block.sourceHeading] }),
3310
- isAnnotated && visualBlock && /* @__PURE__ */ jsx19("div", { className: "squisq-linear-card", children: /* @__PURE__ */ jsx19(
3762
+ block.sourceHeading && !isAnnotated && /* @__PURE__ */ jsx20(MarkdownRenderer, { nodes: [block.sourceHeading] }),
3763
+ isAnnotated && visualBlock && /* @__PURE__ */ jsx20("div", { className: "squisq-linear-card", children: /* @__PURE__ */ jsx20(
3311
3764
  "div",
3312
3765
  {
3313
3766
  className: "squisq-linear-card-svg",
@@ -3317,26 +3770,29 @@ function BlockSection({ block, basePath, viewport, renderContext, blockIndex })
3317
3770
  overflow: "hidden",
3318
3771
  marginBottom: "1em"
3319
3772
  },
3320
- children: /* @__PURE__ */ jsx19(
3773
+ children: /* @__PURE__ */ jsx20(
3321
3774
  BlockRenderer,
3322
3775
  {
3323
3776
  block: visualBlock,
3324
3777
  blockTime: 0,
3325
3778
  basePath,
3326
- viewport
3779
+ viewport,
3780
+ animationsEnabled
3327
3781
  }
3328
3782
  )
3329
3783
  }
3330
3784
  ) }),
3331
- !isAnnotated && block.contents && block.contents.length > 0 && /* @__PURE__ */ jsx19(MarkdownRenderer, { nodes: block.contents }),
3332
- block.children && block.children.length > 0 && /* @__PURE__ */ jsx19("div", { className: "squisq-linear-children", children: block.children.map((child, i) => /* @__PURE__ */ jsx19(
3785
+ !isAnnotated && block.contents && block.contents.length > 0 && /* @__PURE__ */ jsx20(MarkdownRenderer, { nodes: block.contents }),
3786
+ block.children && block.children.length > 0 && /* @__PURE__ */ jsx20("div", { className: "squisq-linear-children", children: block.children.map((child, i) => /* @__PURE__ */ jsx20(
3333
3787
  BlockSection,
3334
3788
  {
3335
3789
  block: child,
3336
3790
  basePath,
3337
3791
  viewport,
3338
3792
  renderContext,
3339
- blockIndex: blockIndex + i + 1
3793
+ blockIndex: blockIndices.get(child) ?? blockIndex + i + 1,
3794
+ blockIndices,
3795
+ animationsEnabled
3340
3796
  },
3341
3797
  child.id
3342
3798
  )) })
@@ -3352,9 +3808,12 @@ function LinearDocView({
3352
3808
  className,
3353
3809
  theme,
3354
3810
  surface,
3811
+ animationsEnabled = true,
3355
3812
  thinMargins = false,
3356
- imageDisplayMode = "inline"
3813
+ imageDisplayMode = "inline",
3814
+ globalKeyboardShortcuts = false
3357
3815
  }) {
3816
+ const scrollRef = useRef8(null);
3358
3817
  const activeViewport = viewport ?? VIEWPORT_PRESETS3.landscape;
3359
3818
  const markdownDoc = useMemo8(
3360
3819
  () => !doc && markdown !== void 0 ? markdownToDoc(parseMarkdown(markdown)) : void 0,
@@ -3365,6 +3824,18 @@ function LinearDocView({
3365
3824
  () => resolvedDoc ? countAll(resolvedDoc.blocks) : 0,
3366
3825
  [resolvedDoc]
3367
3826
  );
3827
+ const blockIndices = useMemo8(() => {
3828
+ const indices = /* @__PURE__ */ new Map();
3829
+ let index = 0;
3830
+ const visit = (blocks) => {
3831
+ for (const block of blocks) {
3832
+ indices.set(block, index++);
3833
+ if (block.children) visit(block.children);
3834
+ }
3835
+ };
3836
+ if (resolvedDoc) visit(resolvedDoc.blocks);
3837
+ return indices;
3838
+ }, [resolvedDoc]);
3368
3839
  const autoSurface = useAutoSurface(surface === "auto");
3369
3840
  const resolvedSurface = surface === "auto" ? autoSurface : surface;
3370
3841
  const renderContext = useMemo8(() => {
@@ -3376,12 +3847,37 @@ function LinearDocView({
3376
3847
  totalBlocks,
3377
3848
  // Theme atmosphere (vignette/grain/gradient persistent layers) shows
3378
3849
  // on the inline template cards so they match the player's look.
3379
- persistentLayers: effectiveTheme.persistentLayers
3850
+ persistentLayers: effectiveTheme.persistentLayers,
3851
+ customTemplates: resolvedDoc?.customTemplates
3380
3852
  };
3381
- }, [activeViewport, totalBlocks, theme, resolvedSurface]);
3853
+ }, [activeViewport, resolvedDoc?.customTemplates, totalBlocks, theme, resolvedSurface]);
3382
3854
  const activeTheme = renderContext.theme;
3855
+ useEffect9(() => {
3856
+ if (!globalKeyboardShortcuts) return;
3857
+ const handleKeyDown = (event) => {
3858
+ if (event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey || event.key !== "ArrowDown" && event.key !== "ArrowUp") {
3859
+ return;
3860
+ }
3861
+ const target = event.target instanceof Element ? event.target : null;
3862
+ if (target?.closest(
3863
+ 'input, textarea, select, [contenteditable]:not([contenteditable="false"]), [role="textbox"], [role="combobox"], [role="listbox"], [role="menu"], [role="dialog"], [aria-modal="true"], .monaco-editor'
3864
+ )) {
3865
+ return;
3866
+ }
3867
+ const scroller = scrollRef.current;
3868
+ if (!scroller) return;
3869
+ event.preventDefault();
3870
+ const distance = Math.max(64, Math.round(scroller.clientHeight * 0.12));
3871
+ scroller.scrollBy({
3872
+ top: event.key === "ArrowDown" ? distance : -distance,
3873
+ behavior: "smooth"
3874
+ });
3875
+ };
3876
+ document.addEventListener("keydown", handleKeyDown);
3877
+ return () => document.removeEventListener("keydown", handleKeyDown);
3878
+ }, [globalKeyboardShortcuts]);
3383
3879
  if (!resolvedDoc) {
3384
- return /* @__PURE__ */ jsx19("div", { className: `squisq-linear squisq-linear--empty ${className || ""}` });
3880
+ return /* @__PURE__ */ jsx20("div", { ref: scrollRef, className: `squisq-linear squisq-linear--empty ${className || ""}` });
3385
3881
  }
3386
3882
  const bgColor = activeTheme.colors.background;
3387
3883
  const textColor = activeTheme.colors.text;
@@ -3390,9 +3886,10 @@ function LinearDocView({
3390
3886
  const bodyFont = resolveFontFamily2(activeTheme.typography.bodyFont, "system-ui, sans-serif");
3391
3887
  const titleFont = resolveFontFamily2(activeTheme.typography.titleFont, "Georgia, serif");
3392
3888
  const lineHt = activeTheme.typography.lineHeight ?? 1.7;
3393
- return /* @__PURE__ */ jsx19(
3889
+ return /* @__PURE__ */ jsx20(
3394
3890
  "div",
3395
3891
  {
3892
+ ref: scrollRef,
3396
3893
  className: `squisq-linear ${className || ""}`,
3397
3894
  style: {
3398
3895
  width: "100%",
@@ -3406,7 +3903,7 @@ function LinearDocView({
3406
3903
  overflowX: "hidden",
3407
3904
  background: bgColor
3408
3905
  },
3409
- children: /* @__PURE__ */ jsxs12(
3906
+ children: /* @__PURE__ */ jsxs13(
3410
3907
  "div",
3411
3908
  {
3412
3909
  className: `squisq-linear-content squisq-md${thinMargins ? " squisq-linear-content--thin" : ""}${imageDisplayMode === "thumbnail" ? " squisq-linear-content--thumbnail-images" : ""}`,
@@ -3431,7 +3928,7 @@ function LinearDocView({
3431
3928
  "--squisq-linear-bg": bgColor
3432
3929
  },
3433
3930
  children: [
3434
- /* @__PURE__ */ jsx19("style", { children: `
3931
+ /* @__PURE__ */ jsx20("style", { children: `
3435
3932
  .squisq-linear-content h1,
3436
3933
  .squisq-linear-content h2,
3437
3934
  .squisq-linear-content h3,
@@ -3543,14 +4040,16 @@ function LinearDocView({
3543
4040
  background: color-mix(in srgb, var(--squisq-linear-primary) 8%, transparent);
3544
4041
  }
3545
4042
  ` }),
3546
- resolvedDoc.blocks.map((block, i) => /* @__PURE__ */ jsx19(
4043
+ resolvedDoc.blocks.map((block, i) => /* @__PURE__ */ jsx20(
3547
4044
  BlockSection,
3548
4045
  {
3549
4046
  block,
3550
4047
  basePath,
3551
4048
  viewport: activeViewport,
3552
4049
  renderContext,
3553
- blockIndex: i
4050
+ blockIndex: blockIndices.get(block) ?? i,
4051
+ blockIndices,
4052
+ animationsEnabled
3554
4053
  },
3555
4054
  block.id
3556
4055
  ))
@@ -3562,7 +4061,7 @@ function LinearDocView({
3562
4061
  }
3563
4062
 
3564
4063
  // src/DocPlayer.tsx
3565
- import { jsx as jsx20, jsxs as jsxs13 } from "react/jsx-runtime";
4064
+ import { jsx as jsx21, jsxs as jsxs14 } from "react/jsx-runtime";
3566
4065
  var SMALL_WORDS = /* @__PURE__ */ new Set([
3567
4066
  "a",
3568
4067
  "an",
@@ -3583,7 +4082,7 @@ var SMALL_WORDS = /* @__PURE__ */ new Set([
3583
4082
  function buildSegmentTitleMap(doc) {
3584
4083
  const map = /* @__PURE__ */ new Map();
3585
4084
  for (const block of doc.blocks) {
3586
- if (isTemplateBlock2(block) && block.template === "sectionHeader" && "title" in block) {
4085
+ if (isTemplateBlock3(block) && block.template === "sectionHeader" && "title" in block) {
3587
4086
  const segIdx = block.audioSegment;
3588
4087
  if (!map.has(segIdx)) {
3589
4088
  map.set(segIdx, block.title);
@@ -3624,14 +4123,15 @@ function DocPlayer(props) {
3624
4123
  );
3625
4124
  const resolvedDoc = doc ?? markdownDoc;
3626
4125
  if (!resolvedDoc) {
3627
- return /* @__PURE__ */ jsx20("div", { className: "doc-player doc-player--empty" });
4126
+ return /* @__PURE__ */ jsx21("div", { className: "doc-player doc-player--empty" });
3628
4127
  }
3629
- return /* @__PURE__ */ jsx20(DocPlayerContent, { ...props, doc: resolvedDoc });
4128
+ return /* @__PURE__ */ jsx21(DocPlayerContent, { ...props, doc: resolvedDoc });
3630
4129
  }
3631
4130
  function DocPlayerContent({
3632
4131
  doc,
3633
4132
  basePath = ".",
3634
4133
  renderMode = false,
4134
+ animationsEnabled = true,
3635
4135
  autoPlay = false,
3636
4136
  onEnded,
3637
4137
  onTimeUpdate,
@@ -3643,23 +4143,27 @@ function DocPlayerContent({
3643
4143
  onCaptionsToggle,
3644
4144
  onPlaybackStateChange,
3645
4145
  onControlsReady,
4146
+ onRenderAPIReady,
3646
4147
  isFullscreen = false,
3647
4148
  onFullscreenToggle,
3648
4149
  onBlockMarkers,
3649
4150
  forceViewport,
3650
4151
  displayMode = "video",
3651
4152
  showCoverSlide = true,
4153
+ coverVisible,
3652
4154
  theme,
3653
4155
  surface,
3654
4156
  captionStyle = "standard",
3655
- enableSwipe = true
4157
+ enableSwipe = true,
4158
+ globalKeyboardShortcuts = false
3656
4159
  }) {
3657
4160
  const isSlideshowMode = displayMode === "slideshow";
3658
4161
  const isLinearMode = displayMode === "linear";
3659
- const audioRef = useRef7(null);
3660
- const containerRef = useRef7(null);
3661
- const [tapFeedback, setTapFeedback] = useState7(null);
3662
- const tapFeedbackTimer = useRef7();
4162
+ const audioRef = useRef9(null);
4163
+ const containerRef = useRef9(null);
4164
+ const playerId = `squisq-player-${useId7().replace(/:/g, "")}`;
4165
+ const [tapFeedback, setTapFeedback] = useState9(null);
4166
+ const tapFeedbackTimer = useRef9();
3663
4167
  const { viewport, orientation } = useViewportOrientation();
3664
4168
  const activeViewport = forceViewport || (renderMode ? VIEWPORT_PRESETS4.landscape : viewport);
3665
4169
  const isDebugMode = useMemo9(() => {
@@ -3667,9 +4171,9 @@ function DocPlayerContent({
3667
4171
  const params = new URLSearchParams(window.location.search);
3668
4172
  return params.get("debug") === "true";
3669
4173
  }, []);
3670
- const internalAudio = useAudioSync(audioRef, doc.audio, basePath);
4174
+ const internalAudio = useAudioSync(audioRef, doc.audio, basePath, !externalAudioController);
3671
4175
  const audio = externalAudioController || internalAudio;
3672
- useEffect8(() => {
4176
+ useEffect10(() => {
3673
4177
  if (warnedMissingStyles || !isDevEnvironment()) return;
3674
4178
  const el = containerRef.current;
3675
4179
  if (!el || typeof getComputedStyle !== "function") return;
@@ -3698,21 +4202,28 @@ function DocPlayerContent({
3698
4202
  restart
3699
4203
  } = audio;
3700
4204
  const mediaSchedule = useMemo9(() => resolveMediaSchedule(doc), [doc]);
3701
- const currentTimeRef = useRef7(currentTime);
4205
+ const currentTimeRef = useRef9(currentTime);
3702
4206
  currentTimeRef.current = currentTime;
3703
- const totalDurationRef = useRef7(totalDuration);
4207
+ const totalDurationRef = useRef9(totalDuration);
3704
4208
  totalDurationRef.current = totalDuration;
3705
- const expandedBlocksLenRef = useRef7(0);
3706
- const handleContainerClick = useCallback6(
4209
+ const expandedBlocksLenRef = useRef9(0);
4210
+ const handleContainerClick = useCallback7(
3707
4211
  (e) => {
3708
- if (renderMode || isSlideshowMode || isLinearMode) return;
4212
+ if (renderMode || isLinearMode) return;
3709
4213
  const target = e.target;
4214
+ if (isSlideshowMode) {
4215
+ if (!target.closest('button, a, input, textarea, select, [contenteditable="true"]')) {
4216
+ containerRef.current?.focus({ preventScroll: true });
4217
+ }
4218
+ return;
4219
+ }
3710
4220
  if (target.closest(
3711
- "button, a, input, .doc-player__controls, .doc-player__scrubber, .doc-controls-sidebar, .doc-controls-slideshow"
4221
+ 'button, a, input, textarea, select, [contenteditable="true"], .doc-player__controls, .doc-player__scrubber, .doc-controls-sidebar, .doc-controls-slideshow'
3712
4222
  ))
3713
4223
  return;
4224
+ containerRef.current?.focus({ preventScroll: true });
3714
4225
  toggle();
3715
- const nextState = isPlaying ? "play" : "pause";
4226
+ const nextState = isPlaying ? "pause" : "play";
3716
4227
  setTapFeedback(nextState);
3717
4228
  clearTimeout(tapFeedbackTimer.current);
3718
4229
  tapFeedbackTimer.current = setTimeout(() => setTapFeedback(null), 600);
@@ -3736,8 +4247,13 @@ function DocPlayerContent({
3736
4247
  docProgress,
3737
4248
  nextBlock: _nextBlock,
3738
4249
  prevBlock: _prevBlock,
3739
- blocks: expandedBlocks
3740
- } = useDocPlayback(doc, currentTime, activeViewport, renderMode, effectiveTheme);
4250
+ blocks: expandedBlocks,
4251
+ suppressOutgoingForNextBlock
4252
+ } = useDocPlayback(doc, currentTime, {
4253
+ viewport: activeViewport,
4254
+ theme: effectiveTheme,
4255
+ onSeek: seekTo
4256
+ });
3741
4257
  const coverBlock = useMemo9(() => {
3742
4258
  const startBlockConfig = doc.startBlock;
3743
4259
  if (!showCoverSlide) return null;
@@ -3755,9 +4271,13 @@ function DocPlayerContent({
3755
4271
  };
3756
4272
  }, [doc.startBlock, activeViewport, effectiveTheme, showCoverSlide]);
3757
4273
  const hasManagedCover = !!coverBlock;
3758
- const [slideshowCoverVisible, setSlideshowCoverVisible] = useState7(false);
3759
- const slideshowCoverInitKeyRef = useRef7("");
3760
- useEffect8(() => {
4274
+ const [slideshowCoverVisible, setSlideshowCoverVisible] = useState9(false);
4275
+ const [isSlideshowPickerOpen, setIsSlideshowPickerOpen] = useState9(false);
4276
+ const slideshowCoverInitKeyRef = useRef9("");
4277
+ useEffect10(() => {
4278
+ slideshowCoverInitKeyRef.current = "";
4279
+ }, [doc]);
4280
+ useEffect10(() => {
3761
4281
  const initKey = `${isSlideshowMode}:${hasManagedCover}:${renderMode}`;
3762
4282
  if (slideshowCoverInitKeyRef.current === initKey) return;
3763
4283
  slideshowCoverInitKeyRef.current = initKey;
@@ -3768,14 +4288,21 @@ function DocPlayerContent({
3768
4288
  setSlideshowCoverVisible(false);
3769
4289
  }
3770
4290
  }, [isSlideshowMode, hasManagedCover, renderMode, pause]);
3771
- const [coverForced, setCoverForced] = useState7(false);
3772
- const [coverGraceActive, setCoverGraceActive] = useState7(false);
3773
- const coverGraceTimer = useRef7();
3774
- const coverWasShowing = useRef7(false);
3775
- const hasPlayedOnce = useRef7(false);
4291
+ const [coverForced, setCoverForced] = useState9(false);
4292
+ const [coverGraceActive, setCoverGraceActive] = useState9(false);
4293
+ const coverGraceTimer = useRef9();
4294
+ const coverWasShowing = useRef9(false);
4295
+ const hasPlayedOnce = useRef9(false);
4296
+ useEffect10(() => {
4297
+ hasPlayedOnce.current = false;
4298
+ coverWasShowing.current = false;
4299
+ clearTimeout(coverGraceTimer.current);
4300
+ setCoverGraceActive(false);
4301
+ setCoverForced(false);
4302
+ }, [doc]);
3776
4303
  const atRest = !!(coverBlock && !isSlideshowMode && !isPlaying && currentTime === 0 && !hasPlayedOnce.current && !renderMode && !autoPlay);
3777
4304
  if (atRest) coverWasShowing.current = true;
3778
- useEffect8(() => {
4305
+ useEffect10(() => {
3779
4306
  if (isPlaying && coverWasShowing.current && coverBlock && !renderMode && !isSlideshowMode) {
3780
4307
  coverWasShowing.current = false;
3781
4308
  hasPlayedOnce.current = true;
@@ -3783,88 +4310,96 @@ function DocPlayerContent({
3783
4310
  coverGraceTimer.current = setTimeout(() => setCoverGraceActive(false), 3e3);
3784
4311
  }
3785
4312
  }, [isPlaying, coverBlock, renderMode, isSlideshowMode]);
3786
- useEffect8(() => () => clearTimeout(coverGraceTimer.current), []);
4313
+ useEffect10(() => () => clearTimeout(coverGraceTimer.current), []);
3787
4314
  const showVideoCoverBlock = !isSlideshowMode && !isLinearMode && !!coverBlock && (coverForced || coverGraceActive || !isPlaying && currentTime === 0 && !hasPlayedOnce.current && !renderMode && !autoPlay);
3788
- const showSlideshowCover = !!(isSlideshowMode && !isLinearMode && !renderMode && coverBlock && slideshowCoverVisible);
3789
- const showCoverBlock = showVideoCoverBlock || showSlideshowCover;
4315
+ const effectiveSlideshowCoverVisible = coverVisible ?? slideshowCoverVisible;
4316
+ const showSlideshowCover = !!(isSlideshowMode && !isLinearMode && !renderMode && coverBlock && effectiveSlideshowCoverVisible);
4317
+ const showCoverBlock = coverVisible === void 0 ? showVideoCoverBlock || showSlideshowCover : !!coverBlock && coverVisible;
3790
4318
  const slideshowHasCover = !!(isSlideshowMode && !renderMode && coverBlock);
3791
- const slideshowSlideIndex = slideshowHasCover ? slideshowCoverVisible ? 0 : currentBlockIndex + 1 : currentBlockIndex;
4319
+ const slideshowSlideIndex = slideshowHasCover ? effectiveSlideshowCoverVisible ? 0 : currentBlockIndex + 1 : currentBlockIndex;
3792
4320
  const slideshowTotalSlides = slideshowHasCover ? expandedBlocks.length + 1 : expandedBlocks.length;
3793
- const hasAutoPlayed = useRef7(false);
3794
- useEffect8(() => {
4321
+ const hasAutoPlayed = useRef9(false);
4322
+ useEffect10(() => {
4323
+ hasAutoPlayed.current = false;
4324
+ }, [doc]);
4325
+ useEffect10(() => {
3795
4326
  if (isAudioReady && autoPlay && !hasAutoPlayed.current) {
3796
4327
  hasAutoPlayed.current = true;
3797
4328
  play();
3798
4329
  }
3799
4330
  }, [isAudioReady, autoPlay, play]);
3800
- useEffect8(() => {
4331
+ useEffect10(() => {
3801
4332
  onTimeUpdate?.(currentTime);
3802
4333
  }, [currentTime, onTimeUpdate]);
3803
- useEffect8(() => {
4334
+ useEffect10(() => {
3804
4335
  if (isEnded) {
3805
4336
  onEnded?.();
3806
4337
  }
3807
4338
  }, [isEnded, onEnded]);
3808
- useEffect8(() => {
3809
- if ((renderMode || isDebugMode) && typeof window !== "undefined") {
3810
- const w = window;
3811
- w.seekTo = (time) => {
3812
- seekTo(time);
3813
- return new Promise((resolve) => {
3814
- requestAnimationFrame(() => {
3815
- let blockStartTime = 0;
3816
- for (let i = expandedBlocks.length - 1; i >= 0; i--) {
3817
- if (time >= expandedBlocks[i].startTime) {
3818
- blockStartTime = expandedBlocks[i].startTime;
3819
- break;
3820
- }
4339
+ const liveRenderAPIRef = useRef9(null);
4340
+ const stableRenderAPIRef = useRef9(null);
4341
+ if (!stableRenderAPIRef.current) {
4342
+ const current = () => {
4343
+ const api = liveRenderAPIRef.current;
4344
+ if (!api) throw new Error("Squisq render API is not currently available.");
4345
+ return api;
4346
+ };
4347
+ stableRenderAPIRef.current = {
4348
+ seekTo: (time) => current().seekTo(time),
4349
+ getDuration: () => current().getDuration(),
4350
+ getBlocks: () => current().getBlocks(),
4351
+ getAudioSegments: () => current().getAudioSegments(),
4352
+ getCaptions: () => current().getCaptions(),
4353
+ getChapters: () => current().getChapters(),
4354
+ showCover: () => current().showCover(),
4355
+ hideCover: () => current().hideCover(),
4356
+ hasCoverBlock: () => current().hasCoverBlock()
4357
+ };
4358
+ }
4359
+ const stableRenderAPI = stableRenderAPIRef.current;
4360
+ useEffect10(() => {
4361
+ if (!renderMode && !isDebugMode) {
4362
+ liveRenderAPIRef.current = null;
4363
+ return;
4364
+ }
4365
+ const root = containerRef.current;
4366
+ if (!root) {
4367
+ liveRenderAPIRef.current = null;
4368
+ return;
4369
+ }
4370
+ const renderSeekTo = (time) => {
4371
+ seekTo(time);
4372
+ return new Promise((resolve) => {
4373
+ requestAnimationFrame(() => {
4374
+ let blockStartTime = 0;
4375
+ for (let i = expandedBlocks.length - 1; i >= 0; i--) {
4376
+ if (time >= expandedBlocks[i].startTime) {
4377
+ blockStartTime = expandedBlocks[i].startTime;
4378
+ break;
3821
4379
  }
3822
- const elapsedMs = (time - blockStartTime) * 1e3;
3823
- document.getAnimations().forEach((anim) => {
3824
- const target = anim.effect?.target;
3825
- if (!target) return;
3826
- if (target.closest(".doc-player__block--active")) {
3827
- anim.currentTime = Math.max(0, elapsedMs);
3828
- } else if (target.closest(".doc-player__block--previous")) {
3829
- anim.currentTime = Math.max(0, elapsedMs);
3830
- }
3831
- });
3832
- const blockElapsed = time - blockStartTime;
3833
- const videoSeekPromises = [];
3834
- const activeBlockEl = document.querySelector(".doc-player__block--active");
3835
- if (activeBlockEl) {
3836
- const videos = activeBlockEl.querySelectorAll("video[data-clip-start]");
3837
- videos.forEach((el) => {
3838
- const video = el;
3839
- const clipStart = parseFloat(video.dataset.clipStart || "0");
3840
- const clipEnd = parseFloat(video.dataset.clipEnd || "0");
3841
- const startAt = parseFloat(video.dataset.startAt || "0");
3842
- const targetTime = Math.min(
3843
- clipStart + Math.max(0, blockElapsed - startAt),
3844
- clipEnd
3845
- );
3846
- video.pause();
3847
- video.currentTime = targetTime;
3848
- videoSeekPromises.push(
3849
- new Promise((r) => {
3850
- if (Math.abs(video.currentTime - targetTime) < 0.1) {
3851
- r();
3852
- } else {
3853
- video.addEventListener("seeked", () => r(), { once: true });
3854
- setTimeout(r, 200);
3855
- }
3856
- })
3857
- );
3858
- });
4380
+ }
4381
+ const elapsedMs = (time - blockStartTime) * 1e3;
4382
+ (root.getAnimations?.() ?? []).forEach((anim) => {
4383
+ const target = anim.effect?.target;
4384
+ if (!target) return;
4385
+ if (target.closest(".doc-player__block--active")) {
4386
+ anim.currentTime = Math.max(0, elapsedMs);
4387
+ } else if (target.closest(".doc-player__block--previous")) {
4388
+ anim.currentTime = Math.max(0, elapsedMs);
3859
4389
  }
3860
- document.querySelectorAll("video[data-clip-id]").forEach((el) => {
4390
+ });
4391
+ const blockElapsed = time - blockStartTime;
4392
+ const videoSeekPromises = [];
4393
+ const activeBlockEl = root.querySelector(".doc-player__block--active");
4394
+ if (activeBlockEl) {
4395
+ const videos = activeBlockEl.querySelectorAll("video[data-clip-start]");
4396
+ videos.forEach((el) => {
3861
4397
  const video = el;
3862
- const absStart = parseFloat(video.dataset.absStart || "0");
3863
- const absEnd = parseFloat(video.dataset.absEnd || "0");
3864
- const sourceIn = parseFloat(video.dataset.sourceIn || "0");
4398
+ const clipStart = parseFloat(video.dataset.clipStart || "0");
4399
+ const clipEnd = parseFloat(video.dataset.clipEnd || "0");
4400
+ const startAt = parseFloat(video.dataset.startAt || "0");
4401
+ const targetTime = Math.min(clipStart + Math.max(0, blockElapsed - startAt), clipEnd);
3865
4402
  video.pause();
3866
- if (time < absStart || time >= absEnd) return;
3867
- const targetTime = sourceIn + (time - absStart);
3868
4403
  video.currentTime = targetTime;
3869
4404
  videoSeekPromises.push(
3870
4405
  new Promise((r) => {
@@ -3877,82 +4412,111 @@ function DocPlayerContent({
3877
4412
  })
3878
4413
  );
3879
4414
  });
3880
- Promise.all(videoSeekPromises).then(() => {
3881
- requestAnimationFrame(() => resolve());
3882
- });
4415
+ }
4416
+ root.querySelectorAll("video[data-clip-id]").forEach((el) => {
4417
+ const video = el;
4418
+ const absStart = parseFloat(video.dataset.absStart || "0");
4419
+ const absEnd = parseFloat(video.dataset.absEnd || "0");
4420
+ const sourceIn = parseFloat(video.dataset.sourceIn || "0");
4421
+ video.pause();
4422
+ if (time < absStart || time >= absEnd) return;
4423
+ const targetTime = sourceIn + (time - absStart);
4424
+ video.currentTime = targetTime;
4425
+ videoSeekPromises.push(
4426
+ new Promise((r) => {
4427
+ if (Math.abs(video.currentTime - targetTime) < 0.1) {
4428
+ r();
4429
+ } else {
4430
+ video.addEventListener("seeked", () => r(), { once: true });
4431
+ setTimeout(r, 200);
4432
+ }
4433
+ })
4434
+ );
4435
+ });
4436
+ Promise.all(videoSeekPromises).then(() => {
4437
+ requestAnimationFrame(() => resolve());
3883
4438
  });
3884
4439
  });
3885
- };
3886
- w.getDuration = () => {
3887
- const mediaDuration = getDocPlaybackDuration(doc);
3888
- if (totalDuration > 0) return Math.max(totalDuration, mediaDuration);
3889
- return mediaDuration;
3890
- };
3891
- w.getBlocks = () => expandedBlocks.map((s) => ({
3892
- id: s.id,
3893
- template: s.template ?? "raw",
3894
- startTime: s.startTime,
3895
- duration: s.duration
3896
- }));
3897
- w.getAudioSegments = () => doc.audio.segments.map((seg) => ({
3898
- src: seg.src,
3899
- name: seg.name,
3900
- duration: seg.duration,
3901
- startTime: seg.startTime
4440
+ });
4441
+ };
4442
+ const getDuration = () => {
4443
+ const mediaDuration = getDocPlaybackDuration(doc);
4444
+ if (totalDuration > 0) return Math.max(totalDuration, mediaDuration);
4445
+ return mediaDuration;
4446
+ };
4447
+ const getBlocks = () => expandedBlocks.map((s) => ({
4448
+ id: s.id,
4449
+ template: s.template ?? "raw",
4450
+ startTime: s.startTime,
4451
+ duration: s.duration
4452
+ }));
4453
+ const getAudioSegments = () => doc.audio.segments.map((seg) => ({
4454
+ src: seg.src,
4455
+ name: seg.name,
4456
+ duration: seg.duration,
4457
+ startTime: seg.startTime
4458
+ }));
4459
+ const getCaptions = () => doc.captions?.phrases?.map((p) => ({
4460
+ text: p.text,
4461
+ startTime: p.startTime,
4462
+ endTime: p.endTime
4463
+ })) || [];
4464
+ const getChapters = () => {
4465
+ const titleMap = buildSegmentTitleMap(doc);
4466
+ return doc.audio.segments.map((seg, i) => ({
4467
+ title: titleMap.get(i) || seg.name,
4468
+ startTime: seg.startTime,
4469
+ duration: seg.duration
3902
4470
  }));
3903
- w.getCaptions = () => doc.captions?.phrases?.map((p) => ({
3904
- text: p.text,
3905
- startTime: p.startTime,
3906
- endTime: p.endTime
3907
- })) || [];
3908
- w.getChapters = () => {
3909
- const titleMap = buildSegmentTitleMap(doc);
3910
- return doc.audio.segments.map((seg, i) => ({
3911
- title: titleMap.get(i) || seg.name,
3912
- startTime: seg.startTime,
3913
- duration: seg.duration
3914
- }));
3915
- };
3916
- w.showCover = () => {
3917
- setCoverForced(true);
3918
- return new Promise((resolve) => requestAnimationFrame(() => resolve()));
3919
- };
3920
- w.hideCover = () => {
3921
- setCoverForced(false);
3922
- return new Promise((resolve) => requestAnimationFrame(() => resolve()));
3923
- };
3924
- w.hasCoverBlock = () => !!coverBlock;
3925
- }
4471
+ };
4472
+ const showCover = () => {
4473
+ setCoverForced(true);
4474
+ return new Promise((resolve) => requestAnimationFrame(() => resolve()));
4475
+ };
4476
+ const hideCover = () => {
4477
+ setCoverForced(false);
4478
+ return new Promise((resolve) => requestAnimationFrame(() => resolve()));
4479
+ };
4480
+ const hasCoverBlock = () => !!coverBlock;
4481
+ const api = {
4482
+ seekTo: renderSeekTo,
4483
+ getDuration,
4484
+ getBlocks,
4485
+ getAudioSegments,
4486
+ getCaptions,
4487
+ getChapters,
4488
+ showCover,
4489
+ hideCover,
4490
+ hasCoverBlock
4491
+ };
4492
+ liveRenderAPIRef.current = api;
3926
4493
  return () => {
3927
- if (typeof window !== "undefined") {
3928
- const w = window;
3929
- delete w.seekTo;
3930
- delete w.getDuration;
3931
- delete w.getBlocks;
3932
- delete w.getAudioSegments;
3933
- delete w.getCaptions;
3934
- delete w.getChapters;
3935
- delete w.showCover;
3936
- delete w.hideCover;
3937
- delete w.hasCoverBlock;
3938
- }
4494
+ if (liveRenderAPIRef.current === api) liveRenderAPIRef.current = null;
3939
4495
  };
3940
- }, [renderMode, isDebugMode, seekTo, totalDuration, expandedBlocks, coverBlock]);
4496
+ }, [renderMode, isDebugMode, seekTo, totalDuration, expandedBlocks, coverBlock, doc]);
4497
+ useEffect10(() => {
4498
+ if (!renderMode && !isDebugMode || !containerRef.current) {
4499
+ onRenderAPIReady?.(null);
4500
+ return;
4501
+ }
4502
+ onRenderAPIReady?.(stableRenderAPI);
4503
+ return () => onRenderAPIReady?.(null);
4504
+ }, [renderMode, isDebugMode, onRenderAPIReady, stableRenderAPI]);
3941
4505
  const defaultMode = captionsEnabledProp === false ? "off" : captionStyle || "standard";
3942
- const [captionMode, setCaptionMode] = useState7(defaultMode);
3943
- useEffect8(() => {
4506
+ const [captionMode, setCaptionMode] = useState9(defaultMode);
4507
+ useEffect10(() => {
3944
4508
  setCaptionMode(defaultMode);
3945
4509
  }, [defaultMode]);
3946
4510
  const captionsEnabled = captionMode !== "off";
3947
4511
  const activeCaptionStyle = captionMode === "social" ? "social" : "standard";
3948
- const setCaptionsEnabled = useCallback6(
4512
+ const setCaptionsEnabled = useCallback7(
3949
4513
  (enabled) => {
3950
4514
  setCaptionMode(enabled ? captionStyle || "standard" : "off");
3951
4515
  onCaptionsToggle?.(enabled);
3952
4516
  },
3953
4517
  [onCaptionsToggle, captionStyle]
3954
4518
  );
3955
- const cycleCaptionMode = useCallback6(() => {
4519
+ const cycleCaptionMode = useCallback7(() => {
3956
4520
  setCaptionMode((prev) => {
3957
4521
  const next = prev === "off" ? "standard" : prev === "standard" ? "social" : "off";
3958
4522
  onCaptionsToggle?.(next !== "off");
@@ -3966,6 +4530,7 @@ function DocPlayerContent({
3966
4530
  isPlaying,
3967
4531
  currentTime,
3968
4532
  totalDuration,
4533
+ isCoverVisible: showCoverBlock,
3969
4534
  currentBlockIndex: slideshowSlideIndex,
3970
4535
  totalBlocks: slideshowTotalSlides,
3971
4536
  docProgress,
@@ -3985,6 +4550,7 @@ function DocPlayerContent({
3985
4550
  isPlaying,
3986
4551
  currentTime,
3987
4552
  totalDuration,
4553
+ showCoverBlock,
3988
4554
  slideshowSlideIndex,
3989
4555
  slideshowTotalSlides,
3990
4556
  docProgress,
@@ -4078,23 +4644,39 @@ function DocPlayerContent({
4078
4644
  [currentBlockIndex, expandedBlocks, seekTo, pause, slideshowHasCover, slideshowCoverVisible]
4079
4645
  );
4080
4646
  const swipeEnabled = isSlideshowMode && !isLinearMode && !renderMode && enableSwipe;
4647
+ const armContextFreeSwipeEntry = useCallback7(
4648
+ (destinationSlideIndex) => {
4649
+ const destinationBlockIndex = destinationSlideIndex - (slideshowHasCover ? 1 : 0);
4650
+ const destinationBlock = expandedBlocks[destinationBlockIndex];
4651
+ if (destinationBlock) suppressOutgoingForNextBlock(destinationBlock.id);
4652
+ },
4653
+ [expandedBlocks, slideshowHasCover, suppressOutgoingForNextBlock]
4654
+ );
4655
+ const handleSwipeNext = useCallback7(() => {
4656
+ armContextFreeSwipeEntry(slideshowSlideIndex + 1);
4657
+ slideNavActions.nextSlide();
4658
+ }, [armContextFreeSwipeEntry, slideshowSlideIndex, slideNavActions]);
4659
+ const handleSwipePrev = useCallback7(() => {
4660
+ armContextFreeSwipeEntry(slideshowSlideIndex - 1);
4661
+ slideNavActions.prevSlide();
4662
+ }, [armContextFreeSwipeEntry, slideshowSlideIndex, slideNavActions]);
4081
4663
  const swipe = useSlideSwipe({
4082
4664
  enabled: swipeEnabled,
4083
4665
  containerRef,
4084
4666
  canGoNext: slideshowSlideIndex < slideshowTotalSlides - 1,
4085
4667
  canGoPrev: slideshowSlideIndex > 0,
4086
- onNext: slideNavActions.nextSlide,
4087
- onPrev: slideNavActions.prevSlide
4668
+ onNext: handleSwipeNext,
4669
+ onPrev: handleSwipePrev
4088
4670
  });
4089
- useEffect8(() => {
4671
+ useEffect10(() => {
4090
4672
  onPlaybackStateChange?.(playbackState);
4091
4673
  }, [playbackState, onPlaybackStateChange]);
4092
- useEffect8(() => {
4674
+ useEffect10(() => {
4093
4675
  onControlsReady?.({ play, pause, ...playbackActions });
4094
4676
  }, [play, pause, playbackActions, onControlsReady]);
4095
- const getBlockTitle = useCallback6((block) => {
4677
+ const getBlockTitle = useCallback7((block) => {
4096
4678
  const docBlock = block;
4097
- if (isTemplateBlock2(docBlock)) {
4679
+ if (isTemplateBlock3(docBlock)) {
4098
4680
  const props = docBlock;
4099
4681
  if (typeof props.title === "string") return props.title;
4100
4682
  if (typeof props.stat === "string") return props.stat;
@@ -4116,6 +4698,22 @@ function DocPlayerContent({
4116
4698
  }
4117
4699
  return block.id.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
4118
4700
  }, []);
4701
+ const slideshowPickerItems = useMemo9(() => {
4702
+ const blockItems = expandedBlocks.map((block, index) => ({
4703
+ id: block.id,
4704
+ label: String(index + 1),
4705
+ summary: getBlockTitle(block)
4706
+ }));
4707
+ if (!slideshowHasCover || !coverBlock) return blockItems;
4708
+ return [
4709
+ {
4710
+ id: "__cover__",
4711
+ label: "Cover",
4712
+ summary: getBlockTitle(coverBlock)
4713
+ },
4714
+ ...blockItems
4715
+ ];
4716
+ }, [coverBlock, expandedBlocks, getBlockTitle, slideshowHasCover]);
4119
4717
  const blockMarkers = useMemo9(() => {
4120
4718
  if (!totalDuration || !expandedBlocks.length) return [];
4121
4719
  let prevSegment = -1;
@@ -4131,16 +4729,26 @@ function DocPlayerContent({
4131
4729
  };
4132
4730
  });
4133
4731
  }, [expandedBlocks, totalDuration, getBlockTitle]);
4134
- useEffect8(() => {
4732
+ useEffect10(() => {
4135
4733
  if (blockMarkers.length > 0) {
4136
4734
  onBlockMarkers?.(blockMarkers);
4137
4735
  }
4138
4736
  }, [blockMarkers, onBlockMarkers]);
4139
4737
  expandedBlocksLenRef.current = isSlideshowMode ? slideshowTotalSlides : expandedBlocks.length;
4140
- const handleKeyDown = useCallback6(
4141
- (e) => {
4142
- const activeEl = document.activeElement;
4143
- if (activeEl && (activeEl.tagName === "INPUT" || activeEl.tagName === "TEXTAREA" || activeEl.tagName === "SELECT")) {
4738
+ const handleKeyboardShortcut = useCallback7(
4739
+ (e, global) => {
4740
+ if (e.defaultPrevented || e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return;
4741
+ const target = e.target instanceof Element ? e.target : null;
4742
+ const isEditableTarget = !!target?.closest(
4743
+ 'input, textarea, select, [contenteditable]:not([contenteditable="false"]), [role="textbox"], [role="combobox"], [role="listbox"], [role="slider"], [role="spinbutton"], .monaco-editor'
4744
+ );
4745
+ const isOpenInteractionTarget = !!target?.closest(
4746
+ '[role="menu"], [role="dialog"], [aria-modal="true"]'
4747
+ );
4748
+ const isSlideshowToolbarTarget = isSlideshowMode && !!target?.closest(".doc-controls-slideshow") && !target.closest('[role="menu"]');
4749
+ if (isEditableTarget || global && isOpenInteractionTarget || !global && !!target?.closest(
4750
+ 'input, textarea, select, button, a, [contenteditable]:not([contenteditable="false"]), [role="textbox"]'
4751
+ ) && !isSlideshowToolbarTarget) {
4144
4752
  return;
4145
4753
  }
4146
4754
  if (isLinearMode) return;
@@ -4153,10 +4761,13 @@ function DocPlayerContent({
4153
4761
  slideNavActions.nextSlide();
4154
4762
  break;
4155
4763
  case "ArrowLeft":
4156
- case "ArrowUp":
4157
4764
  e.preventDefault();
4158
4765
  slideNavActions.prevSlide();
4159
4766
  break;
4767
+ case "ArrowUp":
4768
+ e.preventDefault();
4769
+ setIsSlideshowPickerOpen(true);
4770
+ break;
4160
4771
  case "Home":
4161
4772
  e.preventDefault();
4162
4773
  slideNavActions.goToSlide(0);
@@ -4173,9 +4784,11 @@ function DocPlayerContent({
4173
4784
  toggle();
4174
4785
  break;
4175
4786
  case "ArrowRight":
4787
+ e.preventDefault();
4176
4788
  seekTo(Math.min(currentTimeRef.current + 10, totalDurationRef.current));
4177
4789
  break;
4178
4790
  case "ArrowLeft":
4791
+ e.preventDefault();
4179
4792
  seekTo(Math.max(currentTimeRef.current - 10, 0));
4180
4793
  break;
4181
4794
  }
@@ -4183,16 +4796,24 @@ function DocPlayerContent({
4183
4796
  },
4184
4797
  [isSlideshowMode, isLinearMode, toggle, seekTo, slideNavActions]
4185
4798
  );
4186
- useEffect8(() => {
4187
- if (renderMode) return;
4188
- window.addEventListener("keydown", handleKeyDown);
4189
- return () => window.removeEventListener("keydown", handleKeyDown);
4190
- }, [handleKeyDown, renderMode]);
4799
+ const handleKeyDown = useCallback7(
4800
+ (e) => handleKeyboardShortcut(e, false),
4801
+ [handleKeyboardShortcut]
4802
+ );
4803
+ useEffect10(() => {
4804
+ if (!globalKeyboardShortcuts || renderMode || isLinearMode) return;
4805
+ const handleDocumentKeyDown = (event) => {
4806
+ handleKeyboardShortcut(event, true);
4807
+ };
4808
+ document.addEventListener("keydown", handleDocumentKeyDown);
4809
+ return () => document.removeEventListener("keydown", handleDocumentKeyDown);
4810
+ }, [globalKeyboardShortcuts, handleKeyboardShortcut, isLinearMode, renderMode]);
4191
4811
  if (isLinearMode) {
4192
- return /* @__PURE__ */ jsx20(
4812
+ return /* @__PURE__ */ jsx21(
4193
4813
  "div",
4194
4814
  {
4195
4815
  ref: containerRef,
4816
+ "data-player-id": playerId,
4196
4817
  className: "doc-player doc-player--linear",
4197
4818
  style: {
4198
4819
  position: "relative",
@@ -4200,23 +4821,28 @@ function DocPlayerContent({
4200
4821
  height: "100%",
4201
4822
  overflow: "hidden"
4202
4823
  },
4203
- children: /* @__PURE__ */ jsx20(
4824
+ children: /* @__PURE__ */ jsx21(
4204
4825
  LinearDocView,
4205
4826
  {
4206
4827
  doc,
4207
4828
  basePath,
4208
4829
  viewport: activeViewport,
4209
4830
  theme,
4210
- surface
4831
+ surface,
4832
+ animationsEnabled
4211
4833
  }
4212
4834
  )
4213
4835
  }
4214
4836
  );
4215
4837
  }
4216
- return /* @__PURE__ */ jsxs13(
4838
+ return /* @__PURE__ */ jsxs14(
4217
4839
  "div",
4218
4840
  {
4219
4841
  ref: containerRef,
4842
+ "data-player-id": playerId,
4843
+ tabIndex: renderMode ? -1 : 0,
4844
+ "aria-label": "Document player",
4845
+ onKeyDown: renderMode ? void 0 : handleKeyDown,
4220
4846
  className: `doc-player${swipeEnabled ? " doc-player--swipe" : ""}${swipe.phase === "dragging" ? " doc-player--grabbing" : ""}`,
4221
4847
  onClick: handleContainerClick,
4222
4848
  onPointerDown: swipe.onPointerDown,
@@ -4232,33 +4858,35 @@ function DocPlayerContent({
4232
4858
  touchAction: swipeEnabled ? "pan-y" : void 0
4233
4859
  },
4234
4860
  children: [
4235
- /* @__PURE__ */ jsx20("audio", { ref: audioRef, preload: "auto", muted }),
4236
- /* @__PURE__ */ jsx20(
4861
+ /* @__PURE__ */ jsx21("audio", { ref: audioRef, preload: "auto", muted }),
4862
+ /* @__PURE__ */ jsx21(
4237
4863
  MediaClipLayer,
4238
4864
  {
4239
4865
  schedule: mediaSchedule,
4240
4866
  currentTime,
4241
4867
  isPlaying,
4242
4868
  basePath,
4243
- renderMode
4869
+ renderMode,
4870
+ muted
4244
4871
  }
4245
4872
  ),
4246
- /* @__PURE__ */ jsxs13("div", { className: "doc-player__viewport", children: [
4247
- showCoverBlock && coverBlock && /* @__PURE__ */ jsx20("div", { className: "doc-player__block doc-player__block--cover", children: /* @__PURE__ */ jsx20(
4873
+ /* @__PURE__ */ jsxs14("div", { className: "doc-player__viewport", children: [
4874
+ showCoverBlock && coverBlock && /* @__PURE__ */ jsx21("div", { className: "doc-player__block doc-player__block--cover", children: /* @__PURE__ */ jsx21(
4248
4875
  BlockRenderer,
4249
4876
  {
4250
4877
  block: coverBlock,
4251
4878
  blockTime: 0,
4252
4879
  basePath,
4253
4880
  isEntering: false,
4254
- viewport: activeViewport
4881
+ viewport: activeViewport,
4882
+ animationsEnabled
4255
4883
  }
4256
4884
  ) }),
4257
- !showCoverBlock && previousBlock && isExiting && // Keyed by block id so each block is its own DOM subtree: React never
4885
+ animationsEnabled && !showCoverBlock && previousBlock && isExiting && // Keyed by block id so each block is its own DOM subtree: React never
4258
4886
  // reconciles one block's layers onto another's (templates reuse layer
4259
4887
  // ids like `title`/`background`), which would otherwise reuse stale
4260
4888
  // DOM / skip entrance animations mid-transition.
4261
- /* @__PURE__ */ jsx20("div", { className: "doc-player__block doc-player__block--previous", children: /* @__PURE__ */ jsx20(
4889
+ /* @__PURE__ */ jsx21("div", { className: "doc-player__block doc-player__block--previous", children: /* @__PURE__ */ jsx21(
4262
4890
  BlockRenderer,
4263
4891
  {
4264
4892
  block: previousBlock,
@@ -4266,29 +4894,31 @@ function DocPlayerContent({
4266
4894
  basePath,
4267
4895
  isExiting: true,
4268
4896
  transition: currentBlock?.transition,
4269
- viewport: activeViewport
4897
+ viewport: activeViewport,
4898
+ animationsEnabled
4270
4899
  }
4271
4900
  ) }, previousBlock.id),
4272
- !showCoverBlock && currentBlock && /* @__PURE__ */ jsx20(
4901
+ !showCoverBlock && currentBlock && /* @__PURE__ */ jsx21(
4273
4902
  "div",
4274
4903
  {
4275
4904
  className: `doc-player__block doc-player__block--active${swipe.phase !== "idle" ? ` doc-player__block--${swipe.phase}` : ""}`,
4276
4905
  style: swipe.phase !== "idle" ? { transform: `translateX(${swipe.offsetPx}px)` } : void 0,
4277
- children: /* @__PURE__ */ jsx20(
4906
+ children: /* @__PURE__ */ jsx21(
4278
4907
  BlockRenderer,
4279
4908
  {
4280
4909
  block: currentBlock,
4281
4910
  blockTime,
4282
4911
  basePath,
4283
- isEntering,
4912
+ isEntering: animationsEnabled && isEntering,
4284
4913
  viewport: activeViewport,
4285
- isPlaying
4914
+ isPlaying,
4915
+ animationsEnabled
4286
4916
  }
4287
4917
  )
4288
4918
  },
4289
4919
  currentBlock.id
4290
4920
  ),
4291
- hasCaptions && (renderMode ? captionsEnabled : true) && /* @__PURE__ */ jsx20(
4921
+ hasCaptions && (renderMode ? captionsEnabled : true) && /* @__PURE__ */ jsx21(
4292
4922
  CaptionOverlay,
4293
4923
  {
4294
4924
  captions: doc.captions,
@@ -4300,7 +4930,7 @@ function DocPlayerContent({
4300
4930
  viewport: activeViewport
4301
4931
  }
4302
4932
  ),
4303
- isDebugMode && /* @__PURE__ */ jsxs13(
4933
+ isDebugMode && /* @__PURE__ */ jsxs14(
4304
4934
  "div",
4305
4935
  {
4306
4936
  className: "doc-player__debug",
@@ -4321,27 +4951,27 @@ function DocPlayerContent({
4321
4951
  textAlign: "left"
4322
4952
  },
4323
4953
  children: [
4324
- /* @__PURE__ */ jsx20("div", { style: { color: "#ffcc00", fontWeight: "bold", marginBottom: "4px" }, children: "DEBUG MODE" }),
4325
- /* @__PURE__ */ jsxs13("div", { children: [
4326
- /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "template:" }),
4954
+ /* @__PURE__ */ jsx21("div", { style: { color: "#ffcc00", fontWeight: "bold", marginBottom: "4px" }, children: "DEBUG MODE" }),
4955
+ /* @__PURE__ */ jsxs14("div", { children: [
4956
+ /* @__PURE__ */ jsx21("span", { style: { color: "#888" }, children: "template:" }),
4327
4957
  " ",
4328
- /* @__PURE__ */ jsx20("span", { style: { color: "#ff6b6b" }, children: currentBlock?.template ?? "raw" })
4958
+ /* @__PURE__ */ jsx21("span", { style: { color: "#ff6b6b" }, children: currentBlock?.template ?? "raw" })
4329
4959
  ] }),
4330
- /* @__PURE__ */ jsxs13("div", { children: [
4331
- /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "block:" }),
4960
+ /* @__PURE__ */ jsxs14("div", { children: [
4961
+ /* @__PURE__ */ jsx21("span", { style: { color: "#888" }, children: "block:" }),
4332
4962
  " ",
4333
4963
  currentBlockIndex + 1,
4334
4964
  "/",
4335
4965
  expandedBlocks.length,
4336
4966
  " ",
4337
- /* @__PURE__ */ jsxs13("span", { style: { color: "#666" }, children: [
4967
+ /* @__PURE__ */ jsxs14("span", { style: { color: "#666" }, children: [
4338
4968
  "(",
4339
4969
  currentBlock?.id || "none",
4340
4970
  ")"
4341
4971
  ] })
4342
4972
  ] }),
4343
- /* @__PURE__ */ jsxs13("div", { children: [
4344
- /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "time:" }),
4973
+ /* @__PURE__ */ jsxs14("div", { children: [
4974
+ /* @__PURE__ */ jsx21("span", { style: { color: "#888" }, children: "time:" }),
4345
4975
  " ",
4346
4976
  currentTime.toFixed(2),
4347
4977
  "s /",
@@ -4349,7 +4979,7 @@ function DocPlayerContent({
4349
4979
  totalDuration.toFixed(1),
4350
4980
  "s",
4351
4981
  " ",
4352
- /* @__PURE__ */ jsxs13("span", { style: { color: "#666" }, children: [
4982
+ /* @__PURE__ */ jsxs14("span", { style: { color: "#666" }, children: [
4353
4983
  "(progress: ",
4354
4984
  (docProgress * 100).toFixed(1),
4355
4985
  "%, scriptDur: ",
@@ -4357,8 +4987,8 @@ function DocPlayerContent({
4357
4987
  ")"
4358
4988
  ] })
4359
4989
  ] }),
4360
- /* @__PURE__ */ jsxs13("div", { children: [
4361
- /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "blockTime:" }),
4990
+ /* @__PURE__ */ jsxs14("div", { children: [
4991
+ /* @__PURE__ */ jsx21("span", { style: { color: "#888" }, children: "blockTime:" }),
4362
4992
  " ",
4363
4993
  blockTime.toFixed(2),
4364
4994
  "s /",
@@ -4366,58 +4996,58 @@ function DocPlayerContent({
4366
4996
  (currentBlock?.duration || 0).toFixed(1),
4367
4997
  "s"
4368
4998
  ] }),
4369
- /* @__PURE__ */ jsxs13("div", { children: [
4370
- /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "segment:" }),
4999
+ /* @__PURE__ */ jsxs14("div", { children: [
5000
+ /* @__PURE__ */ jsx21("span", { style: { color: "#888" }, children: "segment:" }),
4371
5001
  " ",
4372
5002
  currentSegment,
4373
5003
  "/",
4374
5004
  doc.audio.segments.length - 1,
4375
5005
  " ",
4376
- /* @__PURE__ */ jsxs13("span", { style: { color: "#666" }, children: [
5006
+ /* @__PURE__ */ jsxs14("span", { style: { color: "#666" }, children: [
4377
5007
  "(",
4378
5008
  doc.audio.segments[currentSegment]?.name || "none",
4379
5009
  ")"
4380
5010
  ] })
4381
5011
  ] }),
4382
- /* @__PURE__ */ jsxs13("div", { children: [
4383
- /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "viewport:" }),
5012
+ /* @__PURE__ */ jsxs14("div", { children: [
5013
+ /* @__PURE__ */ jsx21("span", { style: { color: "#888" }, children: "viewport:" }),
4384
5014
  " ",
4385
5015
  activeViewport.name || `${activeViewport.width}x${activeViewport.height}`,
4386
5016
  " ",
4387
- /* @__PURE__ */ jsxs13("span", { style: { color: "#666" }, children: [
5017
+ /* @__PURE__ */ jsxs14("span", { style: { color: "#666" }, children: [
4388
5018
  "(",
4389
5019
  orientation,
4390
5020
  ")"
4391
5021
  ] })
4392
5022
  ] }),
4393
- /* @__PURE__ */ jsxs13("div", { children: [
4394
- /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "playing:" }),
5023
+ /* @__PURE__ */ jsxs14("div", { children: [
5024
+ /* @__PURE__ */ jsx21("span", { style: { color: "#888" }, children: "playing:" }),
4395
5025
  " ",
4396
- /* @__PURE__ */ jsx20("span", { style: { color: isPlaying ? "#4ade80" : "#f87171" }, children: isPlaying ? "yes" : "no" }),
4397
- showCoverBlock && /* @__PURE__ */ jsx20("span", { style: { color: "#60a5fa" }, children: " (cover)" })
5026
+ /* @__PURE__ */ jsx21("span", { style: { color: isPlaying ? "#4ade80" : "#f87171" }, children: isPlaying ? "yes" : "no" }),
5027
+ showCoverBlock && /* @__PURE__ */ jsx21("span", { style: { color: "#60a5fa" }, children: " (cover)" })
4398
5028
  ] }),
4399
5029
  hasCaptions && (() => {
4400
5030
  const debugPhrase = getCaptionAtTime2(doc.captions, currentTime);
4401
5031
  const debugEnabled = captionsEnabled && (isPlaying || currentTime > 0);
4402
- return /* @__PURE__ */ jsxs13(Fragment3, { children: [
4403
- /* @__PURE__ */ jsxs13("div", { children: [
4404
- /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "captions:" }),
5032
+ return /* @__PURE__ */ jsxs14(Fragment3, { children: [
5033
+ /* @__PURE__ */ jsxs14("div", { children: [
5034
+ /* @__PURE__ */ jsx21("span", { style: { color: "#888" }, children: "captions:" }),
4405
5035
  " ",
4406
5036
  doc.captions?.phrases.length || 0,
4407
5037
  " phrases",
4408
5038
  " ",
4409
- /* @__PURE__ */ jsxs13("span", { style: { color: captionsEnabled ? "#4ade80" : "#666" }, children: [
5039
+ /* @__PURE__ */ jsxs14("span", { style: { color: captionsEnabled ? "#4ade80" : "#666" }, children: [
4410
5040
  "(",
4411
5041
  captionsEnabled ? "on" : "off",
4412
5042
  ")"
4413
5043
  ] })
4414
5044
  ] }),
4415
- /* @__PURE__ */ jsxs13("div", { children: [
4416
- /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "cc.enabled:" }),
5045
+ /* @__PURE__ */ jsxs14("div", { children: [
5046
+ /* @__PURE__ */ jsx21("span", { style: { color: "#888" }, children: "cc.enabled:" }),
4417
5047
  " ",
4418
- /* @__PURE__ */ jsx20("span", { style: { color: debugEnabled ? "#4ade80" : "#f87171" }, children: String(debugEnabled) }),
5048
+ /* @__PURE__ */ jsx21("span", { style: { color: debugEnabled ? "#4ade80" : "#f87171" }, children: String(debugEnabled) }),
4419
5049
  " ",
4420
- /* @__PURE__ */ jsxs13("span", { style: { color: "#666" }, children: [
5050
+ /* @__PURE__ */ jsxs14("span", { style: { color: "#666" }, children: [
4421
5051
  "(playing=",
4422
5052
  String(isPlaying),
4423
5053
  " t>0=",
@@ -4425,15 +5055,15 @@ function DocPlayerContent({
4425
5055
  ")"
4426
5056
  ] })
4427
5057
  ] }),
4428
- /* @__PURE__ */ jsxs13("div", { children: [
4429
- /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "cc.phrase:" }),
5058
+ /* @__PURE__ */ jsxs14("div", { children: [
5059
+ /* @__PURE__ */ jsx21("span", { style: { color: "#888" }, children: "cc.phrase:" }),
4430
5060
  " ",
4431
- /* @__PURE__ */ jsx20("span", { style: { color: debugPhrase ? "#4ade80" : "#f87171" }, children: debugPhrase ? `"${debugPhrase.text.slice(0, 30)}..."` : "null" })
5061
+ /* @__PURE__ */ jsx21("span", { style: { color: debugPhrase ? "#4ade80" : "#f87171" }, children: debugPhrase ? `"${debugPhrase.text.slice(0, 30)}..."` : "null" })
4432
5062
  ] }),
4433
- debugPhrase && /* @__PURE__ */ jsxs13("div", { children: [
4434
- /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "cc.range:" }),
5063
+ debugPhrase && /* @__PURE__ */ jsxs14("div", { children: [
5064
+ /* @__PURE__ */ jsx21("span", { style: { color: "#888" }, children: "cc.range:" }),
4435
5065
  " ",
4436
- /* @__PURE__ */ jsxs13("span", { style: { color: "#60a5fa" }, children: [
5066
+ /* @__PURE__ */ jsxs14("span", { style: { color: "#60a5fa" }, children: [
4437
5067
  debugPhrase.startTime.toFixed(2),
4438
5068
  "-",
4439
5069
  debugPhrase.endTime.toFixed(2)
@@ -4445,7 +5075,7 @@ function DocPlayerContent({
4445
5075
  }
4446
5076
  )
4447
5077
  ] }),
4448
- !isAvailable && unavailableMessage && /* @__PURE__ */ jsxs13(
5078
+ !isAvailable && unavailableMessage && /* @__PURE__ */ jsxs14(
4449
5079
  "div",
4450
5080
  {
4451
5081
  className: "doc-player__unavailable",
@@ -4466,12 +5096,12 @@ function DocPlayerContent({
4466
5096
  zIndex: 50
4467
5097
  },
4468
5098
  children: [
4469
- /* @__PURE__ */ jsx20("span", { style: { fontSize: "32px" }, children: "\u{1F50A}" }),
4470
- /* @__PURE__ */ jsx20("span", { children: unavailableMessage })
5099
+ /* @__PURE__ */ jsx21("span", { style: { fontSize: "32px" }, children: "\u{1F50A}" }),
5100
+ /* @__PURE__ */ jsx21("span", { children: unavailableMessage })
4471
5101
  ]
4472
5102
  }
4473
5103
  ),
4474
- !renderMode && !isSlideshowMode && showControls && /* @__PURE__ */ jsx20(
5104
+ !renderMode && !isSlideshowMode && showControls && /* @__PURE__ */ jsx21(
4475
5105
  DocControlsOverlay,
4476
5106
  {
4477
5107
  state: playbackState,
@@ -4481,7 +5111,7 @@ function DocPlayerContent({
4481
5111
  getBlockTitle
4482
5112
  }
4483
5113
  ),
4484
- !renderMode && !isSlideshowMode && !showControls && showScrubber && /* @__PURE__ */ jsx20(
5114
+ !renderMode && !isSlideshowMode && !showControls && showScrubber && /* @__PURE__ */ jsx21(
4485
5115
  "div",
4486
5116
  {
4487
5117
  className: "doc-player__scrubber",
@@ -4496,7 +5126,7 @@ function DocPlayerContent({
4496
5126
  alignItems: "center",
4497
5127
  zIndex: 100
4498
5128
  },
4499
- children: /* @__PURE__ */ jsx20(
5129
+ children: /* @__PURE__ */ jsx21(
4500
5130
  DocProgressBar,
4501
5131
  {
4502
5132
  state: playbackState,
@@ -4508,15 +5138,24 @@ function DocPlayerContent({
4508
5138
  )
4509
5139
  }
4510
5140
  ),
4511
- !renderMode && isSlideshowMode && /* @__PURE__ */ jsx20(DocControlsSlideshow, { state: playbackState, slideNav: slideNavActions }),
4512
- !isSlideshowMode && tapFeedback && /* @__PURE__ */ jsx20("div", { className: "doc-player__tap-feedback", children: /* @__PURE__ */ jsx20("svg", { viewBox: "0 0 24 24", fill: "white", width: "48", height: "48", children: tapFeedback === "pause" ? /* @__PURE__ */ jsx20("path", { d: "M6 19h4V5H6v14zm8-14v14h4V5h-4z" }) : /* @__PURE__ */ jsx20("path", { d: "M8 5v14l11-7z" }) }) }, Date.now())
5141
+ !renderMode && isSlideshowMode && showControls && /* @__PURE__ */ jsx21(
5142
+ DocControlsSlideshow,
5143
+ {
5144
+ state: playbackState,
5145
+ slideNav: slideNavActions,
5146
+ slides: slideshowPickerItems,
5147
+ pickerOpen: isSlideshowPickerOpen,
5148
+ onPickerOpenChange: setIsSlideshowPickerOpen
5149
+ }
5150
+ ),
5151
+ !isSlideshowMode && tapFeedback && /* @__PURE__ */ jsx21("div", { className: "doc-player__tap-feedback", children: /* @__PURE__ */ jsx21("svg", { viewBox: "0 0 24 24", fill: "white", width: "48", height: "48", children: tapFeedback === "pause" ? /* @__PURE__ */ jsx21("path", { d: "M6 19h4V5H6v14zm8-14v14h4V5h-4z" }) : /* @__PURE__ */ jsx21("path", { d: "M8 5v14l11-7z" }) }) }, tapFeedback)
4513
5152
  ]
4514
5153
  }
4515
5154
  );
4516
5155
  }
4517
5156
 
4518
5157
  // src/DocControlsBottom.tsx
4519
- import { jsx as jsx21, jsxs as jsxs14 } from "react/jsx-runtime";
5158
+ import { jsx as jsx22, jsxs as jsxs15 } from "react/jsx-runtime";
4520
5159
  function DocControlsBottom({
4521
5160
  state,
4522
5161
  actions,
@@ -4524,32 +5163,32 @@ function DocControlsBottom({
4524
5163
  expandedBlocks,
4525
5164
  getBlockTitle
4526
5165
  }) {
4527
- return /* @__PURE__ */ jsxs14("div", { className: "doc-controls-bottom", children: [
4528
- /* @__PURE__ */ jsx21(
5166
+ return /* @__PURE__ */ jsxs15("div", { className: "doc-controls-bottom", children: [
5167
+ /* @__PURE__ */ jsx22(
4529
5168
  "button",
4530
5169
  {
4531
5170
  className: "bottom-ctrl-btn",
4532
5171
  onClick: actions.restart,
4533
5172
  title: "Restart",
4534
5173
  "aria-label": "Restart from beginning",
4535
- children: /* @__PURE__ */ jsx21("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx21("path", { d: "M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z" }) })
5174
+ children: /* @__PURE__ */ jsx22("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx22("path", { d: "M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z" }) })
4536
5175
  }
4537
5176
  ),
4538
- /* @__PURE__ */ jsx21(
5177
+ /* @__PURE__ */ jsx22(
4539
5178
  "button",
4540
5179
  {
4541
5180
  className: "bottom-ctrl-btn bottom-play-btn",
4542
5181
  onClick: actions.toggle,
4543
5182
  "aria-label": state.isPlaying ? "Pause" : "Play",
4544
- children: state.isPlaying ? /* @__PURE__ */ jsx21("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx21("path", { d: "M6 19h4V5H6v14zm8-14v14h4V5h-4z" }) }) : /* @__PURE__ */ jsx21("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx21("path", { d: "M8 5v14l11-7z" }) })
5183
+ children: state.isPlaying ? /* @__PURE__ */ jsx22("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx22("path", { d: "M6 19h4V5H6v14zm8-14v14h4V5h-4z" }) }) : /* @__PURE__ */ jsx22("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx22("path", { d: "M8 5v14l11-7z" }) })
4545
5184
  }
4546
5185
  ),
4547
- /* @__PURE__ */ jsxs14("span", { className: "bottom-time", children: [
5186
+ /* @__PURE__ */ jsxs15("span", { className: "bottom-time", children: [
4548
5187
  formatTime(state.currentTime),
4549
5188
  " / ",
4550
5189
  formatTime(state.totalDuration)
4551
5190
  ] }),
4552
- /* @__PURE__ */ jsx21(
5191
+ /* @__PURE__ */ jsx22(
4553
5192
  DocProgressBar,
4554
5193
  {
4555
5194
  state,
@@ -4559,82 +5198,82 @@ function DocControlsBottom({
4559
5198
  getBlockTitle
4560
5199
  }
4561
5200
  ),
4562
- /* @__PURE__ */ jsxs14("span", { className: "bottom-segment", children: [
5201
+ /* @__PURE__ */ jsxs15("span", { className: "bottom-segment", children: [
4563
5202
  state.currentBlockIndex + 1,
4564
5203
  "/",
4565
5204
  state.totalBlocks
4566
5205
  ] }),
4567
- state.hasCaptions && /* @__PURE__ */ jsx21(
5206
+ state.hasCaptions && /* @__PURE__ */ jsx22(
4568
5207
  "button",
4569
5208
  {
4570
5209
  className: `bottom-ctrl-btn ${state.captionMode !== "off" ? "bottom-ctrl-btn--active" : ""}`,
4571
5210
  onClick: () => actions.cycleCaptionMode(),
4572
5211
  title: state.captionMode === "off" ? "Captions: Off" : state.captionMode === "standard" ? "Captions: Standard" : "Captions: Social",
4573
5212
  "aria-label": "Cycle caption style",
4574
- children: /* @__PURE__ */ jsx21("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx21("path", { d: "M19 4H5a2 2 0 00-2 2v12a2 2 0 002 2h14a2 2 0 002-2V6a2 2 0 00-2-2zm-8 7H9.5v-.5h-2v3h2V13H11v1a1 1 0 01-1 1H7a1 1 0 01-1-1v-4a1 1 0 011-1h3a1 1 0 011 1v1zm7 0h-1.5v-.5h-2v3h2V13H18v1a1 1 0 01-1 1h-3a1 1 0 01-1-1v-4a1 1 0 011-1h3a1 1 0 011 1v1z" }) })
5213
+ children: /* @__PURE__ */ jsx22("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx22("path", { d: "M19 4H5a2 2 0 00-2 2v12a2 2 0 002 2h14a2 2 0 002-2V6a2 2 0 00-2-2zm-8 7H9.5v-.5h-2v3h2V13H11v1a1 1 0 01-1 1H7a1 1 0 01-1-1v-4a1 1 0 011-1h3a1 1 0 011 1v1zm7 0h-1.5v-.5h-2v3h2V13H18v1a1 1 0 01-1 1h-3a1 1 0 01-1-1v-4a1 1 0 011-1h3a1 1 0 011 1v1z" }) })
4575
5214
  }
4576
5215
  )
4577
5216
  ] });
4578
5217
  }
4579
5218
 
4580
5219
  // src/DocControlsSidebar.tsx
4581
- import { jsx as jsx22, jsxs as jsxs15 } from "react/jsx-runtime";
5220
+ import { jsx as jsx23, jsxs as jsxs16 } from "react/jsx-runtime";
4582
5221
  function DocControlsSidebar({ state, actions }) {
4583
- return /* @__PURE__ */ jsxs15("div", { className: "doc-controls-sidebar", children: [
4584
- /* @__PURE__ */ jsx22(
5222
+ return /* @__PURE__ */ jsxs16("div", { className: "doc-controls-sidebar", children: [
5223
+ /* @__PURE__ */ jsx23(
4585
5224
  "button",
4586
5225
  {
4587
5226
  className: "sidebar-ctrl-btn",
4588
5227
  onClick: actions.restart,
4589
5228
  title: "Restart",
4590
5229
  "aria-label": "Restart from beginning",
4591
- children: /* @__PURE__ */ jsx22("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx22("path", { d: "M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z" }) })
5230
+ children: /* @__PURE__ */ jsx23("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx23("path", { d: "M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z" }) })
4592
5231
  }
4593
5232
  ),
4594
- /* @__PURE__ */ jsx22(
5233
+ /* @__PURE__ */ jsx23(
4595
5234
  "button",
4596
5235
  {
4597
5236
  className: "sidebar-ctrl-btn sidebar-play-btn",
4598
5237
  onClick: actions.toggle,
4599
5238
  "aria-label": state.isPlaying ? "Pause" : "Play",
4600
- children: state.isPlaying ? /* @__PURE__ */ jsx22("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "22", height: "22", children: /* @__PURE__ */ jsx22("path", { d: "M6 19h4V5H6v14zm8-14v14h4V5h-4z" }) }) : /* @__PURE__ */ jsx22("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "22", height: "22", children: /* @__PURE__ */ jsx22("path", { d: "M8 5v14l11-7z" }) })
5239
+ children: state.isPlaying ? /* @__PURE__ */ jsx23("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "22", height: "22", children: /* @__PURE__ */ jsx23("path", { d: "M6 19h4V5H6v14zm8-14v14h4V5h-4z" }) }) : /* @__PURE__ */ jsx23("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "22", height: "22", children: /* @__PURE__ */ jsx23("path", { d: "M8 5v14l11-7z" }) })
4601
5240
  }
4602
5241
  ),
4603
- /* @__PURE__ */ jsxs15("div", { className: "sidebar-time", children: [
4604
- /* @__PURE__ */ jsx22("div", { children: formatTime(state.currentTime) }),
4605
- /* @__PURE__ */ jsx22("div", { className: "sidebar-time-total", children: formatTime(state.totalDuration) })
5242
+ /* @__PURE__ */ jsxs16("div", { className: "sidebar-time", children: [
5243
+ /* @__PURE__ */ jsx23("div", { children: formatTime(state.currentTime) }),
5244
+ /* @__PURE__ */ jsx23("div", { className: "sidebar-time-total", children: formatTime(state.totalDuration) })
4606
5245
  ] }),
4607
- /* @__PURE__ */ jsxs15("div", { className: "sidebar-segment", children: [
5246
+ /* @__PURE__ */ jsxs16("div", { className: "sidebar-segment", children: [
4608
5247
  state.currentBlockIndex + 1,
4609
5248
  "/",
4610
5249
  state.totalBlocks
4611
5250
  ] }),
4612
- state.hasCaptions && /* @__PURE__ */ jsx22(
5251
+ state.hasCaptions && /* @__PURE__ */ jsx23(
4613
5252
  "button",
4614
5253
  {
4615
5254
  className: `sidebar-ctrl-btn ${state.captionMode !== "off" ? "sidebar-ctrl-btn--active" : ""}`,
4616
5255
  onClick: () => actions.cycleCaptionMode(),
4617
5256
  title: state.captionMode === "off" ? "Captions: Off" : state.captionMode === "standard" ? "Captions: Standard" : "Captions: Social",
4618
5257
  "aria-label": "Cycle caption style",
4619
- children: /* @__PURE__ */ jsx22("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx22("path", { d: "M19 4H5a2 2 0 00-2 2v12a2 2 0 002 2h14a2 2 0 002-2V6a2 2 0 00-2-2zm-8 7H9.5v-.5h-2v3h2V13H11v1a1 1 0 01-1 1H7a1 1 0 01-1-1v-4a1 1 0 011-1h3a1 1 0 011 1v1zm7 0h-1.5v-.5h-2v3h2V13H18v1a1 1 0 01-1 1h-3a1 1 0 01-1-1v-4a1 1 0 011-1h3a1 1 0 011 1v1z" }) })
5258
+ children: /* @__PURE__ */ jsx23("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx23("path", { d: "M19 4H5a2 2 0 00-2 2v12a2 2 0 002 2h14a2 2 0 002-2V6a2 2 0 00-2-2zm-8 7H9.5v-.5h-2v3h2V13H11v1a1 1 0 01-1 1H7a1 1 0 01-1-1v-4a1 1 0 011-1h3a1 1 0 011 1v1zm7 0h-1.5v-.5h-2v3h2V13H18v1a1 1 0 01-1 1h-3a1 1 0 01-1-1v-4a1 1 0 011-1h3a1 1 0 011 1v1z" }) })
4620
5259
  }
4621
5260
  ),
4622
- actions.toggleFullscreen && /* @__PURE__ */ jsx22(
5261
+ actions.toggleFullscreen && /* @__PURE__ */ jsx23(
4623
5262
  "button",
4624
5263
  {
4625
5264
  className: `sidebar-ctrl-btn ${state.isFullscreen ? "sidebar-ctrl-btn--active" : ""}`,
4626
5265
  onClick: actions.toggleFullscreen,
4627
5266
  title: state.isFullscreen ? "Exit fullscreen (F)" : "Fullscreen (F)",
4628
5267
  "aria-label": state.isFullscreen ? "Exit fullscreen" : "Enter fullscreen",
4629
- children: state.isFullscreen ? /* @__PURE__ */ jsx22("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx22("path", { d: "M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z" }) }) : /* @__PURE__ */ jsx22("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx22("path", { d: "M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" }) })
5268
+ children: state.isFullscreen ? /* @__PURE__ */ jsx23("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx23("path", { d: "M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z" }) }) : /* @__PURE__ */ jsx23("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx23("path", { d: "M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" }) })
4630
5269
  }
4631
5270
  )
4632
5271
  ] });
4633
5272
  }
4634
5273
 
4635
5274
  // src/DocPlayerWithSidebar.tsx
4636
- import { useRef as useRef8, useState as useState8, useCallback as useCallback7, useEffect as useEffect9 } from "react";
4637
- import { jsx as jsx23, jsxs as jsxs16 } from "react/jsx-runtime";
5275
+ import { useRef as useRef10, useState as useState10, useCallback as useCallback8, useEffect as useEffect11 } from "react";
5276
+ import { jsx as jsx24, jsxs as jsxs17 } from "react/jsx-runtime";
4638
5277
  var DEFAULT_STATE = {
4639
5278
  isPlaying: false,
4640
5279
  currentTime: 0,
@@ -4656,6 +5295,7 @@ function DocPlayerWithSidebar({
4656
5295
  onEnded,
4657
5296
  onTimeUpdate,
4658
5297
  audioController,
5298
+ animationsEnabled = true,
4659
5299
  muted,
4660
5300
  captionsEnabled,
4661
5301
  isFullscreen,
@@ -4664,11 +5304,11 @@ function DocPlayerWithSidebar({
4664
5304
  onPlayingChange,
4665
5305
  theme
4666
5306
  }) {
4667
- const stateRef = useRef8(DEFAULT_STATE);
4668
- const actionsRef = useRef8(null);
4669
- const wasPlayingRef = useRef8(false);
4670
- const [, setTick] = useState8(0);
4671
- const handleStateChange = useCallback7(
5307
+ const stateRef = useRef10(DEFAULT_STATE);
5308
+ const actionsRef = useRef10(null);
5309
+ const wasPlayingRef = useRef10(false);
5310
+ const [, setTick] = useState10(0);
5311
+ const handleStateChange = useCallback8(
4672
5312
  (state) => {
4673
5313
  stateRef.current = state;
4674
5314
  if (onPlayingChange && state.isPlaying !== wasPlayingRef.current) {
@@ -4678,7 +5318,7 @@ function DocPlayerWithSidebar({
4678
5318
  },
4679
5319
  [onPlayingChange]
4680
5320
  );
4681
- const handleControlsReady = useCallback7(
5321
+ const handleControlsReady = useCallback8(
4682
5322
  (controls) => {
4683
5323
  const isFirst = !actionsRef.current;
4684
5324
  actionsRef.current = controls;
@@ -4686,14 +5326,14 @@ function DocPlayerWithSidebar({
4686
5326
  },
4687
5327
  []
4688
5328
  );
4689
- useEffect9(() => {
5329
+ useEffect11(() => {
4690
5330
  const interval = setInterval(() => {
4691
5331
  setTick((t) => t + 1);
4692
5332
  }, 250);
4693
5333
  return () => clearInterval(interval);
4694
5334
  }, []);
4695
- return /* @__PURE__ */ jsxs16("div", { className: "doc-player-sidebar-layout", children: [
4696
- /* @__PURE__ */ jsx23("div", { className: "doc-player-sidebar-layout__video", children: /* @__PURE__ */ jsx23(
5335
+ return /* @__PURE__ */ jsxs17("div", { className: "doc-player-sidebar-layout", children: [
5336
+ /* @__PURE__ */ jsx24("div", { className: "doc-player-sidebar-layout__video", children: /* @__PURE__ */ jsx24(
4697
5337
  DocPlayer,
4698
5338
  {
4699
5339
  doc,
@@ -4703,6 +5343,7 @@ function DocPlayerWithSidebar({
4703
5343
  onEnded,
4704
5344
  onTimeUpdate,
4705
5345
  audioController,
5346
+ animationsEnabled,
4706
5347
  muted,
4707
5348
  captionsEnabled,
4708
5349
  showControls: isFullscreen,
@@ -4714,7 +5355,7 @@ function DocPlayerWithSidebar({
4714
5355
  forceViewport
4715
5356
  }
4716
5357
  ) }),
4717
- actionsRef.current && /* @__PURE__ */ jsx23(DocControlsSidebar, { state: stateRef.current, actions: actionsRef.current })
5358
+ actionsRef.current && /* @__PURE__ */ jsx24(DocControlsSidebar, { state: stateRef.current, actions: actionsRef.current })
4718
5359
  ] });
4719
5360
  }
4720
5361
 
@@ -4745,18 +5386,18 @@ import {
4745
5386
  arrayItemKind
4746
5387
  } from "@bendyline/squisq/jsonForm";
4747
5388
  import { parseMarkdown as parseMarkdown3 } from "@bendyline/squisq/markdown";
4748
- import { Fragment as Fragment5, jsx as jsx24, jsxs as jsxs17 } from "react/jsx-runtime";
5389
+ import { Fragment as Fragment5, jsx as jsx25, jsxs as jsxs18 } from "react/jsx-runtime";
4749
5390
  function TextViewer({ value }) {
4750
5391
  if (value === void 0 || value === null || value === "") {
4751
- return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-empty", children: "\u2014" });
5392
+ return /* @__PURE__ */ jsx25("span", { className: "squisq-jv-empty", children: "\u2014" });
4752
5393
  }
4753
- return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-value", children: String(value) });
5394
+ return /* @__PURE__ */ jsx25("span", { className: "squisq-jv-value", children: String(value) });
4754
5395
  }
4755
5396
  function MultilineViewer({ value }) {
4756
5397
  if (value === void 0 || value === null || value === "") {
4757
- return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-empty", children: "\u2014" });
5398
+ return /* @__PURE__ */ jsx25("span", { className: "squisq-jv-empty", children: "\u2014" });
4758
5399
  }
4759
- return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-value squisq-jv-value--multiline", children: String(value) });
5400
+ return /* @__PURE__ */ jsx25("span", { className: "squisq-jv-value squisq-jv-value--multiline", children: String(value) });
4760
5401
  }
4761
5402
  function RichTextViewer({ value }) {
4762
5403
  const nodes = useMemo11(() => {
@@ -4768,39 +5409,39 @@ function RichTextViewer({ value }) {
4768
5409
  return null;
4769
5410
  }
4770
5411
  }, [value]);
4771
- if (!nodes) return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-empty", children: "\u2014" });
4772
- return /* @__PURE__ */ jsx24("div", { className: "squisq-jv-richtext", children: /* @__PURE__ */ jsx24(MarkdownRenderer, { nodes }) });
5412
+ if (!nodes) return /* @__PURE__ */ jsx25("span", { className: "squisq-jv-empty", children: "\u2014" });
5413
+ return /* @__PURE__ */ jsx25("div", { className: "squisq-jv-richtext", children: /* @__PURE__ */ jsx25(MarkdownRenderer, { nodes }) });
4773
5414
  }
4774
5415
  function NumberViewer({ value }) {
4775
5416
  if (value === void 0 || value === null) {
4776
- return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-empty", children: "\u2014" });
5417
+ return /* @__PURE__ */ jsx25("span", { className: "squisq-jv-empty", children: "\u2014" });
4777
5418
  }
4778
- return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-value", children: String(value) });
5419
+ return /* @__PURE__ */ jsx25("span", { className: "squisq-jv-value", children: String(value) });
4779
5420
  }
4780
5421
  function BooleanViewer({ value }) {
4781
5422
  const on = Boolean(value);
4782
- return /* @__PURE__ */ jsx24("span", { className: `squisq-jv-toggle squisq-jv-toggle--${on ? "on" : "off"}`, children: on ? "On" : "Off" });
5423
+ return /* @__PURE__ */ jsx25("span", { className: `squisq-jv-toggle squisq-jv-toggle--${on ? "on" : "off"}`, children: on ? "On" : "Off" });
4783
5424
  }
4784
5425
  function EnumViewer({ value, schema }) {
4785
5426
  if (value === void 0 || value === null || value === "") {
4786
- return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-empty", children: "\u2014" });
5427
+ return /* @__PURE__ */ jsx25("span", { className: "squisq-jv-empty", children: "\u2014" });
4787
5428
  }
4788
5429
  const labels = schema.squisq?.enumLabels;
4789
5430
  const display = labels && typeof value === "string" ? labels[value] ?? value : String(value);
4790
- return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-value", children: display });
5431
+ return /* @__PURE__ */ jsx25("span", { className: "squisq-jv-value", children: display });
4791
5432
  }
4792
5433
  function ColorViewer({ value }) {
4793
5434
  if (typeof value !== "string" || value === "") {
4794
- return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-empty", children: "\u2014" });
5435
+ return /* @__PURE__ */ jsx25("span", { className: "squisq-jv-empty", children: "\u2014" });
4795
5436
  }
4796
- return /* @__PURE__ */ jsxs17("span", { className: "squisq-jv-color", children: [
4797
- /* @__PURE__ */ jsx24("span", { className: "squisq-jv-color__swatch", style: { background: value }, "aria-hidden": true }),
4798
- /* @__PURE__ */ jsx24("span", { className: "squisq-jv-color__hex", children: value })
5437
+ return /* @__PURE__ */ jsxs18("span", { className: "squisq-jv-color", children: [
5438
+ /* @__PURE__ */ jsx25("span", { className: "squisq-jv-color__swatch", style: { background: value }, "aria-hidden": true }),
5439
+ /* @__PURE__ */ jsx25("span", { className: "squisq-jv-color__hex", children: value })
4799
5440
  ] });
4800
5441
  }
4801
5442
  function DateViewer({ value, schema }) {
4802
5443
  if (typeof value !== "string" || value === "") {
4803
- return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-empty", children: "\u2014" });
5444
+ return /* @__PURE__ */ jsx25("span", { className: "squisq-jv-empty", children: "\u2014" });
4804
5445
  }
4805
5446
  const fmt = schema.format;
4806
5447
  let display = value;
@@ -4817,31 +5458,31 @@ function DateViewer({ value, schema }) {
4817
5458
  }
4818
5459
  } catch {
4819
5460
  }
4820
- return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-value", children: display });
5461
+ return /* @__PURE__ */ jsx25("span", { className: "squisq-jv-value", children: display });
4821
5462
  }
4822
5463
  function ChipBinViewer({ value, schema }) {
4823
5464
  if (!Array.isArray(value) || value.length === 0) {
4824
- return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-empty", children: "\u2014" });
5465
+ return /* @__PURE__ */ jsx25("span", { className: "squisq-jv-empty", children: "\u2014" });
4825
5466
  }
4826
5467
  const itemSchema = Array.isArray(schema.items) ? schema.items[0] : schema.items;
4827
5468
  const labels = itemSchema?.squisq?.enumLabels;
4828
- return /* @__PURE__ */ jsx24("div", { className: "squisq-jv-chip-bin", children: value.map((item, i) => {
5469
+ return /* @__PURE__ */ jsx25("div", { className: "squisq-jv-chip-bin", children: value.map((item, i) => {
4829
5470
  const label = labels && typeof item === "string" ? labels[item] ?? String(item) : String(item);
4830
- return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-chip", children: label }, i);
5471
+ return /* @__PURE__ */ jsx25("span", { className: "squisq-jv-chip", children: label }, i);
4831
5472
  }) });
4832
5473
  }
4833
5474
  function CardStackViewer(props) {
4834
5475
  const { value, schema, rootSchema, rootData, pointer, density } = props;
4835
5476
  if (!Array.isArray(value) || value.length === 0) {
4836
- return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-empty", children: "No items" });
5477
+ return /* @__PURE__ */ jsx25("span", { className: "squisq-jv-empty", children: "No items" });
4837
5478
  }
4838
5479
  const itemSchema = (Array.isArray(schema.items) ? schema.items[0] : schema.items) ?? {};
4839
5480
  const itemLabel = itemSchema.squisq?.itemLabel;
4840
- return /* @__PURE__ */ jsx24("div", { className: "squisq-jv-card-stack", children: value.map((item, i) => {
5481
+ return /* @__PURE__ */ jsx25("div", { className: "squisq-jv-card-stack", children: value.map((item, i) => {
4841
5482
  const title = resolveItemTitle(itemLabel, item, i);
4842
- return /* @__PURE__ */ jsxs17("div", { className: "squisq-jv-card", children: [
4843
- title ? /* @__PURE__ */ jsx24("h4", { className: "squisq-jv-card__title", children: title }) : null,
4844
- /* @__PURE__ */ jsx24(
5483
+ return /* @__PURE__ */ jsxs18("div", { className: "squisq-jv-card", children: [
5484
+ title ? /* @__PURE__ */ jsx25("h4", { className: "squisq-jv-card__title", children: title }) : null,
5485
+ /* @__PURE__ */ jsx25(
4845
5486
  RenderNode,
4846
5487
  {
4847
5488
  value: item,
@@ -4872,16 +5513,16 @@ function GroupViewer(props) {
4872
5513
  const help = schema.squisq?.help ?? schema.description;
4873
5514
  const obj = (value && typeof value === "object" ? value : {}) ?? {};
4874
5515
  const propEntries = Object.entries(schema.properties ?? {});
4875
- return /* @__PURE__ */ jsxs17("section", { className: "squisq-jv-group", children: [
4876
- title ? /* @__PURE__ */ jsx24("h3", { className: "squisq-jv-group__title", children: title }) : null,
4877
- help ? /* @__PURE__ */ jsx24("p", { className: "squisq-jv-group__help", children: help }) : null,
4878
- propEntries.map(([key, propSchema]) => /* @__PURE__ */ jsx24(Fragment4, { children: /* @__PURE__ */ jsx24(
5516
+ return /* @__PURE__ */ jsxs18("section", { className: "squisq-jv-group", children: [
5517
+ title ? /* @__PURE__ */ jsx25("h3", { className: "squisq-jv-group__title", children: title }) : null,
5518
+ help ? /* @__PURE__ */ jsx25("p", { className: "squisq-jv-group__help", children: help }) : null,
5519
+ propEntries.map(([key, propSchema]) => /* @__PURE__ */ jsx25(Fragment4, { children: /* @__PURE__ */ jsx25(
4879
5520
  RowOrSection,
4880
5521
  {
4881
5522
  label: propSchema.squisq?.label ?? propSchema.title ?? key,
4882
5523
  help: propSchema.squisq?.help ?? propSchema.description,
4883
5524
  kindHint: propSchema,
4884
- children: /* @__PURE__ */ jsx24(
5525
+ children: /* @__PURE__ */ jsx25(
4885
5526
  RenderNode,
4886
5527
  {
4887
5528
  value: obj[key],
@@ -4905,11 +5546,11 @@ function RowOrSection({
4905
5546
  }) {
4906
5547
  const composite = isCompositeKind(kindHint);
4907
5548
  if (composite) {
4908
- return /* @__PURE__ */ jsx24(Fragment5, { children });
5549
+ return /* @__PURE__ */ jsx25(Fragment5, { children });
4909
5550
  }
4910
- return /* @__PURE__ */ jsxs17("div", { className: "squisq-jv-row", children: [
4911
- /* @__PURE__ */ jsx24("div", { className: "squisq-jv-label", title: help, children: label }),
4912
- /* @__PURE__ */ jsx24("div", { children })
5551
+ return /* @__PURE__ */ jsxs18("div", { className: "squisq-jv-row", children: [
5552
+ /* @__PURE__ */ jsx25("div", { className: "squisq-jv-label", title: help, children: label }),
5553
+ /* @__PURE__ */ jsx25("div", { children })
4913
5554
  ] });
4914
5555
  }
4915
5556
  function isCompositeKind(schema) {
@@ -4930,11 +5571,11 @@ function TabsViewer(props) {
4930
5571
  const matchedIndex = pickMatchingBranch(branches, value);
4931
5572
  const branch = branches[matchedIndex];
4932
5573
  if (!branch) {
4933
- return /* @__PURE__ */ jsx24(TextViewer, { ...props });
5574
+ return /* @__PURE__ */ jsx25(TextViewer, { ...props });
4934
5575
  }
4935
- return /* @__PURE__ */ jsxs17("div", { className: "squisq-jv-tabs", children: [
4936
- /* @__PURE__ */ jsx24("span", { className: "squisq-jv-tabs__discriminator", children: branch.squisq?.label ?? branch.title ?? `Option ${matchedIndex + 1}` }),
4937
- /* @__PURE__ */ jsx24(
5576
+ return /* @__PURE__ */ jsxs18("div", { className: "squisq-jv-tabs", children: [
5577
+ /* @__PURE__ */ jsx25("span", { className: "squisq-jv-tabs__discriminator", children: branch.squisq?.label ?? branch.title ?? `Option ${matchedIndex + 1}` }),
5578
+ /* @__PURE__ */ jsx25(
4938
5579
  RenderNode,
4939
5580
  {
4940
5581
  value,
@@ -4998,7 +5639,7 @@ var VIEWERS = {
4998
5639
  };
4999
5640
 
5000
5641
  // src/jsonView/RenderNode.tsx
5001
- import { jsx as jsx25 } from "react/jsx-runtime";
5642
+ import { jsx as jsx26 } from "react/jsx-runtime";
5002
5643
  function RenderNode(props) {
5003
5644
  const resolved = resolveRef(props.schema, props.rootSchema) ?? props.schema;
5004
5645
  if (resolveFlag(resolved.squisq?.hidden, props.rootData)) return null;
@@ -5014,18 +5655,18 @@ function RenderNode(props) {
5014
5655
  };
5015
5656
  if (kind === "group" || kind === "card") {
5016
5657
  const Group = Viewer;
5017
- return /* @__PURE__ */ jsx25(Group, { ...viewerProps, suppressTitle: props.suppressTopGroupTitle });
5658
+ return /* @__PURE__ */ jsx26(Group, { ...viewerProps, suppressTitle: props.suppressTopGroupTitle });
5018
5659
  }
5019
- return /* @__PURE__ */ jsx25(Viewer, { ...viewerProps });
5660
+ return /* @__PURE__ */ jsx26(Viewer, { ...viewerProps });
5020
5661
  }
5021
5662
 
5022
5663
  // src/jsonView/JsonView.tsx
5023
- import { jsx as jsx26 } from "react/jsx-runtime";
5664
+ import { jsx as jsx27 } from "react/jsx-runtime";
5024
5665
  function JsonView(props) {
5025
5666
  const { schema, value, theme, surface, density = "comfortable", className } = props;
5026
5667
  const { style } = useJsonViewTokens(theme, surface);
5027
5668
  const cls = "squisq-json-view" + (density === "compact" ? " squisq-json-view--compact" : "") + (className ? ` ${className}` : "");
5028
- return /* @__PURE__ */ jsx26("div", { className: cls, style, children: /* @__PURE__ */ jsx26(
5669
+ return /* @__PURE__ */ jsx27("div", { className: cls, style, children: /* @__PURE__ */ jsx27(
5029
5670
  RenderNode,
5030
5671
  {
5031
5672
  value,
@@ -5061,7 +5702,7 @@ export {
5061
5702
  SocialCaptionOverlay,
5062
5703
  TableLayer,
5063
5704
  TextLayer,
5064
- VIEWPORT,
5705
+ TreeLayer,
5065
5706
  VideoLayer,
5066
5707
  formatTime,
5067
5708
  getAnimationStyle,