@bendyline/squisq-react 1.4.1 → 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 +185 -27
  3. package/dist/index.js +1323 -614
  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 +235 -7
  11. package/src/DocPlayer.tsx +462 -195
  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 +556 -2
  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 +19 -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,14 +2994,101 @@ 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 }) {
2715
- const { currentBlockIndex, totalBlocks } = state;
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();
3020
+ const {
3021
+ currentBlockIndex,
3022
+ currentSlideLabel,
3023
+ currentSlideNumber,
3024
+ totalBlocks,
3025
+ totalSlideNumber
3026
+ } = state;
2716
3027
  const isFirst = currentBlockIndex <= 0;
2717
3028
  const isLast = currentBlockIndex >= totalBlocks - 1;
2718
- return /* @__PURE__ */ jsxs10(
3029
+ const counterText = totalBlocks > 0 ? currentSlideLabel ?? `${currentSlideNumber ?? currentBlockIndex + 1} / ${totalSlideNumber ?? totalBlocks}` : "\u2014";
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(
2719
3089
  "div",
2720
3090
  {
3091
+ ref: controlsRef,
2721
3092
  className: "doc-controls-slideshow",
2722
3093
  "data-testid": "slideshow-controls",
2723
3094
  style: {
@@ -2736,7 +3107,7 @@ function DocControlsSlideshow({ state, slideNav }) {
2736
3107
  WebkitBackdropFilter: "blur(8px)"
2737
3108
  },
2738
3109
  children: [
2739
- /* @__PURE__ */ jsx15(
3110
+ /* @__PURE__ */ jsx16(
2740
3111
  "button",
2741
3112
  {
2742
3113
  onClick: (e) => {
@@ -2765,27 +3136,138 @@ function DocControlsSlideshow({ state, slideNav }) {
2765
3136
  onMouseLeave: (e) => {
2766
3137
  e.currentTarget.style.background = "none";
2767
3138
  },
2768
- 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" }) })
2769
3140
  }
2770
3141
  ),
2771
- /* @__PURE__ */ jsx15(
2772
- "span",
3142
+ /* @__PURE__ */ jsx16(
3143
+ "button",
2773
3144
  {
3145
+ ref: triggerRef,
3146
+ type: "button",
2774
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
+ },
2775
3158
  style: {
3159
+ background: isPickerOpen ? "rgba(255,255,255,0.12)" : "none",
3160
+ border: "none",
2776
3161
  color: "rgba(255,255,255,0.9)",
3162
+ cursor: slides.length > 0 ? "pointer" : "default",
2777
3163
  fontSize: "13px",
2778
3164
  fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
2779
3165
  fontVariantNumeric: "tabular-nums",
2780
3166
  minWidth: "48px",
2781
3167
  textAlign: "center",
2782
- padding: "0 4px",
2783
- letterSpacing: "0.02em"
3168
+ padding: "6px 4px",
3169
+ letterSpacing: "0.02em",
3170
+ borderRadius: "4px",
3171
+ transition: "background 0.15s"
2784
3172
  },
2785
- children: totalBlocks > 0 ? `${currentBlockIndex + 1} / ${totalBlocks}` : "\u2014"
3173
+ children: counterText
2786
3174
  }
2787
3175
  ),
2788
- /* @__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(
2789
3271
  "button",
2790
3272
  {
2791
3273
  onClick: (e) => {
@@ -2814,7 +3296,7 @@ function DocControlsSlideshow({ state, slideNav }) {
2814
3296
  onMouseLeave: (e) => {
2815
3297
  e.currentTarget.style.background = "none";
2816
3298
  },
2817
- 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" }) })
2818
3300
  }
2819
3301
  )
2820
3302
  ]
@@ -2823,18 +3305,18 @@ function DocControlsSlideshow({ state, slideNav }) {
2823
3305
  }
2824
3306
 
2825
3307
  // src/LinearDocView.tsx
2826
- import { useMemo as useMemo8 } from "react";
3308
+ import { useEffect as useEffect9, useMemo as useMemo8, useRef as useRef8 } from "react";
2827
3309
  import {
2828
3310
  applySurface,
2829
3311
  resolveFontFamily as resolveFontFamily2
2830
3312
  } from "@bendyline/squisq/schemas";
2831
3313
  import { VIEWPORT_PRESETS as VIEWPORT_PRESETS3 } from "@bendyline/squisq/schemas";
2832
3314
  import {
2833
- getLayers,
2834
- hasTemplate,
3315
+ materializeBlockLayers,
2835
3316
  markdownToDoc,
2836
3317
  DEFAULT_THEME as DEFAULT_THEME2,
2837
- deriveTemplateInputs
3318
+ deriveTemplateInputs,
3319
+ isTemplateBlock as isTemplateBlock2
2838
3320
  } from "@bendyline/squisq/doc";
2839
3321
  import { extractPlainText, parseMarkdown } from "@bendyline/squisq/markdown";
2840
3322
 
@@ -2846,7 +3328,7 @@ import {
2846
3328
  } from "@bendyline/squisq/markdown";
2847
3329
 
2848
3330
  // src/InlineVideoPlayer.tsx
2849
- import { jsx as jsx16 } from "react/jsx-runtime";
3331
+ import { jsx as jsx17 } from "react/jsx-runtime";
2850
3332
  function InlineVideoPlayer({
2851
3333
  src,
2852
3334
  basePath = "",
@@ -2861,7 +3343,7 @@ function InlineVideoPlayer({
2861
3343
  const resolvedPoster = useMediaUrl(poster ?? "", basePath);
2862
3344
  const posterUrl = poster ? resolvedPoster : void 0;
2863
3345
  if (!resolvedSrc) return null;
2864
- 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(
2865
3347
  "video",
2866
3348
  {
2867
3349
  src: resolvedSrc,
@@ -2876,7 +3358,7 @@ function InlineVideoPlayer({
2876
3358
  }
2877
3359
 
2878
3360
  // src/InlineAudioPlayer.tsx
2879
- import { jsx as jsx17 } from "react/jsx-runtime";
3361
+ import { jsx as jsx18 } from "react/jsx-runtime";
2880
3362
  function InlineAudioPlayer({
2881
3363
  src,
2882
3364
  basePath = "",
@@ -2886,11 +3368,11 @@ function InlineAudioPlayer({
2886
3368
  }) {
2887
3369
  const resolvedSrc = useMediaUrl(src, basePath);
2888
3370
  if (!resolvedSrc) return null;
2889
- 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 }) });
2890
3372
  }
2891
3373
 
2892
3374
  // src/MarkdownRenderer.tsx
2893
- import { jsx as jsx18, jsxs as jsxs11 } from "react/jsx-runtime";
3375
+ import { jsx as jsx19, jsxs as jsxs12 } from "react/jsx-runtime";
2894
3376
  var DEFAULT_CTX = { htmlPolicy: "sanitize" };
2895
3377
  function renderInline(nodes, keyPrefix = "", ctx = DEFAULT_CTX) {
2896
3378
  return nodes.map((node, i) => {
@@ -2898,28 +3380,28 @@ function renderInline(nodes, keyPrefix = "", ctx = DEFAULT_CTX) {
2898
3380
  switch (node.type) {
2899
3381
  case "text": {
2900
3382
  if (!node.value.includes("\n")) {
2901
- return /* @__PURE__ */ jsx18(Fragment2, { children: node.value }, key);
3383
+ return /* @__PURE__ */ jsx19(Fragment2, { children: node.value }, key);
2902
3384
  }
2903
3385
  const parts = node.value.split("\n");
2904
- return /* @__PURE__ */ jsx18(Fragment2, { children: parts.map((part, j) => /* @__PURE__ */ jsxs11(Fragment2, { children: [
2905
- 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", {}),
2906
3388
  part
2907
3389
  ] }, j)) }, key);
2908
3390
  }
2909
3391
  case "emphasis":
2910
- 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);
2911
3393
  case "strong":
2912
- 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);
2913
3395
  case "delete":
2914
- 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);
2915
3397
  case "inlineCode":
2916
- 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);
2917
3399
  case "link": {
2918
3400
  const href = sanitizeUrl(node.url, "link", { extraLinkSchemes: ctx.linkSchemes });
2919
3401
  if (!href) {
2920
- 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);
2921
3403
  }
2922
- return /* @__PURE__ */ jsx18(
3404
+ return /* @__PURE__ */ jsx19(
2923
3405
  "a",
2924
3406
  {
2925
3407
  className: "squisq-md-link",
@@ -2933,42 +3415,32 @@ function renderInline(nodes, keyPrefix = "", ctx = DEFAULT_CTX) {
2933
3415
  );
2934
3416
  }
2935
3417
  case "image":
2936
- 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);
2937
3419
  case "break":
2938
- return /* @__PURE__ */ jsx18("br", {}, key);
3420
+ return /* @__PURE__ */ jsx19("br", {}, key);
2939
3421
  case "inlineMath":
2940
- 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);
2941
3423
  case "htmlInline":
2942
3424
  if (ctx.htmlPolicy === "strip") return null;
2943
- if (ctx.htmlPolicy === "trusted" && !containsMediaTag(node.htmlChildren) && !containsDangerousTag(node.htmlChildren) && !hasDangerousRawHtml(node.rawHtml)) {
2944
- return /* @__PURE__ */ jsx18(
2945
- "span",
2946
- {
2947
- className: "squisq-md-html-inline",
2948
- dangerouslySetInnerHTML: { __html: node.rawHtml }
2949
- },
2950
- key
2951
- );
2952
- }
2953
- 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);
2954
3426
  case "footnoteReference":
2955
- 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: [
2956
3428
  "[",
2957
3429
  node.label ?? node.identifier,
2958
3430
  "]"
2959
3431
  ] }) }, key);
2960
3432
  case "linkReference":
2961
- 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);
2962
3434
  case "imageReference":
2963
- return /* @__PURE__ */ jsxs11("span", { className: "squisq-md-image-ref", children: [
3435
+ return /* @__PURE__ */ jsxs12("span", { className: "squisq-md-image-ref", children: [
2964
3436
  "[",
2965
3437
  node.alt ?? node.identifier,
2966
3438
  "]"
2967
3439
  ] }, key);
2968
3440
  case "textDirective":
2969
- 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);
2970
3442
  case "mention":
2971
- return /* @__PURE__ */ jsxs11(
3443
+ return /* @__PURE__ */ jsxs12(
2972
3444
  "span",
2973
3445
  {
2974
3446
  className: "squisq-md-mention mention",
@@ -2991,61 +3463,51 @@ function renderInline(nodes, keyPrefix = "", ctx = DEFAULT_CTX) {
2991
3463
  function renderBlock(node, key, ctx = DEFAULT_CTX) {
2992
3464
  switch (node.type) {
2993
3465
  case "paragraph":
2994
- 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);
2995
3467
  case "heading": {
2996
3468
  const Tag = `h${node.depth}`;
2997
- 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);
2998
3470
  }
2999
3471
  case "blockquote":
3000
- 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);
3001
3473
  case "list":
3002
3474
  if (node.ordered) {
3003
- 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);
3004
3476
  }
3005
- 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);
3006
3478
  case "code":
3007
- 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);
3008
3480
  case "thematicBreak":
3009
- return /* @__PURE__ */ jsx18("hr", { className: "squisq-md-hr" }, key);
3481
+ return /* @__PURE__ */ jsx19("hr", { className: "squisq-md-hr" }, key);
3010
3482
  case "table":
3011
3483
  return renderTable(node.children, node.align, key, ctx);
3012
3484
  case "htmlBlock":
3013
3485
  if (ctx.htmlPolicy === "strip") return null;
3014
- if (ctx.htmlPolicy === "trusted" && !containsMediaTag(node.htmlChildren) && !containsDangerousTag(node.htmlChildren) && !hasDangerousRawHtml(node.rawHtml)) {
3015
- return /* @__PURE__ */ jsx18(
3016
- "div",
3017
- {
3018
- className: "squisq-md-html-block",
3019
- dangerouslySetInnerHTML: { __html: node.rawHtml }
3020
- },
3021
- key
3022
- );
3023
- }
3024
- 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);
3025
3487
  case "math":
3026
- 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);
3027
3489
  case "definition":
3028
3490
  return null;
3029
3491
  case "footnoteDefinition":
3030
- return /* @__PURE__ */ jsxs11("div", { className: "squisq-md-footnote-def", id: `fn-${node.identifier}`, children: [
3031
- /* @__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 }),
3032
3494
  renderBlocks(node.children, key, ctx)
3033
3495
  ] }, key);
3034
3496
  case "containerDirective":
3035
- return /* @__PURE__ */ jsxs11(
3497
+ return /* @__PURE__ */ jsxs12(
3036
3498
  "div",
3037
3499
  {
3038
3500
  className: `squisq-md-directive squisq-md-directive-${node.name}`,
3039
3501
  "data-directive": node.name,
3040
3502
  children: [
3041
- 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 }),
3042
3504
  renderBlocks(node.children, key, ctx)
3043
3505
  ]
3044
3506
  },
3045
3507
  key
3046
3508
  );
3047
3509
  case "leafDirective":
3048
- return /* @__PURE__ */ jsx18(
3510
+ return /* @__PURE__ */ jsx19(
3049
3511
  "div",
3050
3512
  {
3051
3513
  className: `squisq-md-directive squisq-md-directive-${node.name}`,
@@ -3055,11 +3517,11 @@ function renderBlock(node, key, ctx = DEFAULT_CTX) {
3055
3517
  key
3056
3518
  );
3057
3519
  case "definitionList":
3058
- 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) => {
3059
3521
  if (child.type === "definitionTerm") {
3060
- 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}`);
3061
3523
  }
3062
- 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}`);
3063
3525
  }) }, key);
3064
3526
  default:
3065
3527
  return null;
@@ -3067,15 +3529,15 @@ function renderBlock(node, key, ctx = DEFAULT_CTX) {
3067
3529
  }
3068
3530
  function renderListItem(item, key, ctx = DEFAULT_CTX) {
3069
3531
  const isTask = item.checked !== null && item.checked !== void 0;
3070
- return /* @__PURE__ */ jsxs11("li", { className: `squisq-md-li${isTask ? " squisq-md-task" : ""}`, children: [
3071
- 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" }),
3072
3534
  renderBlocks(item.children, key, ctx)
3073
3535
  ] }, key);
3074
3536
  }
3075
3537
  function renderTable(rows, align, key, ctx = DEFAULT_CTX) {
3076
3538
  const [headerRow, ...bodyRows] = rows;
3077
- return /* @__PURE__ */ jsxs11("table", { className: "squisq-md-table", children: [
3078
- 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(
3079
3541
  "th",
3080
3542
  {
3081
3543
  className: "squisq-md-th",
@@ -3084,7 +3546,7 @@ function renderTable(rows, align, key, ctx = DEFAULT_CTX) {
3084
3546
  },
3085
3547
  `${key}th${ci}`
3086
3548
  )) }) }),
3087
- 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(
3088
3550
  "td",
3089
3551
  {
3090
3552
  className: "squisq-md-td",
@@ -3102,22 +3564,13 @@ function MdImage({ src, alt, title }) {
3102
3564
  const safeSrc = sanitizeUrl(src, "media");
3103
3565
  const resolved = useMediaUrl(safeSrc ?? "", ".");
3104
3566
  if (!safeSrc) return null;
3105
- 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 });
3106
3568
  }
3107
3569
  function resolveHtmlNodes(nodes, htmlPolicy) {
3108
3570
  if (htmlPolicy === "strip") return [];
3109
3571
  if (htmlPolicy === "trusted") return nodes;
3110
3572
  return sanitizeHtmlNodes2(nodes);
3111
3573
  }
3112
- function containsMediaTag(nodes) {
3113
- for (const node of nodes) {
3114
- if (node.type !== "htmlElement") continue;
3115
- const tagName = node.tagName.toLowerCase();
3116
- if (tagName === "video" || tagName === "audio") return true;
3117
- if (containsMediaTag(node.children)) return true;
3118
- }
3119
- return false;
3120
- }
3121
3574
  var DANGEROUS_HTML_TAGS = /* @__PURE__ */ new Set([
3122
3575
  "base",
3123
3576
  "embed",
@@ -3129,18 +3582,6 @@ var DANGEROUS_HTML_TAGS = /* @__PURE__ */ new Set([
3129
3582
  "style",
3130
3583
  "title"
3131
3584
  ]);
3132
- function containsDangerousTag(nodes) {
3133
- for (const node of nodes) {
3134
- if (node.type !== "htmlElement") continue;
3135
- if (DANGEROUS_HTML_TAGS.has(node.tagName.toLowerCase())) return true;
3136
- if (containsDangerousTag(node.children)) return true;
3137
- }
3138
- return false;
3139
- }
3140
- var DANGEROUS_RAW_HTML_RE = /<\s*\/?\s*(?:base|embed|iframe|link|meta|object|script|style|title)\b/i;
3141
- function hasDangerousRawHtml(rawHtml) {
3142
- return DANGEROUS_RAW_HTML_RE.test(rawHtml);
3143
- }
3144
3585
  var PASSTHROUGH_ATTRS = {
3145
3586
  // common
3146
3587
  class: "className",
@@ -3159,7 +3600,7 @@ var PASSTHROUGH_ATTRS = {
3159
3600
  target: "target",
3160
3601
  rel: "rel"
3161
3602
  };
3162
- function reactPropsFromAttrs(attrs) {
3603
+ function reactPropsFromAttrs(attrs, ctx) {
3163
3604
  const out = {};
3164
3605
  for (const [name, value] of Object.entries(attrs)) {
3165
3606
  const propName = PASSTHROUGH_ATTRS[name];
@@ -3168,21 +3609,33 @@ function reactPropsFromAttrs(attrs) {
3168
3609
  out["data-style"] = value;
3169
3610
  continue;
3170
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
+ }
3171
3622
  out[propName] = value;
3172
3623
  }
3173
3624
  return out;
3174
3625
  }
3175
- function renderHtmlElement(el, key) {
3626
+ function renderHtmlElement(el, key, ctx) {
3176
3627
  const tagName = el.tagName.toLowerCase();
3177
3628
  if (DANGEROUS_HTML_TAGS.has(tagName)) return null;
3178
3629
  if (tagName === "video") {
3179
- 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(
3180
3633
  InlineVideoPlayer,
3181
3634
  {
3182
- src: el.attributes.src ?? "",
3635
+ src,
3183
3636
  width: el.attributes.width,
3184
3637
  height: el.attributes.height,
3185
- poster: el.attributes.poster,
3638
+ poster,
3186
3639
  controls: "controls" in el.attributes,
3187
3640
  preload: el.attributes.preload === "none" || el.attributes.preload === "metadata" || el.attributes.preload === "auto" ? el.attributes.preload : void 0
3188
3641
  },
@@ -3190,10 +3643,11 @@ function renderHtmlElement(el, key) {
3190
3643
  );
3191
3644
  }
3192
3645
  if (tagName === "audio") {
3193
- return /* @__PURE__ */ jsx18(
3646
+ const src = sanitizeUrl(el.attributes.src ?? "", "media") ?? "";
3647
+ return /* @__PURE__ */ jsx19(
3194
3648
  InlineAudioPlayer,
3195
3649
  {
3196
- src: el.attributes.src ?? "",
3650
+ src,
3197
3651
  controls: "controls" in el.attributes,
3198
3652
  preload: el.attributes.preload === "none" || el.attributes.preload === "metadata" || el.attributes.preload === "auto" ? el.attributes.preload : void 0
3199
3653
  },
@@ -3201,20 +3655,20 @@ function renderHtmlElement(el, key) {
3201
3655
  );
3202
3656
  }
3203
3657
  const Tag = tagName;
3204
- const props = reactPropsFromAttrs(el.attributes);
3658
+ const props = reactPropsFromAttrs(el.attributes, ctx);
3205
3659
  if (el.selfClosing) {
3206
- return /* @__PURE__ */ jsx18(Tag, { ...props }, key);
3660
+ return /* @__PURE__ */ jsx19(Tag, { ...props }, key);
3207
3661
  }
3208
- 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);
3209
3663
  }
3210
- function renderHtmlNodes(nodes, keyPrefix) {
3664
+ function renderHtmlNodes(nodes, keyPrefix, ctx = DEFAULT_CTX) {
3211
3665
  return nodes.map((node, i) => {
3212
3666
  const key = `${keyPrefix}${i}`;
3213
3667
  switch (node.type) {
3214
3668
  case "htmlElement":
3215
- return renderHtmlElement(node, key);
3669
+ return renderHtmlElement(node, key, ctx);
3216
3670
  case "htmlText":
3217
- return /* @__PURE__ */ jsx18(Fragment2, { children: node.value }, key);
3671
+ return /* @__PURE__ */ jsx19(Fragment2, { children: node.value }, key);
3218
3672
  case "htmlComment":
3219
3673
  return null;
3220
3674
  default:
@@ -3229,25 +3683,16 @@ function MarkdownRenderer({
3229
3683
  linkSchemes
3230
3684
  }) {
3231
3685
  if (!nodes || nodes.length === 0) return null;
3232
- 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 }) });
3233
3687
  }
3234
3688
 
3235
3689
  // src/LinearDocView.tsx
3236
- import { jsx as jsx19, jsxs as jsxs12 } from "react/jsx-runtime";
3237
- var warnedUnknownTemplates = /* @__PURE__ */ new Set();
3690
+ import { jsx as jsx20, jsxs as jsxs13 } from "react/jsx-runtime";
3238
3691
  function isAnnotatedBlock(block) {
3239
- const annotation = block.sourceHeading?.templateAnnotation;
3240
- if (!annotation?.template) return false;
3241
- if (!hasTemplate(annotation.template)) {
3242
- if (!warnedUnknownTemplates.has(annotation.template)) {
3243
- warnedUnknownTemplates.add(annotation.template);
3244
- console.warn(
3245
- `[squisq] Unknown template "${annotation.template}" \u2014 rendering the block as plain markdown.`
3246
- );
3247
- }
3248
- return false;
3249
- }
3250
- 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;
3251
3696
  }
3252
3697
  function countAll(blocks) {
3253
3698
  let count = 0;
@@ -3257,50 +3702,65 @@ function countAll(blocks) {
3257
3702
  }
3258
3703
  return count;
3259
3704
  }
3260
- 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
+ }) {
3261
3714
  const isAnnotated = isAnnotatedBlock(block);
3262
3715
  const visualBlock = useMemo8(() => {
3263
3716
  if (!isAnnotated) return null;
3264
- const annotation = block.sourceHeading.templateAnnotation;
3265
- const headingText = extractPlainText(block.sourceHeading);
3266
- const templateBlock = {
3267
- id: block.id,
3268
- template: annotation.template,
3269
- startTime: 0,
3270
- duration: 1,
3271
- audioSegment: 0,
3272
- title: headingText,
3273
- ...deriveTemplateInputs(
3274
- annotation.template ?? "sectionHeader",
3275
- headingText,
3276
- block.contents,
3277
- {
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, {
3278
3731
  placeholders: true
3279
- }
3280
- ) ?? {},
3281
- ...annotation.params,
3282
- ...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
3283
3742
  };
3284
3743
  const ctx = {
3285
3744
  ...renderContext,
3286
3745
  blockIndex
3287
3746
  };
3288
- const layers = getLayers(templateBlock, ctx);
3747
+ const { layers } = materializeBlockLayers(templateBlock, ctx);
3289
3748
  return {
3290
3749
  ...block,
3291
3750
  layers,
3292
- template: annotation.template
3751
+ template: templateName
3293
3752
  };
3294
3753
  }, [block, isAnnotated, renderContext, blockIndex]);
3295
- return /* @__PURE__ */ jsxs12(
3754
+ return /* @__PURE__ */ jsxs13(
3296
3755
  "div",
3297
3756
  {
3298
3757
  className: "squisq-linear-section",
3299
3758
  "data-block-id": block.id,
3300
- "data-template": isAnnotated ? block.sourceHeading?.templateAnnotation?.template : void 0,
3759
+ "data-block-index": blockIndex,
3760
+ "data-template": isAnnotated ? visualTemplateName(block) : void 0,
3301
3761
  children: [
3302
- block.sourceHeading && !isAnnotated && /* @__PURE__ */ jsx19(MarkdownRenderer, { nodes: [block.sourceHeading] }),
3303
- 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(
3304
3764
  "div",
3305
3765
  {
3306
3766
  className: "squisq-linear-card-svg",
@@ -3310,26 +3770,29 @@ function BlockSection({ block, basePath, viewport, renderContext, blockIndex })
3310
3770
  overflow: "hidden",
3311
3771
  marginBottom: "1em"
3312
3772
  },
3313
- children: /* @__PURE__ */ jsx19(
3773
+ children: /* @__PURE__ */ jsx20(
3314
3774
  BlockRenderer,
3315
3775
  {
3316
3776
  block: visualBlock,
3317
3777
  blockTime: 0,
3318
3778
  basePath,
3319
- viewport
3779
+ viewport,
3780
+ animationsEnabled
3320
3781
  }
3321
3782
  )
3322
3783
  }
3323
3784
  ) }),
3324
- !isAnnotated && block.contents && block.contents.length > 0 && /* @__PURE__ */ jsx19(MarkdownRenderer, { nodes: block.contents }),
3325
- 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(
3326
3787
  BlockSection,
3327
3788
  {
3328
3789
  block: child,
3329
3790
  basePath,
3330
3791
  viewport,
3331
3792
  renderContext,
3332
- blockIndex: blockIndex + i + 1
3793
+ blockIndex: blockIndices.get(child) ?? blockIndex + i + 1,
3794
+ blockIndices,
3795
+ animationsEnabled
3333
3796
  },
3334
3797
  child.id
3335
3798
  )) })
@@ -3345,9 +3808,12 @@ function LinearDocView({
3345
3808
  className,
3346
3809
  theme,
3347
3810
  surface,
3811
+ animationsEnabled = true,
3348
3812
  thinMargins = false,
3349
- imageDisplayMode = "inline"
3813
+ imageDisplayMode = "inline",
3814
+ globalKeyboardShortcuts = false
3350
3815
  }) {
3816
+ const scrollRef = useRef8(null);
3351
3817
  const activeViewport = viewport ?? VIEWPORT_PRESETS3.landscape;
3352
3818
  const markdownDoc = useMemo8(
3353
3819
  () => !doc && markdown !== void 0 ? markdownToDoc(parseMarkdown(markdown)) : void 0,
@@ -3358,6 +3824,18 @@ function LinearDocView({
3358
3824
  () => resolvedDoc ? countAll(resolvedDoc.blocks) : 0,
3359
3825
  [resolvedDoc]
3360
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]);
3361
3839
  const autoSurface = useAutoSurface(surface === "auto");
3362
3840
  const resolvedSurface = surface === "auto" ? autoSurface : surface;
3363
3841
  const renderContext = useMemo8(() => {
@@ -3369,12 +3847,37 @@ function LinearDocView({
3369
3847
  totalBlocks,
3370
3848
  // Theme atmosphere (vignette/grain/gradient persistent layers) shows
3371
3849
  // on the inline template cards so they match the player's look.
3372
- persistentLayers: effectiveTheme.persistentLayers
3850
+ persistentLayers: effectiveTheme.persistentLayers,
3851
+ customTemplates: resolvedDoc?.customTemplates
3373
3852
  };
3374
- }, [activeViewport, totalBlocks, theme, resolvedSurface]);
3853
+ }, [activeViewport, resolvedDoc?.customTemplates, totalBlocks, theme, resolvedSurface]);
3375
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]);
3376
3879
  if (!resolvedDoc) {
3377
- 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 || ""}` });
3378
3881
  }
3379
3882
  const bgColor = activeTheme.colors.background;
3380
3883
  const textColor = activeTheme.colors.text;
@@ -3383,9 +3886,10 @@ function LinearDocView({
3383
3886
  const bodyFont = resolveFontFamily2(activeTheme.typography.bodyFont, "system-ui, sans-serif");
3384
3887
  const titleFont = resolveFontFamily2(activeTheme.typography.titleFont, "Georgia, serif");
3385
3888
  const lineHt = activeTheme.typography.lineHeight ?? 1.7;
3386
- return /* @__PURE__ */ jsx19(
3889
+ return /* @__PURE__ */ jsx20(
3387
3890
  "div",
3388
3891
  {
3892
+ ref: scrollRef,
3389
3893
  className: `squisq-linear ${className || ""}`,
3390
3894
  style: {
3391
3895
  width: "100%",
@@ -3399,7 +3903,7 @@ function LinearDocView({
3399
3903
  overflowX: "hidden",
3400
3904
  background: bgColor
3401
3905
  },
3402
- children: /* @__PURE__ */ jsxs12(
3906
+ children: /* @__PURE__ */ jsxs13(
3403
3907
  "div",
3404
3908
  {
3405
3909
  className: `squisq-linear-content squisq-md${thinMargins ? " squisq-linear-content--thin" : ""}${imageDisplayMode === "thumbnail" ? " squisq-linear-content--thumbnail-images" : ""}`,
@@ -3424,7 +3928,7 @@ function LinearDocView({
3424
3928
  "--squisq-linear-bg": bgColor
3425
3929
  },
3426
3930
  children: [
3427
- /* @__PURE__ */ jsx19("style", { children: `
3931
+ /* @__PURE__ */ jsx20("style", { children: `
3428
3932
  .squisq-linear-content h1,
3429
3933
  .squisq-linear-content h2,
3430
3934
  .squisq-linear-content h3,
@@ -3536,14 +4040,16 @@ function LinearDocView({
3536
4040
  background: color-mix(in srgb, var(--squisq-linear-primary) 8%, transparent);
3537
4041
  }
3538
4042
  ` }),
3539
- resolvedDoc.blocks.map((block, i) => /* @__PURE__ */ jsx19(
4043
+ resolvedDoc.blocks.map((block, i) => /* @__PURE__ */ jsx20(
3540
4044
  BlockSection,
3541
4045
  {
3542
4046
  block,
3543
4047
  basePath,
3544
4048
  viewport: activeViewport,
3545
4049
  renderContext,
3546
- blockIndex: i
4050
+ blockIndex: blockIndices.get(block) ?? i,
4051
+ blockIndices,
4052
+ animationsEnabled
3547
4053
  },
3548
4054
  block.id
3549
4055
  ))
@@ -3555,7 +4061,7 @@ function LinearDocView({
3555
4061
  }
3556
4062
 
3557
4063
  // src/DocPlayer.tsx
3558
- import { jsx as jsx20, jsxs as jsxs13 } from "react/jsx-runtime";
4064
+ import { jsx as jsx21, jsxs as jsxs14 } from "react/jsx-runtime";
3559
4065
  var SMALL_WORDS = /* @__PURE__ */ new Set([
3560
4066
  "a",
3561
4067
  "an",
@@ -3576,7 +4082,7 @@ var SMALL_WORDS = /* @__PURE__ */ new Set([
3576
4082
  function buildSegmentTitleMap(doc) {
3577
4083
  const map = /* @__PURE__ */ new Map();
3578
4084
  for (const block of doc.blocks) {
3579
- if (isTemplateBlock2(block) && block.template === "sectionHeader" && "title" in block) {
4085
+ if (isTemplateBlock3(block) && block.template === "sectionHeader" && "title" in block) {
3580
4086
  const segIdx = block.audioSegment;
3581
4087
  if (!map.has(segIdx)) {
3582
4088
  map.set(segIdx, block.title);
@@ -3617,14 +4123,15 @@ function DocPlayer(props) {
3617
4123
  );
3618
4124
  const resolvedDoc = doc ?? markdownDoc;
3619
4125
  if (!resolvedDoc) {
3620
- return /* @__PURE__ */ jsx20("div", { className: "doc-player doc-player--empty" });
4126
+ return /* @__PURE__ */ jsx21("div", { className: "doc-player doc-player--empty" });
3621
4127
  }
3622
- return /* @__PURE__ */ jsx20(DocPlayerContent, { ...props, doc: resolvedDoc });
4128
+ return /* @__PURE__ */ jsx21(DocPlayerContent, { ...props, doc: resolvedDoc });
3623
4129
  }
3624
4130
  function DocPlayerContent({
3625
4131
  doc,
3626
4132
  basePath = ".",
3627
4133
  renderMode = false,
4134
+ animationsEnabled = true,
3628
4135
  autoPlay = false,
3629
4136
  onEnded,
3630
4137
  onTimeUpdate,
@@ -3636,22 +4143,27 @@ function DocPlayerContent({
3636
4143
  onCaptionsToggle,
3637
4144
  onPlaybackStateChange,
3638
4145
  onControlsReady,
4146
+ onRenderAPIReady,
3639
4147
  isFullscreen = false,
3640
4148
  onFullscreenToggle,
3641
4149
  onBlockMarkers,
3642
4150
  forceViewport,
3643
4151
  displayMode = "video",
4152
+ showCoverSlide = true,
4153
+ coverVisible,
3644
4154
  theme,
3645
4155
  surface,
3646
4156
  captionStyle = "standard",
3647
- enableSwipe = true
4157
+ enableSwipe = true,
4158
+ globalKeyboardShortcuts = false
3648
4159
  }) {
3649
4160
  const isSlideshowMode = displayMode === "slideshow";
3650
4161
  const isLinearMode = displayMode === "linear";
3651
- const audioRef = useRef7(null);
3652
- const containerRef = useRef7(null);
3653
- const [tapFeedback, setTapFeedback] = useState7(null);
3654
- 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();
3655
4167
  const { viewport, orientation } = useViewportOrientation();
3656
4168
  const activeViewport = forceViewport || (renderMode ? VIEWPORT_PRESETS4.landscape : viewport);
3657
4169
  const isDebugMode = useMemo9(() => {
@@ -3659,9 +4171,9 @@ function DocPlayerContent({
3659
4171
  const params = new URLSearchParams(window.location.search);
3660
4172
  return params.get("debug") === "true";
3661
4173
  }, []);
3662
- const internalAudio = useAudioSync(audioRef, doc.audio, basePath);
4174
+ const internalAudio = useAudioSync(audioRef, doc.audio, basePath, !externalAudioController);
3663
4175
  const audio = externalAudioController || internalAudio;
3664
- useEffect8(() => {
4176
+ useEffect10(() => {
3665
4177
  if (warnedMissingStyles || !isDevEnvironment()) return;
3666
4178
  const el = containerRef.current;
3667
4179
  if (!el || typeof getComputedStyle !== "function") return;
@@ -3690,21 +4202,28 @@ function DocPlayerContent({
3690
4202
  restart
3691
4203
  } = audio;
3692
4204
  const mediaSchedule = useMemo9(() => resolveMediaSchedule(doc), [doc]);
3693
- const currentTimeRef = useRef7(currentTime);
4205
+ const currentTimeRef = useRef9(currentTime);
3694
4206
  currentTimeRef.current = currentTime;
3695
- const totalDurationRef = useRef7(totalDuration);
4207
+ const totalDurationRef = useRef9(totalDuration);
3696
4208
  totalDurationRef.current = totalDuration;
3697
- const expandedBlocksLenRef = useRef7(0);
3698
- const handleContainerClick = useCallback6(
4209
+ const expandedBlocksLenRef = useRef9(0);
4210
+ const handleContainerClick = useCallback7(
3699
4211
  (e) => {
3700
- if (renderMode || isSlideshowMode || isLinearMode) return;
4212
+ if (renderMode || isLinearMode) return;
3701
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
+ }
3702
4220
  if (target.closest(
3703
- "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'
3704
4222
  ))
3705
4223
  return;
4224
+ containerRef.current?.focus({ preventScroll: true });
3706
4225
  toggle();
3707
- const nextState = isPlaying ? "play" : "pause";
4226
+ const nextState = isPlaying ? "pause" : "play";
3708
4227
  setTapFeedback(nextState);
3709
4228
  clearTimeout(tapFeedbackTimer.current);
3710
4229
  tapFeedbackTimer.current = setTimeout(() => setTapFeedback(null), 600);
@@ -3728,10 +4247,16 @@ function DocPlayerContent({
3728
4247
  docProgress,
3729
4248
  nextBlock: _nextBlock,
3730
4249
  prevBlock: _prevBlock,
3731
- blocks: expandedBlocks
3732
- } = 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
+ });
3733
4257
  const coverBlock = useMemo9(() => {
3734
4258
  const startBlockConfig = doc.startBlock;
4259
+ if (!showCoverSlide) return null;
3735
4260
  if (!startBlockConfig) return null;
3736
4261
  const context = createTemplateContext(effectiveTheme, 0, 1, activeViewport);
3737
4262
  const layers = expandCoverBlock(startBlockConfig, context);
@@ -3744,99 +4269,137 @@ function DocPlayerContent({
3744
4269
  audioSegment: -1,
3745
4270
  layers
3746
4271
  };
3747
- }, [doc.startBlock, activeViewport, effectiveTheme]);
3748
- const [coverForced, setCoverForced] = useState7(false);
3749
- const [coverGraceActive, setCoverGraceActive] = useState7(false);
3750
- const coverGraceTimer = useRef7();
3751
- const coverWasShowing = useRef7(false);
3752
- const hasPlayedOnce = useRef7(false);
3753
- const atRest = !!(coverBlock && !isPlaying && currentTime === 0 && !hasPlayedOnce.current && !renderMode && !autoPlay);
4272
+ }, [doc.startBlock, activeViewport, effectiveTheme, showCoverSlide]);
4273
+ const hasManagedCover = !!coverBlock;
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(() => {
4281
+ const initKey = `${isSlideshowMode}:${hasManagedCover}:${renderMode}`;
4282
+ if (slideshowCoverInitKeyRef.current === initKey) return;
4283
+ slideshowCoverInitKeyRef.current = initKey;
4284
+ if (isSlideshowMode && hasManagedCover && !renderMode) {
4285
+ setSlideshowCoverVisible(true);
4286
+ pause();
4287
+ } else {
4288
+ setSlideshowCoverVisible(false);
4289
+ }
4290
+ }, [isSlideshowMode, hasManagedCover, renderMode, pause]);
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]);
4303
+ const atRest = !!(coverBlock && !isSlideshowMode && !isPlaying && currentTime === 0 && !hasPlayedOnce.current && !renderMode && !autoPlay);
3754
4304
  if (atRest) coverWasShowing.current = true;
3755
- useEffect8(() => {
3756
- if (isPlaying && coverWasShowing.current && coverBlock && !renderMode) {
4305
+ useEffect10(() => {
4306
+ if (isPlaying && coverWasShowing.current && coverBlock && !renderMode && !isSlideshowMode) {
3757
4307
  coverWasShowing.current = false;
3758
4308
  hasPlayedOnce.current = true;
3759
4309
  setCoverGraceActive(true);
3760
4310
  coverGraceTimer.current = setTimeout(() => setCoverGraceActive(false), 3e3);
3761
4311
  }
3762
- }, [isPlaying, coverBlock, renderMode]);
3763
- useEffect8(() => () => clearTimeout(coverGraceTimer.current), []);
3764
- const showCoverBlock = !isSlideshowMode && !isLinearMode && coverBlock && (coverForced || coverGraceActive || !isPlaying && currentTime === 0 && !hasPlayedOnce.current && !renderMode && !autoPlay);
3765
- const hasAutoPlayed = useRef7(false);
3766
- useEffect8(() => {
4312
+ }, [isPlaying, coverBlock, renderMode, isSlideshowMode]);
4313
+ useEffect10(() => () => clearTimeout(coverGraceTimer.current), []);
4314
+ const showVideoCoverBlock = !isSlideshowMode && !isLinearMode && !!coverBlock && (coverForced || coverGraceActive || !isPlaying && currentTime === 0 && !hasPlayedOnce.current && !renderMode && !autoPlay);
4315
+ const effectiveSlideshowCoverVisible = coverVisible ?? slideshowCoverVisible;
4316
+ const showSlideshowCover = !!(isSlideshowMode && !isLinearMode && !renderMode && coverBlock && effectiveSlideshowCoverVisible);
4317
+ const showCoverBlock = coverVisible === void 0 ? showVideoCoverBlock || showSlideshowCover : !!coverBlock && coverVisible;
4318
+ const slideshowHasCover = !!(isSlideshowMode && !renderMode && coverBlock);
4319
+ const slideshowSlideIndex = slideshowHasCover ? effectiveSlideshowCoverVisible ? 0 : currentBlockIndex + 1 : currentBlockIndex;
4320
+ const slideshowTotalSlides = slideshowHasCover ? expandedBlocks.length + 1 : expandedBlocks.length;
4321
+ const hasAutoPlayed = useRef9(false);
4322
+ useEffect10(() => {
4323
+ hasAutoPlayed.current = false;
4324
+ }, [doc]);
4325
+ useEffect10(() => {
3767
4326
  if (isAudioReady && autoPlay && !hasAutoPlayed.current) {
3768
4327
  hasAutoPlayed.current = true;
3769
4328
  play();
3770
4329
  }
3771
4330
  }, [isAudioReady, autoPlay, play]);
3772
- useEffect8(() => {
4331
+ useEffect10(() => {
3773
4332
  onTimeUpdate?.(currentTime);
3774
4333
  }, [currentTime, onTimeUpdate]);
3775
- useEffect8(() => {
4334
+ useEffect10(() => {
3776
4335
  if (isEnded) {
3777
4336
  onEnded?.();
3778
4337
  }
3779
4338
  }, [isEnded, onEnded]);
3780
- useEffect8(() => {
3781
- if ((renderMode || isDebugMode) && typeof window !== "undefined") {
3782
- const w = window;
3783
- w.seekTo = (time) => {
3784
- seekTo(time);
3785
- return new Promise((resolve) => {
3786
- requestAnimationFrame(() => {
3787
- let blockStartTime = 0;
3788
- for (let i = expandedBlocks.length - 1; i >= 0; i--) {
3789
- if (time >= expandedBlocks[i].startTime) {
3790
- blockStartTime = expandedBlocks[i].startTime;
3791
- break;
3792
- }
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;
3793
4379
  }
3794
- const elapsedMs = (time - blockStartTime) * 1e3;
3795
- document.getAnimations().forEach((anim) => {
3796
- const target = anim.effect?.target;
3797
- if (!target) return;
3798
- if (target.closest(".doc-player__block--active")) {
3799
- anim.currentTime = Math.max(0, elapsedMs);
3800
- } else if (target.closest(".doc-player__block--previous")) {
3801
- anim.currentTime = Math.max(0, elapsedMs);
3802
- }
3803
- });
3804
- const blockElapsed = time - blockStartTime;
3805
- const videoSeekPromises = [];
3806
- const activeBlockEl = document.querySelector(".doc-player__block--active");
3807
- if (activeBlockEl) {
3808
- const videos = activeBlockEl.querySelectorAll("video[data-clip-start]");
3809
- videos.forEach((el) => {
3810
- const video = el;
3811
- const clipStart = parseFloat(video.dataset.clipStart || "0");
3812
- const clipEnd = parseFloat(video.dataset.clipEnd || "0");
3813
- const startAt = parseFloat(video.dataset.startAt || "0");
3814
- const targetTime = Math.min(
3815
- clipStart + Math.max(0, blockElapsed - startAt),
3816
- clipEnd
3817
- );
3818
- video.pause();
3819
- video.currentTime = targetTime;
3820
- videoSeekPromises.push(
3821
- new Promise((r) => {
3822
- if (Math.abs(video.currentTime - targetTime) < 0.1) {
3823
- r();
3824
- } else {
3825
- video.addEventListener("seeked", () => r(), { once: true });
3826
- setTimeout(r, 200);
3827
- }
3828
- })
3829
- );
3830
- });
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);
3831
4389
  }
3832
- 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) => {
3833
4397
  const video = el;
3834
- const absStart = parseFloat(video.dataset.absStart || "0");
3835
- const absEnd = parseFloat(video.dataset.absEnd || "0");
3836
- 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);
3837
4402
  video.pause();
3838
- if (time < absStart || time >= absEnd) return;
3839
- const targetTime = sourceIn + (time - absStart);
3840
4403
  video.currentTime = targetTime;
3841
4404
  videoSeekPromises.push(
3842
4405
  new Promise((r) => {
@@ -3849,82 +4412,111 @@ function DocPlayerContent({
3849
4412
  })
3850
4413
  );
3851
4414
  });
3852
- Promise.all(videoSeekPromises).then(() => {
3853
- requestAnimationFrame(() => resolve());
3854
- });
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());
3855
4438
  });
3856
4439
  });
3857
- };
3858
- w.getDuration = () => {
3859
- const mediaDuration = getDocPlaybackDuration(doc);
3860
- if (totalDuration > 0) return Math.max(totalDuration, mediaDuration);
3861
- return mediaDuration;
3862
- };
3863
- w.getBlocks = () => expandedBlocks.map((s) => ({
3864
- id: s.id,
3865
- template: s.template ?? "raw",
3866
- startTime: s.startTime,
3867
- duration: s.duration
3868
- }));
3869
- w.getAudioSegments = () => doc.audio.segments.map((seg) => ({
3870
- src: seg.src,
3871
- name: seg.name,
3872
- duration: seg.duration,
3873
- 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
3874
4470
  }));
3875
- w.getCaptions = () => doc.captions?.phrases?.map((p) => ({
3876
- text: p.text,
3877
- startTime: p.startTime,
3878
- endTime: p.endTime
3879
- })) || [];
3880
- w.getChapters = () => {
3881
- const titleMap = buildSegmentTitleMap(doc);
3882
- return doc.audio.segments.map((seg, i) => ({
3883
- title: titleMap.get(i) || seg.name,
3884
- startTime: seg.startTime,
3885
- duration: seg.duration
3886
- }));
3887
- };
3888
- w.showCover = () => {
3889
- setCoverForced(true);
3890
- return new Promise((resolve) => requestAnimationFrame(() => resolve()));
3891
- };
3892
- w.hideCover = () => {
3893
- setCoverForced(false);
3894
- return new Promise((resolve) => requestAnimationFrame(() => resolve()));
3895
- };
3896
- w.hasCoverBlock = () => !!coverBlock;
3897
- }
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;
3898
4493
  return () => {
3899
- if (typeof window !== "undefined") {
3900
- const w = window;
3901
- delete w.seekTo;
3902
- delete w.getDuration;
3903
- delete w.getBlocks;
3904
- delete w.getAudioSegments;
3905
- delete w.getCaptions;
3906
- delete w.getChapters;
3907
- delete w.showCover;
3908
- delete w.hideCover;
3909
- delete w.hasCoverBlock;
3910
- }
4494
+ if (liveRenderAPIRef.current === api) liveRenderAPIRef.current = null;
3911
4495
  };
3912
- }, [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]);
3913
4505
  const defaultMode = captionsEnabledProp === false ? "off" : captionStyle || "standard";
3914
- const [captionMode, setCaptionMode] = useState7(defaultMode);
3915
- useEffect8(() => {
4506
+ const [captionMode, setCaptionMode] = useState9(defaultMode);
4507
+ useEffect10(() => {
3916
4508
  setCaptionMode(defaultMode);
3917
4509
  }, [defaultMode]);
3918
4510
  const captionsEnabled = captionMode !== "off";
3919
4511
  const activeCaptionStyle = captionMode === "social" ? "social" : "standard";
3920
- const setCaptionsEnabled = useCallback6(
4512
+ const setCaptionsEnabled = useCallback7(
3921
4513
  (enabled) => {
3922
4514
  setCaptionMode(enabled ? captionStyle || "standard" : "off");
3923
4515
  onCaptionsToggle?.(enabled);
3924
4516
  },
3925
4517
  [onCaptionsToggle, captionStyle]
3926
4518
  );
3927
- const cycleCaptionMode = useCallback6(() => {
4519
+ const cycleCaptionMode = useCallback7(() => {
3928
4520
  setCaptionMode((prev) => {
3929
4521
  const next = prev === "off" ? "standard" : prev === "standard" ? "social" : "off";
3930
4522
  onCaptionsToggle?.(next !== "off");
@@ -3938,8 +4530,9 @@ function DocPlayerContent({
3938
4530
  isPlaying,
3939
4531
  currentTime,
3940
4532
  totalDuration,
3941
- currentBlockIndex,
3942
- totalBlocks: expandedBlocks.length,
4533
+ isCoverVisible: showCoverBlock,
4534
+ currentBlockIndex: slideshowSlideIndex,
4535
+ totalBlocks: slideshowTotalSlides,
3943
4536
  docProgress,
3944
4537
  hasCaptions: !!hasCaptions,
3945
4538
  captionsEnabled,
@@ -3947,15 +4540,19 @@ function DocPlayerContent({
3947
4540
  isFullscreen,
3948
4541
  currentSegmentIndex: currentSegment,
3949
4542
  currentSegmentName: segmentTitleMap.get(currentSegment) ?? doc.audio.segments[currentSegment]?.name ?? null,
3950
- currentBlock: currentBlock ?? null
4543
+ currentBlock: showSlideshowCover ? coverBlock : currentBlock ?? null,
4544
+ currentSlideLabel: showSlideshowCover ? "Cover" : void 0,
4545
+ currentSlideNumber: slideshowHasCover && !showSlideshowCover ? currentBlockIndex + 1 : void 0,
4546
+ totalSlideNumber: slideshowHasCover ? expandedBlocks.length : void 0
3951
4547
  }),
3952
4548
  // eslint-disable-next-line react-hooks/exhaustive-deps -- doc.audio.segments is stable within a given doc
3953
4549
  [
3954
4550
  isPlaying,
3955
4551
  currentTime,
3956
4552
  totalDuration,
3957
- currentBlockIndex,
3958
- expandedBlocks.length,
4553
+ showCoverBlock,
4554
+ slideshowSlideIndex,
4555
+ slideshowTotalSlides,
3959
4556
  docProgress,
3960
4557
  hasCaptions,
3961
4558
  captionsEnabled,
@@ -3963,7 +4560,12 @@ function DocPlayerContent({
3963
4560
  isFullscreen,
3964
4561
  currentSegment,
3965
4562
  segmentTitleMap,
3966
- currentBlock
4563
+ currentBlock,
4564
+ currentBlockIndex,
4565
+ showSlideshowCover,
4566
+ coverBlock,
4567
+ slideshowHasCover,
4568
+ expandedBlocks.length
3967
4569
  ]
3968
4570
  );
3969
4571
  const playbackActions = useMemo9(
@@ -3980,24 +4582,56 @@ function DocPlayerContent({
3980
4582
  const slideNavActions = useMemo9(
3981
4583
  () => ({
3982
4584
  nextSlide: () => {
4585
+ if (slideshowHasCover && slideshowCoverVisible) {
4586
+ const target = expandedBlocks[0];
4587
+ if (target) {
4588
+ setSlideshowCoverVisible(false);
4589
+ seekTo(target.startTime);
4590
+ pause();
4591
+ }
4592
+ return;
4593
+ }
3983
4594
  if (currentBlockIndex < expandedBlocks.length - 1) {
3984
4595
  const target = expandedBlocks[currentBlockIndex + 1];
3985
4596
  if (target) {
4597
+ setSlideshowCoverVisible(false);
3986
4598
  seekTo(target.startTime);
3987
4599
  pause();
3988
4600
  }
3989
4601
  }
3990
4602
  },
3991
4603
  prevSlide: () => {
4604
+ if (slideshowHasCover && !slideshowCoverVisible && currentBlockIndex <= 0) {
4605
+ setSlideshowCoverVisible(true);
4606
+ seekTo(0);
4607
+ pause();
4608
+ return;
4609
+ }
3992
4610
  if (currentBlockIndex > 0) {
3993
4611
  const target = expandedBlocks[currentBlockIndex - 1];
3994
4612
  if (target) {
4613
+ setSlideshowCoverVisible(false);
3995
4614
  seekTo(target.startTime);
3996
4615
  pause();
3997
4616
  }
3998
4617
  }
3999
4618
  },
4000
4619
  goToSlide: (index) => {
4620
+ if (slideshowHasCover) {
4621
+ if (index === 0) {
4622
+ setSlideshowCoverVisible(true);
4623
+ seekTo(0);
4624
+ pause();
4625
+ return;
4626
+ }
4627
+ const target = expandedBlocks[index - 1];
4628
+ if (target) {
4629
+ setSlideshowCoverVisible(false);
4630
+ seekTo(target.startTime);
4631
+ pause();
4632
+ }
4633
+ return;
4634
+ }
4001
4635
  if (index >= 0 && index < expandedBlocks.length) {
4002
4636
  const target = expandedBlocks[index];
4003
4637
  if (target) {
@@ -4007,26 +4641,42 @@ function DocPlayerContent({
4007
4641
  }
4008
4642
  }
4009
4643
  }),
4010
- [currentBlockIndex, expandedBlocks, seekTo, pause]
4644
+ [currentBlockIndex, expandedBlocks, seekTo, pause, slideshowHasCover, slideshowCoverVisible]
4011
4645
  );
4012
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]);
4013
4663
  const swipe = useSlideSwipe({
4014
4664
  enabled: swipeEnabled,
4015
4665
  containerRef,
4016
- canGoNext: currentBlockIndex < expandedBlocks.length - 1,
4017
- canGoPrev: currentBlockIndex > 0,
4018
- onNext: slideNavActions.nextSlide,
4019
- onPrev: slideNavActions.prevSlide
4666
+ canGoNext: slideshowSlideIndex < slideshowTotalSlides - 1,
4667
+ canGoPrev: slideshowSlideIndex > 0,
4668
+ onNext: handleSwipeNext,
4669
+ onPrev: handleSwipePrev
4020
4670
  });
4021
- useEffect8(() => {
4671
+ useEffect10(() => {
4022
4672
  onPlaybackStateChange?.(playbackState);
4023
4673
  }, [playbackState, onPlaybackStateChange]);
4024
- useEffect8(() => {
4674
+ useEffect10(() => {
4025
4675
  onControlsReady?.({ play, pause, ...playbackActions });
4026
4676
  }, [play, pause, playbackActions, onControlsReady]);
4027
- const getBlockTitle = useCallback6((block) => {
4677
+ const getBlockTitle = useCallback7((block) => {
4028
4678
  const docBlock = block;
4029
- if (isTemplateBlock2(docBlock)) {
4679
+ if (isTemplateBlock3(docBlock)) {
4030
4680
  const props = docBlock;
4031
4681
  if (typeof props.title === "string") return props.title;
4032
4682
  if (typeof props.stat === "string") return props.stat;
@@ -4048,6 +4698,22 @@ function DocPlayerContent({
4048
4698
  }
4049
4699
  return block.id.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
4050
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]);
4051
4717
  const blockMarkers = useMemo9(() => {
4052
4718
  if (!totalDuration || !expandedBlocks.length) return [];
4053
4719
  let prevSegment = -1;
@@ -4063,16 +4729,26 @@ function DocPlayerContent({
4063
4729
  };
4064
4730
  });
4065
4731
  }, [expandedBlocks, totalDuration, getBlockTitle]);
4066
- useEffect8(() => {
4732
+ useEffect10(() => {
4067
4733
  if (blockMarkers.length > 0) {
4068
4734
  onBlockMarkers?.(blockMarkers);
4069
4735
  }
4070
4736
  }, [blockMarkers, onBlockMarkers]);
4071
- expandedBlocksLenRef.current = expandedBlocks.length;
4072
- const handleKeyDown = useCallback6(
4073
- (e) => {
4074
- const activeEl = document.activeElement;
4075
- if (activeEl && (activeEl.tagName === "INPUT" || activeEl.tagName === "TEXTAREA" || activeEl.tagName === "SELECT")) {
4737
+ expandedBlocksLenRef.current = isSlideshowMode ? slideshowTotalSlides : expandedBlocks.length;
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) {
4076
4752
  return;
4077
4753
  }
4078
4754
  if (isLinearMode) return;
@@ -4085,10 +4761,13 @@ function DocPlayerContent({
4085
4761
  slideNavActions.nextSlide();
4086
4762
  break;
4087
4763
  case "ArrowLeft":
4088
- case "ArrowUp":
4089
4764
  e.preventDefault();
4090
4765
  slideNavActions.prevSlide();
4091
4766
  break;
4767
+ case "ArrowUp":
4768
+ e.preventDefault();
4769
+ setIsSlideshowPickerOpen(true);
4770
+ break;
4092
4771
  case "Home":
4093
4772
  e.preventDefault();
4094
4773
  slideNavActions.goToSlide(0);
@@ -4105,9 +4784,11 @@ function DocPlayerContent({
4105
4784
  toggle();
4106
4785
  break;
4107
4786
  case "ArrowRight":
4787
+ e.preventDefault();
4108
4788
  seekTo(Math.min(currentTimeRef.current + 10, totalDurationRef.current));
4109
4789
  break;
4110
4790
  case "ArrowLeft":
4791
+ e.preventDefault();
4111
4792
  seekTo(Math.max(currentTimeRef.current - 10, 0));
4112
4793
  break;
4113
4794
  }
@@ -4115,16 +4796,24 @@ function DocPlayerContent({
4115
4796
  },
4116
4797
  [isSlideshowMode, isLinearMode, toggle, seekTo, slideNavActions]
4117
4798
  );
4118
- useEffect8(() => {
4119
- if (renderMode) return;
4120
- window.addEventListener("keydown", handleKeyDown);
4121
- return () => window.removeEventListener("keydown", handleKeyDown);
4122
- }, [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]);
4123
4811
  if (isLinearMode) {
4124
- return /* @__PURE__ */ jsx20(
4812
+ return /* @__PURE__ */ jsx21(
4125
4813
  "div",
4126
4814
  {
4127
4815
  ref: containerRef,
4816
+ "data-player-id": playerId,
4128
4817
  className: "doc-player doc-player--linear",
4129
4818
  style: {
4130
4819
  position: "relative",
@@ -4132,23 +4821,28 @@ function DocPlayerContent({
4132
4821
  height: "100%",
4133
4822
  overflow: "hidden"
4134
4823
  },
4135
- children: /* @__PURE__ */ jsx20(
4824
+ children: /* @__PURE__ */ jsx21(
4136
4825
  LinearDocView,
4137
4826
  {
4138
4827
  doc,
4139
4828
  basePath,
4140
4829
  viewport: activeViewport,
4141
4830
  theme,
4142
- surface
4831
+ surface,
4832
+ animationsEnabled
4143
4833
  }
4144
4834
  )
4145
4835
  }
4146
4836
  );
4147
4837
  }
4148
- return /* @__PURE__ */ jsxs13(
4838
+ return /* @__PURE__ */ jsxs14(
4149
4839
  "div",
4150
4840
  {
4151
4841
  ref: containerRef,
4842
+ "data-player-id": playerId,
4843
+ tabIndex: renderMode ? -1 : 0,
4844
+ "aria-label": "Document player",
4845
+ onKeyDown: renderMode ? void 0 : handleKeyDown,
4152
4846
  className: `doc-player${swipeEnabled ? " doc-player--swipe" : ""}${swipe.phase === "dragging" ? " doc-player--grabbing" : ""}`,
4153
4847
  onClick: handleContainerClick,
4154
4848
  onPointerDown: swipe.onPointerDown,
@@ -4164,33 +4858,35 @@ function DocPlayerContent({
4164
4858
  touchAction: swipeEnabled ? "pan-y" : void 0
4165
4859
  },
4166
4860
  children: [
4167
- /* @__PURE__ */ jsx20("audio", { ref: audioRef, preload: "auto", muted }),
4168
- /* @__PURE__ */ jsx20(
4861
+ /* @__PURE__ */ jsx21("audio", { ref: audioRef, preload: "auto", muted }),
4862
+ /* @__PURE__ */ jsx21(
4169
4863
  MediaClipLayer,
4170
4864
  {
4171
4865
  schedule: mediaSchedule,
4172
4866
  currentTime,
4173
4867
  isPlaying,
4174
4868
  basePath,
4175
- renderMode
4869
+ renderMode,
4870
+ muted
4176
4871
  }
4177
4872
  ),
4178
- /* @__PURE__ */ jsxs13("div", { className: "doc-player__viewport", children: [
4179
- 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(
4180
4875
  BlockRenderer,
4181
4876
  {
4182
4877
  block: coverBlock,
4183
4878
  blockTime: 0,
4184
4879
  basePath,
4185
4880
  isEntering: false,
4186
- viewport: activeViewport
4881
+ viewport: activeViewport,
4882
+ animationsEnabled
4187
4883
  }
4188
4884
  ) }),
4189
- !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
4190
4886
  // reconciles one block's layers onto another's (templates reuse layer
4191
4887
  // ids like `title`/`background`), which would otherwise reuse stale
4192
4888
  // DOM / skip entrance animations mid-transition.
4193
- /* @__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(
4194
4890
  BlockRenderer,
4195
4891
  {
4196
4892
  block: previousBlock,
@@ -4198,29 +4894,31 @@ function DocPlayerContent({
4198
4894
  basePath,
4199
4895
  isExiting: true,
4200
4896
  transition: currentBlock?.transition,
4201
- viewport: activeViewport
4897
+ viewport: activeViewport,
4898
+ animationsEnabled
4202
4899
  }
4203
4900
  ) }, previousBlock.id),
4204
- !showCoverBlock && currentBlock && /* @__PURE__ */ jsx20(
4901
+ !showCoverBlock && currentBlock && /* @__PURE__ */ jsx21(
4205
4902
  "div",
4206
4903
  {
4207
4904
  className: `doc-player__block doc-player__block--active${swipe.phase !== "idle" ? ` doc-player__block--${swipe.phase}` : ""}`,
4208
4905
  style: swipe.phase !== "idle" ? { transform: `translateX(${swipe.offsetPx}px)` } : void 0,
4209
- children: /* @__PURE__ */ jsx20(
4906
+ children: /* @__PURE__ */ jsx21(
4210
4907
  BlockRenderer,
4211
4908
  {
4212
4909
  block: currentBlock,
4213
4910
  blockTime,
4214
4911
  basePath,
4215
- isEntering,
4912
+ isEntering: animationsEnabled && isEntering,
4216
4913
  viewport: activeViewport,
4217
- isPlaying
4914
+ isPlaying,
4915
+ animationsEnabled
4218
4916
  }
4219
4917
  )
4220
4918
  },
4221
4919
  currentBlock.id
4222
4920
  ),
4223
- hasCaptions && (renderMode ? captionsEnabled : true) && /* @__PURE__ */ jsx20(
4921
+ hasCaptions && (renderMode ? captionsEnabled : true) && /* @__PURE__ */ jsx21(
4224
4922
  CaptionOverlay,
4225
4923
  {
4226
4924
  captions: doc.captions,
@@ -4232,7 +4930,7 @@ function DocPlayerContent({
4232
4930
  viewport: activeViewport
4233
4931
  }
4234
4932
  ),
4235
- isDebugMode && /* @__PURE__ */ jsxs13(
4933
+ isDebugMode && /* @__PURE__ */ jsxs14(
4236
4934
  "div",
4237
4935
  {
4238
4936
  className: "doc-player__debug",
@@ -4253,27 +4951,27 @@ function DocPlayerContent({
4253
4951
  textAlign: "left"
4254
4952
  },
4255
4953
  children: [
4256
- /* @__PURE__ */ jsx20("div", { style: { color: "#ffcc00", fontWeight: "bold", marginBottom: "4px" }, children: "DEBUG MODE" }),
4257
- /* @__PURE__ */ jsxs13("div", { children: [
4258
- /* @__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:" }),
4259
4957
  " ",
4260
- /* @__PURE__ */ jsx20("span", { style: { color: "#ff6b6b" }, children: currentBlock?.template ?? "raw" })
4958
+ /* @__PURE__ */ jsx21("span", { style: { color: "#ff6b6b" }, children: currentBlock?.template ?? "raw" })
4261
4959
  ] }),
4262
- /* @__PURE__ */ jsxs13("div", { children: [
4263
- /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "block:" }),
4960
+ /* @__PURE__ */ jsxs14("div", { children: [
4961
+ /* @__PURE__ */ jsx21("span", { style: { color: "#888" }, children: "block:" }),
4264
4962
  " ",
4265
4963
  currentBlockIndex + 1,
4266
4964
  "/",
4267
4965
  expandedBlocks.length,
4268
4966
  " ",
4269
- /* @__PURE__ */ jsxs13("span", { style: { color: "#666" }, children: [
4967
+ /* @__PURE__ */ jsxs14("span", { style: { color: "#666" }, children: [
4270
4968
  "(",
4271
4969
  currentBlock?.id || "none",
4272
4970
  ")"
4273
4971
  ] })
4274
4972
  ] }),
4275
- /* @__PURE__ */ jsxs13("div", { children: [
4276
- /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "time:" }),
4973
+ /* @__PURE__ */ jsxs14("div", { children: [
4974
+ /* @__PURE__ */ jsx21("span", { style: { color: "#888" }, children: "time:" }),
4277
4975
  " ",
4278
4976
  currentTime.toFixed(2),
4279
4977
  "s /",
@@ -4281,7 +4979,7 @@ function DocPlayerContent({
4281
4979
  totalDuration.toFixed(1),
4282
4980
  "s",
4283
4981
  " ",
4284
- /* @__PURE__ */ jsxs13("span", { style: { color: "#666" }, children: [
4982
+ /* @__PURE__ */ jsxs14("span", { style: { color: "#666" }, children: [
4285
4983
  "(progress: ",
4286
4984
  (docProgress * 100).toFixed(1),
4287
4985
  "%, scriptDur: ",
@@ -4289,8 +4987,8 @@ function DocPlayerContent({
4289
4987
  ")"
4290
4988
  ] })
4291
4989
  ] }),
4292
- /* @__PURE__ */ jsxs13("div", { children: [
4293
- /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "blockTime:" }),
4990
+ /* @__PURE__ */ jsxs14("div", { children: [
4991
+ /* @__PURE__ */ jsx21("span", { style: { color: "#888" }, children: "blockTime:" }),
4294
4992
  " ",
4295
4993
  blockTime.toFixed(2),
4296
4994
  "s /",
@@ -4298,58 +4996,58 @@ function DocPlayerContent({
4298
4996
  (currentBlock?.duration || 0).toFixed(1),
4299
4997
  "s"
4300
4998
  ] }),
4301
- /* @__PURE__ */ jsxs13("div", { children: [
4302
- /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "segment:" }),
4999
+ /* @__PURE__ */ jsxs14("div", { children: [
5000
+ /* @__PURE__ */ jsx21("span", { style: { color: "#888" }, children: "segment:" }),
4303
5001
  " ",
4304
5002
  currentSegment,
4305
5003
  "/",
4306
5004
  doc.audio.segments.length - 1,
4307
5005
  " ",
4308
- /* @__PURE__ */ jsxs13("span", { style: { color: "#666" }, children: [
5006
+ /* @__PURE__ */ jsxs14("span", { style: { color: "#666" }, children: [
4309
5007
  "(",
4310
5008
  doc.audio.segments[currentSegment]?.name || "none",
4311
5009
  ")"
4312
5010
  ] })
4313
5011
  ] }),
4314
- /* @__PURE__ */ jsxs13("div", { children: [
4315
- /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "viewport:" }),
5012
+ /* @__PURE__ */ jsxs14("div", { children: [
5013
+ /* @__PURE__ */ jsx21("span", { style: { color: "#888" }, children: "viewport:" }),
4316
5014
  " ",
4317
5015
  activeViewport.name || `${activeViewport.width}x${activeViewport.height}`,
4318
5016
  " ",
4319
- /* @__PURE__ */ jsxs13("span", { style: { color: "#666" }, children: [
5017
+ /* @__PURE__ */ jsxs14("span", { style: { color: "#666" }, children: [
4320
5018
  "(",
4321
5019
  orientation,
4322
5020
  ")"
4323
5021
  ] })
4324
5022
  ] }),
4325
- /* @__PURE__ */ jsxs13("div", { children: [
4326
- /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "playing:" }),
5023
+ /* @__PURE__ */ jsxs14("div", { children: [
5024
+ /* @__PURE__ */ jsx21("span", { style: { color: "#888" }, children: "playing:" }),
4327
5025
  " ",
4328
- /* @__PURE__ */ jsx20("span", { style: { color: isPlaying ? "#4ade80" : "#f87171" }, children: isPlaying ? "yes" : "no" }),
4329
- 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)" })
4330
5028
  ] }),
4331
5029
  hasCaptions && (() => {
4332
5030
  const debugPhrase = getCaptionAtTime2(doc.captions, currentTime);
4333
5031
  const debugEnabled = captionsEnabled && (isPlaying || currentTime > 0);
4334
- return /* @__PURE__ */ jsxs13(Fragment3, { children: [
4335
- /* @__PURE__ */ jsxs13("div", { children: [
4336
- /* @__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:" }),
4337
5035
  " ",
4338
5036
  doc.captions?.phrases.length || 0,
4339
5037
  " phrases",
4340
5038
  " ",
4341
- /* @__PURE__ */ jsxs13("span", { style: { color: captionsEnabled ? "#4ade80" : "#666" }, children: [
5039
+ /* @__PURE__ */ jsxs14("span", { style: { color: captionsEnabled ? "#4ade80" : "#666" }, children: [
4342
5040
  "(",
4343
5041
  captionsEnabled ? "on" : "off",
4344
5042
  ")"
4345
5043
  ] })
4346
5044
  ] }),
4347
- /* @__PURE__ */ jsxs13("div", { children: [
4348
- /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "cc.enabled:" }),
5045
+ /* @__PURE__ */ jsxs14("div", { children: [
5046
+ /* @__PURE__ */ jsx21("span", { style: { color: "#888" }, children: "cc.enabled:" }),
4349
5047
  " ",
4350
- /* @__PURE__ */ jsx20("span", { style: { color: debugEnabled ? "#4ade80" : "#f87171" }, children: String(debugEnabled) }),
5048
+ /* @__PURE__ */ jsx21("span", { style: { color: debugEnabled ? "#4ade80" : "#f87171" }, children: String(debugEnabled) }),
4351
5049
  " ",
4352
- /* @__PURE__ */ jsxs13("span", { style: { color: "#666" }, children: [
5050
+ /* @__PURE__ */ jsxs14("span", { style: { color: "#666" }, children: [
4353
5051
  "(playing=",
4354
5052
  String(isPlaying),
4355
5053
  " t>0=",
@@ -4357,15 +5055,15 @@ function DocPlayerContent({
4357
5055
  ")"
4358
5056
  ] })
4359
5057
  ] }),
4360
- /* @__PURE__ */ jsxs13("div", { children: [
4361
- /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "cc.phrase:" }),
5058
+ /* @__PURE__ */ jsxs14("div", { children: [
5059
+ /* @__PURE__ */ jsx21("span", { style: { color: "#888" }, children: "cc.phrase:" }),
4362
5060
  " ",
4363
- /* @__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" })
4364
5062
  ] }),
4365
- debugPhrase && /* @__PURE__ */ jsxs13("div", { children: [
4366
- /* @__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:" }),
4367
5065
  " ",
4368
- /* @__PURE__ */ jsxs13("span", { style: { color: "#60a5fa" }, children: [
5066
+ /* @__PURE__ */ jsxs14("span", { style: { color: "#60a5fa" }, children: [
4369
5067
  debugPhrase.startTime.toFixed(2),
4370
5068
  "-",
4371
5069
  debugPhrase.endTime.toFixed(2)
@@ -4377,7 +5075,7 @@ function DocPlayerContent({
4377
5075
  }
4378
5076
  )
4379
5077
  ] }),
4380
- !isAvailable && unavailableMessage && /* @__PURE__ */ jsxs13(
5078
+ !isAvailable && unavailableMessage && /* @__PURE__ */ jsxs14(
4381
5079
  "div",
4382
5080
  {
4383
5081
  className: "doc-player__unavailable",
@@ -4398,12 +5096,12 @@ function DocPlayerContent({
4398
5096
  zIndex: 50
4399
5097
  },
4400
5098
  children: [
4401
- /* @__PURE__ */ jsx20("span", { style: { fontSize: "32px" }, children: "\u{1F50A}" }),
4402
- /* @__PURE__ */ jsx20("span", { children: unavailableMessage })
5099
+ /* @__PURE__ */ jsx21("span", { style: { fontSize: "32px" }, children: "\u{1F50A}" }),
5100
+ /* @__PURE__ */ jsx21("span", { children: unavailableMessage })
4403
5101
  ]
4404
5102
  }
4405
5103
  ),
4406
- !renderMode && !isSlideshowMode && showControls && /* @__PURE__ */ jsx20(
5104
+ !renderMode && !isSlideshowMode && showControls && /* @__PURE__ */ jsx21(
4407
5105
  DocControlsOverlay,
4408
5106
  {
4409
5107
  state: playbackState,
@@ -4413,7 +5111,7 @@ function DocPlayerContent({
4413
5111
  getBlockTitle
4414
5112
  }
4415
5113
  ),
4416
- !renderMode && !isSlideshowMode && !showControls && showScrubber && /* @__PURE__ */ jsx20(
5114
+ !renderMode && !isSlideshowMode && !showControls && showScrubber && /* @__PURE__ */ jsx21(
4417
5115
  "div",
4418
5116
  {
4419
5117
  className: "doc-player__scrubber",
@@ -4428,7 +5126,7 @@ function DocPlayerContent({
4428
5126
  alignItems: "center",
4429
5127
  zIndex: 100
4430
5128
  },
4431
- children: /* @__PURE__ */ jsx20(
5129
+ children: /* @__PURE__ */ jsx21(
4432
5130
  DocProgressBar,
4433
5131
  {
4434
5132
  state: playbackState,
@@ -4440,15 +5138,24 @@ function DocPlayerContent({
4440
5138
  )
4441
5139
  }
4442
5140
  ),
4443
- !renderMode && isSlideshowMode && /* @__PURE__ */ jsx20(DocControlsSlideshow, { state: playbackState, slideNav: slideNavActions }),
4444
- !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)
4445
5152
  ]
4446
5153
  }
4447
5154
  );
4448
5155
  }
4449
5156
 
4450
5157
  // src/DocControlsBottom.tsx
4451
- import { jsx as jsx21, jsxs as jsxs14 } from "react/jsx-runtime";
5158
+ import { jsx as jsx22, jsxs as jsxs15 } from "react/jsx-runtime";
4452
5159
  function DocControlsBottom({
4453
5160
  state,
4454
5161
  actions,
@@ -4456,32 +5163,32 @@ function DocControlsBottom({
4456
5163
  expandedBlocks,
4457
5164
  getBlockTitle
4458
5165
  }) {
4459
- return /* @__PURE__ */ jsxs14("div", { className: "doc-controls-bottom", children: [
4460
- /* @__PURE__ */ jsx21(
5166
+ return /* @__PURE__ */ jsxs15("div", { className: "doc-controls-bottom", children: [
5167
+ /* @__PURE__ */ jsx22(
4461
5168
  "button",
4462
5169
  {
4463
5170
  className: "bottom-ctrl-btn",
4464
5171
  onClick: actions.restart,
4465
5172
  title: "Restart",
4466
5173
  "aria-label": "Restart from beginning",
4467
- 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" }) })
4468
5175
  }
4469
5176
  ),
4470
- /* @__PURE__ */ jsx21(
5177
+ /* @__PURE__ */ jsx22(
4471
5178
  "button",
4472
5179
  {
4473
5180
  className: "bottom-ctrl-btn bottom-play-btn",
4474
5181
  onClick: actions.toggle,
4475
5182
  "aria-label": state.isPlaying ? "Pause" : "Play",
4476
- 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" }) })
4477
5184
  }
4478
5185
  ),
4479
- /* @__PURE__ */ jsxs14("span", { className: "bottom-time", children: [
5186
+ /* @__PURE__ */ jsxs15("span", { className: "bottom-time", children: [
4480
5187
  formatTime(state.currentTime),
4481
5188
  " / ",
4482
5189
  formatTime(state.totalDuration)
4483
5190
  ] }),
4484
- /* @__PURE__ */ jsx21(
5191
+ /* @__PURE__ */ jsx22(
4485
5192
  DocProgressBar,
4486
5193
  {
4487
5194
  state,
@@ -4491,82 +5198,82 @@ function DocControlsBottom({
4491
5198
  getBlockTitle
4492
5199
  }
4493
5200
  ),
4494
- /* @__PURE__ */ jsxs14("span", { className: "bottom-segment", children: [
5201
+ /* @__PURE__ */ jsxs15("span", { className: "bottom-segment", children: [
4495
5202
  state.currentBlockIndex + 1,
4496
5203
  "/",
4497
5204
  state.totalBlocks
4498
5205
  ] }),
4499
- state.hasCaptions && /* @__PURE__ */ jsx21(
5206
+ state.hasCaptions && /* @__PURE__ */ jsx22(
4500
5207
  "button",
4501
5208
  {
4502
5209
  className: `bottom-ctrl-btn ${state.captionMode !== "off" ? "bottom-ctrl-btn--active" : ""}`,
4503
5210
  onClick: () => actions.cycleCaptionMode(),
4504
5211
  title: state.captionMode === "off" ? "Captions: Off" : state.captionMode === "standard" ? "Captions: Standard" : "Captions: Social",
4505
5212
  "aria-label": "Cycle caption style",
4506
- 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" }) })
4507
5214
  }
4508
5215
  )
4509
5216
  ] });
4510
5217
  }
4511
5218
 
4512
5219
  // src/DocControlsSidebar.tsx
4513
- import { jsx as jsx22, jsxs as jsxs15 } from "react/jsx-runtime";
5220
+ import { jsx as jsx23, jsxs as jsxs16 } from "react/jsx-runtime";
4514
5221
  function DocControlsSidebar({ state, actions }) {
4515
- return /* @__PURE__ */ jsxs15("div", { className: "doc-controls-sidebar", children: [
4516
- /* @__PURE__ */ jsx22(
5222
+ return /* @__PURE__ */ jsxs16("div", { className: "doc-controls-sidebar", children: [
5223
+ /* @__PURE__ */ jsx23(
4517
5224
  "button",
4518
5225
  {
4519
5226
  className: "sidebar-ctrl-btn",
4520
5227
  onClick: actions.restart,
4521
5228
  title: "Restart",
4522
5229
  "aria-label": "Restart from beginning",
4523
- 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" }) })
4524
5231
  }
4525
5232
  ),
4526
- /* @__PURE__ */ jsx22(
5233
+ /* @__PURE__ */ jsx23(
4527
5234
  "button",
4528
5235
  {
4529
5236
  className: "sidebar-ctrl-btn sidebar-play-btn",
4530
5237
  onClick: actions.toggle,
4531
5238
  "aria-label": state.isPlaying ? "Pause" : "Play",
4532
- 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" }) })
4533
5240
  }
4534
5241
  ),
4535
- /* @__PURE__ */ jsxs15("div", { className: "sidebar-time", children: [
4536
- /* @__PURE__ */ jsx22("div", { children: formatTime(state.currentTime) }),
4537
- /* @__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) })
4538
5245
  ] }),
4539
- /* @__PURE__ */ jsxs15("div", { className: "sidebar-segment", children: [
5246
+ /* @__PURE__ */ jsxs16("div", { className: "sidebar-segment", children: [
4540
5247
  state.currentBlockIndex + 1,
4541
5248
  "/",
4542
5249
  state.totalBlocks
4543
5250
  ] }),
4544
- state.hasCaptions && /* @__PURE__ */ jsx22(
5251
+ state.hasCaptions && /* @__PURE__ */ jsx23(
4545
5252
  "button",
4546
5253
  {
4547
5254
  className: `sidebar-ctrl-btn ${state.captionMode !== "off" ? "sidebar-ctrl-btn--active" : ""}`,
4548
5255
  onClick: () => actions.cycleCaptionMode(),
4549
5256
  title: state.captionMode === "off" ? "Captions: Off" : state.captionMode === "standard" ? "Captions: Standard" : "Captions: Social",
4550
5257
  "aria-label": "Cycle caption style",
4551
- 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" }) })
4552
5259
  }
4553
5260
  ),
4554
- actions.toggleFullscreen && /* @__PURE__ */ jsx22(
5261
+ actions.toggleFullscreen && /* @__PURE__ */ jsx23(
4555
5262
  "button",
4556
5263
  {
4557
5264
  className: `sidebar-ctrl-btn ${state.isFullscreen ? "sidebar-ctrl-btn--active" : ""}`,
4558
5265
  onClick: actions.toggleFullscreen,
4559
5266
  title: state.isFullscreen ? "Exit fullscreen (F)" : "Fullscreen (F)",
4560
5267
  "aria-label": state.isFullscreen ? "Exit fullscreen" : "Enter fullscreen",
4561
- 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" }) })
4562
5269
  }
4563
5270
  )
4564
5271
  ] });
4565
5272
  }
4566
5273
 
4567
5274
  // src/DocPlayerWithSidebar.tsx
4568
- import { useRef as useRef8, useState as useState8, useCallback as useCallback7, useEffect as useEffect9 } from "react";
4569
- 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";
4570
5277
  var DEFAULT_STATE = {
4571
5278
  isPlaying: false,
4572
5279
  currentTime: 0,
@@ -4588,6 +5295,7 @@ function DocPlayerWithSidebar({
4588
5295
  onEnded,
4589
5296
  onTimeUpdate,
4590
5297
  audioController,
5298
+ animationsEnabled = true,
4591
5299
  muted,
4592
5300
  captionsEnabled,
4593
5301
  isFullscreen,
@@ -4596,11 +5304,11 @@ function DocPlayerWithSidebar({
4596
5304
  onPlayingChange,
4597
5305
  theme
4598
5306
  }) {
4599
- const stateRef = useRef8(DEFAULT_STATE);
4600
- const actionsRef = useRef8(null);
4601
- const wasPlayingRef = useRef8(false);
4602
- const [, setTick] = useState8(0);
4603
- 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(
4604
5312
  (state) => {
4605
5313
  stateRef.current = state;
4606
5314
  if (onPlayingChange && state.isPlaying !== wasPlayingRef.current) {
@@ -4610,7 +5318,7 @@ function DocPlayerWithSidebar({
4610
5318
  },
4611
5319
  [onPlayingChange]
4612
5320
  );
4613
- const handleControlsReady = useCallback7(
5321
+ const handleControlsReady = useCallback8(
4614
5322
  (controls) => {
4615
5323
  const isFirst = !actionsRef.current;
4616
5324
  actionsRef.current = controls;
@@ -4618,14 +5326,14 @@ function DocPlayerWithSidebar({
4618
5326
  },
4619
5327
  []
4620
5328
  );
4621
- useEffect9(() => {
5329
+ useEffect11(() => {
4622
5330
  const interval = setInterval(() => {
4623
5331
  setTick((t) => t + 1);
4624
5332
  }, 250);
4625
5333
  return () => clearInterval(interval);
4626
5334
  }, []);
4627
- return /* @__PURE__ */ jsxs16("div", { className: "doc-player-sidebar-layout", children: [
4628
- /* @__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(
4629
5337
  DocPlayer,
4630
5338
  {
4631
5339
  doc,
@@ -4635,6 +5343,7 @@ function DocPlayerWithSidebar({
4635
5343
  onEnded,
4636
5344
  onTimeUpdate,
4637
5345
  audioController,
5346
+ animationsEnabled,
4638
5347
  muted,
4639
5348
  captionsEnabled,
4640
5349
  showControls: isFullscreen,
@@ -4646,7 +5355,7 @@ function DocPlayerWithSidebar({
4646
5355
  forceViewport
4647
5356
  }
4648
5357
  ) }),
4649
- actionsRef.current && /* @__PURE__ */ jsx23(DocControlsSidebar, { state: stateRef.current, actions: actionsRef.current })
5358
+ actionsRef.current && /* @__PURE__ */ jsx24(DocControlsSidebar, { state: stateRef.current, actions: actionsRef.current })
4650
5359
  ] });
4651
5360
  }
4652
5361
 
@@ -4677,18 +5386,18 @@ import {
4677
5386
  arrayItemKind
4678
5387
  } from "@bendyline/squisq/jsonForm";
4679
5388
  import { parseMarkdown as parseMarkdown3 } from "@bendyline/squisq/markdown";
4680
- 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";
4681
5390
  function TextViewer({ value }) {
4682
5391
  if (value === void 0 || value === null || value === "") {
4683
- return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-empty", children: "\u2014" });
5392
+ return /* @__PURE__ */ jsx25("span", { className: "squisq-jv-empty", children: "\u2014" });
4684
5393
  }
4685
- return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-value", children: String(value) });
5394
+ return /* @__PURE__ */ jsx25("span", { className: "squisq-jv-value", children: String(value) });
4686
5395
  }
4687
5396
  function MultilineViewer({ value }) {
4688
5397
  if (value === void 0 || value === null || value === "") {
4689
- return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-empty", children: "\u2014" });
5398
+ return /* @__PURE__ */ jsx25("span", { className: "squisq-jv-empty", children: "\u2014" });
4690
5399
  }
4691
- 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) });
4692
5401
  }
4693
5402
  function RichTextViewer({ value }) {
4694
5403
  const nodes = useMemo11(() => {
@@ -4700,39 +5409,39 @@ function RichTextViewer({ value }) {
4700
5409
  return null;
4701
5410
  }
4702
5411
  }, [value]);
4703
- if (!nodes) return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-empty", children: "\u2014" });
4704
- 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 }) });
4705
5414
  }
4706
5415
  function NumberViewer({ value }) {
4707
5416
  if (value === void 0 || value === null) {
4708
- return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-empty", children: "\u2014" });
5417
+ return /* @__PURE__ */ jsx25("span", { className: "squisq-jv-empty", children: "\u2014" });
4709
5418
  }
4710
- return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-value", children: String(value) });
5419
+ return /* @__PURE__ */ jsx25("span", { className: "squisq-jv-value", children: String(value) });
4711
5420
  }
4712
5421
  function BooleanViewer({ value }) {
4713
5422
  const on = Boolean(value);
4714
- 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" });
4715
5424
  }
4716
5425
  function EnumViewer({ value, schema }) {
4717
5426
  if (value === void 0 || value === null || value === "") {
4718
- return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-empty", children: "\u2014" });
5427
+ return /* @__PURE__ */ jsx25("span", { className: "squisq-jv-empty", children: "\u2014" });
4719
5428
  }
4720
5429
  const labels = schema.squisq?.enumLabels;
4721
5430
  const display = labels && typeof value === "string" ? labels[value] ?? value : String(value);
4722
- return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-value", children: display });
5431
+ return /* @__PURE__ */ jsx25("span", { className: "squisq-jv-value", children: display });
4723
5432
  }
4724
5433
  function ColorViewer({ value }) {
4725
5434
  if (typeof value !== "string" || value === "") {
4726
- return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-empty", children: "\u2014" });
5435
+ return /* @__PURE__ */ jsx25("span", { className: "squisq-jv-empty", children: "\u2014" });
4727
5436
  }
4728
- return /* @__PURE__ */ jsxs17("span", { className: "squisq-jv-color", children: [
4729
- /* @__PURE__ */ jsx24("span", { className: "squisq-jv-color__swatch", style: { background: value }, "aria-hidden": true }),
4730
- /* @__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 })
4731
5440
  ] });
4732
5441
  }
4733
5442
  function DateViewer({ value, schema }) {
4734
5443
  if (typeof value !== "string" || value === "") {
4735
- return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-empty", children: "\u2014" });
5444
+ return /* @__PURE__ */ jsx25("span", { className: "squisq-jv-empty", children: "\u2014" });
4736
5445
  }
4737
5446
  const fmt = schema.format;
4738
5447
  let display = value;
@@ -4749,31 +5458,31 @@ function DateViewer({ value, schema }) {
4749
5458
  }
4750
5459
  } catch {
4751
5460
  }
4752
- return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-value", children: display });
5461
+ return /* @__PURE__ */ jsx25("span", { className: "squisq-jv-value", children: display });
4753
5462
  }
4754
5463
  function ChipBinViewer({ value, schema }) {
4755
5464
  if (!Array.isArray(value) || value.length === 0) {
4756
- return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-empty", children: "\u2014" });
5465
+ return /* @__PURE__ */ jsx25("span", { className: "squisq-jv-empty", children: "\u2014" });
4757
5466
  }
4758
5467
  const itemSchema = Array.isArray(schema.items) ? schema.items[0] : schema.items;
4759
5468
  const labels = itemSchema?.squisq?.enumLabels;
4760
- 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) => {
4761
5470
  const label = labels && typeof item === "string" ? labels[item] ?? String(item) : String(item);
4762
- return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-chip", children: label }, i);
5471
+ return /* @__PURE__ */ jsx25("span", { className: "squisq-jv-chip", children: label }, i);
4763
5472
  }) });
4764
5473
  }
4765
5474
  function CardStackViewer(props) {
4766
5475
  const { value, schema, rootSchema, rootData, pointer, density } = props;
4767
5476
  if (!Array.isArray(value) || value.length === 0) {
4768
- return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-empty", children: "No items" });
5477
+ return /* @__PURE__ */ jsx25("span", { className: "squisq-jv-empty", children: "No items" });
4769
5478
  }
4770
5479
  const itemSchema = (Array.isArray(schema.items) ? schema.items[0] : schema.items) ?? {};
4771
5480
  const itemLabel = itemSchema.squisq?.itemLabel;
4772
- 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) => {
4773
5482
  const title = resolveItemTitle(itemLabel, item, i);
4774
- return /* @__PURE__ */ jsxs17("div", { className: "squisq-jv-card", children: [
4775
- title ? /* @__PURE__ */ jsx24("h4", { className: "squisq-jv-card__title", children: title }) : null,
4776
- /* @__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(
4777
5486
  RenderNode,
4778
5487
  {
4779
5488
  value: item,
@@ -4804,16 +5513,16 @@ function GroupViewer(props) {
4804
5513
  const help = schema.squisq?.help ?? schema.description;
4805
5514
  const obj = (value && typeof value === "object" ? value : {}) ?? {};
4806
5515
  const propEntries = Object.entries(schema.properties ?? {});
4807
- return /* @__PURE__ */ jsxs17("section", { className: "squisq-jv-group", children: [
4808
- title ? /* @__PURE__ */ jsx24("h3", { className: "squisq-jv-group__title", children: title }) : null,
4809
- help ? /* @__PURE__ */ jsx24("p", { className: "squisq-jv-group__help", children: help }) : null,
4810
- 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(
4811
5520
  RowOrSection,
4812
5521
  {
4813
5522
  label: propSchema.squisq?.label ?? propSchema.title ?? key,
4814
5523
  help: propSchema.squisq?.help ?? propSchema.description,
4815
5524
  kindHint: propSchema,
4816
- children: /* @__PURE__ */ jsx24(
5525
+ children: /* @__PURE__ */ jsx25(
4817
5526
  RenderNode,
4818
5527
  {
4819
5528
  value: obj[key],
@@ -4837,11 +5546,11 @@ function RowOrSection({
4837
5546
  }) {
4838
5547
  const composite = isCompositeKind(kindHint);
4839
5548
  if (composite) {
4840
- return /* @__PURE__ */ jsx24(Fragment5, { children });
5549
+ return /* @__PURE__ */ jsx25(Fragment5, { children });
4841
5550
  }
4842
- return /* @__PURE__ */ jsxs17("div", { className: "squisq-jv-row", children: [
4843
- /* @__PURE__ */ jsx24("div", { className: "squisq-jv-label", title: help, children: label }),
4844
- /* @__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 })
4845
5554
  ] });
4846
5555
  }
4847
5556
  function isCompositeKind(schema) {
@@ -4862,11 +5571,11 @@ function TabsViewer(props) {
4862
5571
  const matchedIndex = pickMatchingBranch(branches, value);
4863
5572
  const branch = branches[matchedIndex];
4864
5573
  if (!branch) {
4865
- return /* @__PURE__ */ jsx24(TextViewer, { ...props });
5574
+ return /* @__PURE__ */ jsx25(TextViewer, { ...props });
4866
5575
  }
4867
- return /* @__PURE__ */ jsxs17("div", { className: "squisq-jv-tabs", children: [
4868
- /* @__PURE__ */ jsx24("span", { className: "squisq-jv-tabs__discriminator", children: branch.squisq?.label ?? branch.title ?? `Option ${matchedIndex + 1}` }),
4869
- /* @__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(
4870
5579
  RenderNode,
4871
5580
  {
4872
5581
  value,
@@ -4930,7 +5639,7 @@ var VIEWERS = {
4930
5639
  };
4931
5640
 
4932
5641
  // src/jsonView/RenderNode.tsx
4933
- import { jsx as jsx25 } from "react/jsx-runtime";
5642
+ import { jsx as jsx26 } from "react/jsx-runtime";
4934
5643
  function RenderNode(props) {
4935
5644
  const resolved = resolveRef(props.schema, props.rootSchema) ?? props.schema;
4936
5645
  if (resolveFlag(resolved.squisq?.hidden, props.rootData)) return null;
@@ -4946,18 +5655,18 @@ function RenderNode(props) {
4946
5655
  };
4947
5656
  if (kind === "group" || kind === "card") {
4948
5657
  const Group = Viewer;
4949
- return /* @__PURE__ */ jsx25(Group, { ...viewerProps, suppressTitle: props.suppressTopGroupTitle });
5658
+ return /* @__PURE__ */ jsx26(Group, { ...viewerProps, suppressTitle: props.suppressTopGroupTitle });
4950
5659
  }
4951
- return /* @__PURE__ */ jsx25(Viewer, { ...viewerProps });
5660
+ return /* @__PURE__ */ jsx26(Viewer, { ...viewerProps });
4952
5661
  }
4953
5662
 
4954
5663
  // src/jsonView/JsonView.tsx
4955
- import { jsx as jsx26 } from "react/jsx-runtime";
5664
+ import { jsx as jsx27 } from "react/jsx-runtime";
4956
5665
  function JsonView(props) {
4957
5666
  const { schema, value, theme, surface, density = "comfortable", className } = props;
4958
5667
  const { style } = useJsonViewTokens(theme, surface);
4959
5668
  const cls = "squisq-json-view" + (density === "compact" ? " squisq-json-view--compact" : "") + (className ? ` ${className}` : "");
4960
- return /* @__PURE__ */ jsx26("div", { className: cls, style, children: /* @__PURE__ */ jsx26(
5669
+ return /* @__PURE__ */ jsx27("div", { className: cls, style, children: /* @__PURE__ */ jsx27(
4961
5670
  RenderNode,
4962
5671
  {
4963
5672
  value,
@@ -4993,7 +5702,7 @@ export {
4993
5702
  SocialCaptionOverlay,
4994
5703
  TableLayer,
4995
5704
  TextLayer,
4996
- VIEWPORT,
5705
+ TreeLayer,
4997
5706
  VideoLayer,
4998
5707
  formatTime,
4999
5708
  getAnimationStyle,