@bendyline/squisq-react 1.4.2 → 2.0.1

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 (50) hide show
  1. package/README.md +30 -3
  2. package/dist/index.d.ts +177 -28
  3. package/dist/index.js +1330 -611
  4. package/dist/index.js.map +1 -1
  5. package/dist/squisq-player.css +1 -1
  6. package/dist/squisq-player.css.map +1 -1
  7. package/dist/squisq-player.global.js +57 -37
  8. package/dist/squisq-player.global.js.map +1 -1
  9. package/dist/standalone-source.js +1 -1
  10. package/dist/styles/index.css +28 -0
  11. package/package.json +2 -2
  12. package/src/BlockRenderer.tsx +54 -17
  13. package/src/DocControlsSlideshow.tsx +222 -5
  14. package/src/DocPlayer.tsx +367 -183
  15. package/src/DocPlayerWithSidebar.tsx +4 -0
  16. package/src/DocProgressBar.tsx +40 -1
  17. package/src/LinearDocView.tsx +138 -62
  18. package/src/MarkdownRenderer.tsx +40 -97
  19. package/src/MediaClipLayer.tsx +12 -2
  20. package/src/__tests__/BlockRenderer.test.tsx +138 -8
  21. package/src/__tests__/DocControlsSlideshow.test.tsx +94 -1
  22. package/src/__tests__/DocPlayer.test.tsx +505 -0
  23. package/src/__tests__/DocProgressBar.test.tsx +28 -2
  24. package/src/__tests__/LinearDocView.test.tsx +104 -11
  25. package/src/__tests__/MapLayer.test.tsx +63 -0
  26. package/src/__tests__/MarkdownRenderer.test.tsx +16 -5
  27. package/src/__tests__/MediaClipLayer.test.tsx +70 -0
  28. package/src/__tests__/MediaContext.test.tsx +51 -0
  29. package/src/__tests__/PathLayer.test.tsx +12 -1
  30. package/src/__tests__/VideoLayer.test.tsx +94 -0
  31. package/src/__tests__/fillStyle.test.tsx +50 -2
  32. package/src/__tests__/standaloneEntry.test.tsx +103 -0
  33. package/src/__tests__/useAudioSync.test.ts +49 -0
  34. package/src/__tests__/useDocPlayback.transition.test.ts +48 -5
  35. package/src/__tests__/useViewportOrientation.test.ts +22 -0
  36. package/src/hooks/MediaContext.tsx +12 -3
  37. package/src/hooks/useAudioSync.ts +61 -12
  38. package/src/hooks/useDocPlayback.ts +40 -12
  39. package/src/hooks/useViewportOrientation.ts +2 -4
  40. package/src/index.ts +5 -2
  41. package/src/layers/ImageLayer.tsx +106 -1
  42. package/src/layers/MapLayer.tsx +7 -6
  43. package/src/layers/PathLayer.tsx +20 -11
  44. package/src/layers/ShapeLayer.tsx +33 -9
  45. package/src/layers/TextLayer.tsx +4 -3
  46. package/src/layers/TreeLayer.tsx +167 -0
  47. package/src/layers/VideoLayer.tsx +20 -6
  48. package/src/standalone-entry.tsx +91 -14
  49. package/src/styles/doc-animations.css +36 -0
  50. package/src/types.ts +13 -13
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // src/DocPlayer.tsx
2
- import { Fragment as Fragment3, useRef as useRef7, useState as useState7, useEffect as useEffect8, useCallback as useCallback6, useMemo as useMemo9 } from "react";
2
+ import { Fragment as Fragment3, useId as useId7, useRef as useRef9, useState as useState9, useEffect as useEffect10, useCallback as useCallback7, useMemo as useMemo9 } from "react";
3
3
  import {
4
- isTemplateBlock as isTemplateBlock2,
4
+ isTemplateBlock as isTemplateBlock3,
5
5
  getCaptionAtTime as getCaptionAtTime2,
6
6
  resolveMediaSchedule,
7
7
  getDocPlaybackDuration
@@ -32,9 +32,15 @@ function useMediaUrl(relativePath, basePath) {
32
32
  return;
33
33
  }
34
34
  let cancelled = false;
35
- provider.resolveUrl(safePath).then((resolved) => {
36
- if (!cancelled) setUrl(resolved);
37
- });
35
+ setUrl(fallback);
36
+ provider.resolveUrl(safePath).then(
37
+ (resolved) => {
38
+ if (!cancelled) setUrl(resolved);
39
+ },
40
+ () => {
41
+ if (!cancelled) setUrl(fallback);
42
+ }
43
+ );
38
44
  return () => {
39
45
  cancelled = true;
40
46
  };
@@ -63,7 +69,8 @@ function MediaClipLayer({
63
69
  currentTime,
64
70
  isPlaying,
65
71
  basePath,
66
- renderMode = false
72
+ renderMode = false,
73
+ muted = false
67
74
  }) {
68
75
  const { renderClips, activeIds } = useMediaSchedule(schedule, currentTime);
69
76
  if (renderClips.length === 0) return null;
@@ -75,7 +82,8 @@ function MediaClipLayer({
75
82
  currentTime,
76
83
  isPlaying,
77
84
  basePath,
78
- renderMode
85
+ renderMode,
86
+ muted
79
87
  },
80
88
  clip.id
81
89
  )) });
@@ -86,7 +94,8 @@ function MediaClipElement({
86
94
  currentTime,
87
95
  isPlaying,
88
96
  basePath,
89
- renderMode
97
+ renderMode,
98
+ muted
90
99
  }) {
91
100
  const ref = useRef(null);
92
101
  const src = useMediaUrl(clip.src, basePath);
@@ -111,7 +120,7 @@ function MediaClipElement({
111
120
  } else {
112
121
  el.pause();
113
122
  }
114
- }, [active, currentTime, isPlaying, renderMode, clip.sourceIn, clip.absoluteStart]);
123
+ }, [active, currentTime, isPlaying, renderMode, clip.sourceIn, clip.absoluteStart, src]);
115
124
  const isVideo = clip.kind === "video";
116
125
  const common = {
117
126
  ref,
@@ -141,13 +150,21 @@ function MediaClipElement({
141
150
  }
142
151
  );
143
152
  }
144
- return /* @__PURE__ */ jsx("audio", { ...common, muted: renderMode, style: { position: "absolute", width: 0, height: 0 } });
153
+ return /* @__PURE__ */ jsx(
154
+ "audio",
155
+ {
156
+ ...common,
157
+ muted: renderMode || muted,
158
+ style: { position: "absolute", width: 0, height: 0 }
159
+ }
160
+ );
145
161
  }
146
162
 
147
163
  // src/DocPlayer.tsx
148
164
  import { applySurface as applySurface2 } from "@bendyline/squisq/schemas";
149
165
 
150
166
  // src/BlockRenderer.tsx
167
+ import { useId as useId5 } from "react";
151
168
  import { resolveTransitionDuration } from "@bendyline/squisq/schemas";
152
169
 
153
170
  // src/layers/ImageLayer.tsx
@@ -190,7 +207,13 @@ function getAnchorOffset(anchor, width, height) {
190
207
 
191
208
  // src/layers/ImageLayer.tsx
192
209
  import { jsx as jsx2 } from "react/jsx-runtime";
193
- function ImageLayer({ layer, basePath, viewport, blockTime }) {
210
+ function ImageLayer({
211
+ layer,
212
+ basePath,
213
+ viewport,
214
+ blockTime,
215
+ animationsEnabled = true
216
+ }) {
194
217
  const { content, position, animation } = layer;
195
218
  const x = resolveValue(position.x, viewport.width);
196
219
  const y = resolveValue(position.y, viewport.height);
@@ -205,6 +228,50 @@ function ImageLayer({ layer, basePath, viewport, blockTime }) {
205
228
  const preserveAspectRatio = getPreserveAspectRatio(content.fit);
206
229
  const isCover = content.fit === "cover";
207
230
  const isSpatialAnim = animation && SPATIAL_ANIMATION_TYPES.has(animation.type);
231
+ const usesPortraitPan = isCover && shouldUsePortraitPan(viewport, width, height, animationsEnabled, animation);
232
+ if (usesPortraitPan) {
233
+ const panClass = getPortraitPanClass(animation);
234
+ const panStyle = getPortraitPanStyle(isSpatialAnim ? animation : void 0);
235
+ const containerAnim = isSpatialAnim ? { className: "", style: {} } : animStyle;
236
+ return /* @__PURE__ */ jsx2(
237
+ "g",
238
+ {
239
+ className: `block-layer block-layer--image ${containerAnim.className}`,
240
+ style: containerAnim.style,
241
+ "data-layer-id": layer.id,
242
+ "data-image-framing": "portrait-pan",
243
+ children: /* @__PURE__ */ jsx2("foreignObject", { x: finalX, y: finalY, width, height, children: /* @__PURE__ */ jsx2(
244
+ "div",
245
+ {
246
+ style: {
247
+ width: `${width}px`,
248
+ height: `${height}px`,
249
+ overflow: "hidden"
250
+ },
251
+ children: /* @__PURE__ */ jsx2(
252
+ "img",
253
+ {
254
+ src,
255
+ alt: content.alt || "",
256
+ className: panClass,
257
+ style: {
258
+ width: `${width}px`,
259
+ height: `${height}px`,
260
+ objectFit: "cover",
261
+ objectPosition: "center",
262
+ display: "block",
263
+ pointerEvents: "none",
264
+ ...filter ? { filter } : {},
265
+ ...content.blur && content.blur > 0 ? { transform: "scale(1.06)" } : {},
266
+ ...panStyle
267
+ }
268
+ }
269
+ )
270
+ }
271
+ ) })
272
+ }
273
+ );
274
+ }
208
275
  if (isCover && isSpatialAnim && animation) {
209
276
  const kbAnim = remapToKenBurns(animation);
210
277
  const kbStyle = getAnimationStyle(kbAnim, blockTime);
@@ -300,6 +367,23 @@ function getPreserveAspectRatio(fit) {
300
367
  }
301
368
  }
302
369
  var SPATIAL_ANIMATION_TYPES = /* @__PURE__ */ new Set(["panLeft", "panRight", "slowZoom", "zoomIn", "zoomOut"]);
370
+ var PORTRAIT_ASPECT_CUTOFF = 0.83;
371
+ function shouldUsePortraitPan(viewport, layerWidth, layerHeight, animationsEnabled, animation) {
372
+ if (!animationsEnabled || animation?.type === "none") return false;
373
+ if (viewport.width / viewport.height >= PORTRAIT_ASPECT_CUTOFF) return false;
374
+ return layerWidth >= viewport.width * 0.7 && layerHeight >= viewport.height * 0.7;
375
+ }
376
+ function getPortraitPanClass(animation) {
377
+ const pansBack = animation?.type === "panRight" || animation?.type === "slowZoom" && animation.panDirection === "right";
378
+ return pansBack ? "squisq-image--portrait-pan-left" : "squisq-image--portrait-pan-right";
379
+ }
380
+ function getPortraitPanStyle(animation) {
381
+ return {
382
+ "--portrait-pan-duration": `${animation?.duration ?? 12}s`,
383
+ "--portrait-pan-delay": `${animation?.delay ?? 0}s`,
384
+ "--portrait-pan-easing": animation?.easing ?? "ease-in-out"
385
+ };
386
+ }
303
387
  function remapToKenBurns(anim) {
304
388
  switch (anim.type) {
305
389
  case "panLeft":
@@ -316,7 +400,7 @@ function remapToKenBurns(anim) {
316
400
  }
317
401
 
318
402
  // src/layers/TextLayer.tsx
319
- import { useMemo as useMemo3 } from "react";
403
+ import { useId, useMemo as useMemo3 } from "react";
320
404
  import { DEFAULT_DOC_FONT } from "@bendyline/squisq/schemas";
321
405
  import {
322
406
  parseHtmlToNodes,
@@ -505,6 +589,7 @@ function IconTextLayer({ layer, viewport, blockTime }) {
505
589
  ) });
506
590
  }
507
591
  function PlainTextLayer({ layer, viewport, blockTime }) {
592
+ const defsId = `${useId().replace(/:/g, "")}-${layer.id}`;
508
593
  const { content, position, animation } = layer;
509
594
  const { text, style } = content;
510
595
  const rawX = resolveValue(position.x, viewport.width);
@@ -538,13 +623,13 @@ function PlainTextLayer({ layer, viewport, blockTime }) {
538
623
  fill: style.color,
539
624
  ...animStyle.style
540
625
  };
541
- const filterId = style.shadow ? `shadow-${layer.id}` : void 0;
626
+ const filterId = style.shadow ? `shadow-${defsId}` : void 0;
542
627
  return /* @__PURE__ */ jsxs2("g", { className: `block-layer block-layer--text ${animStyle.className}`, "data-layer-id": layer.id, children: [
543
628
  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
629
  /* @__PURE__ */ jsx4(
545
630
  TextBox,
546
631
  {
547
- layerId: layer.id,
632
+ layerId: defsId,
548
633
  style,
549
634
  box: boxWidth != null && boxHeight != null ? {
550
635
  x: rawX - anchorAxis(anchor, boxWidth, "x"),
@@ -746,9 +831,12 @@ function wrapText(text, fontSize, maxWidth) {
746
831
  }
747
832
 
748
833
  // src/layers/ShapeLayer.tsx
834
+ import { useId as useId2 } from "react";
749
835
  import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
836
+ var FULL_BLEED_OVERSCAN = 1;
750
837
  function ShapeLayer({ layer, viewport, blockTime }) {
751
838
  const { content, position, animation } = layer;
839
+ const defsId = `${useId2().replace(/:/g, "")}-${layer.id}`;
752
840
  const rawX = resolveValue(position.x, viewport.width);
753
841
  const rawY = resolveValue(position.y, viewport.height);
754
842
  const width = position.width ? resolveValue(position.width, viewport.width) : 100;
@@ -756,6 +844,12 @@ function ShapeLayer({ layer, viewport, blockTime }) {
756
844
  const anchorOffset = getAnchorOffset(position.anchor, width, height);
757
845
  const x = rawX + anchorOffset.x;
758
846
  const y = rawY + anchorOffset.y;
847
+ const isUnborderedFullBleedRect = content.shape === "rect" && x === 0 && y === 0 && width === viewport.width && height === viewport.height && !content.stroke && !content.borderRadius;
848
+ const overscan = isUnborderedFullBleedRect ? FULL_BLEED_OVERSCAN : 0;
849
+ const paintX = x - overscan;
850
+ const paintY = y - overscan;
851
+ const paintWidth = width + overscan * 2;
852
+ const paintHeight = height + overscan * 2;
759
853
  const animStyle = getAnimationStyle(animation, blockTime);
760
854
  const fill = content.fill || "none";
761
855
  const isCSSGradient = typeof fill === "string" && fill.includes("gradient(");
@@ -766,12 +860,12 @@ function ShapeLayer({ layer, viewport, blockTime }) {
766
860
  className: `block-layer block-layer--shape ${animStyle.className}`,
767
861
  style: animStyle.style,
768
862
  "data-layer-id": layer.id,
769
- children: /* @__PURE__ */ jsx5("foreignObject", { x, y, width, height, children: /* @__PURE__ */ jsx5(
863
+ children: /* @__PURE__ */ jsx5("foreignObject", { x: paintX, y: paintY, width: paintWidth, height: paintHeight, children: /* @__PURE__ */ jsx5(
770
864
  "div",
771
865
  {
772
866
  style: {
773
- width: `${width}px`,
774
- height: `${height}px`,
867
+ width: `${paintWidth}px`,
868
+ height: `${paintHeight}px`,
775
869
  background: fill,
776
870
  borderRadius: content.borderRadius ? `${content.borderRadius}px` : void 0,
777
871
  pointerEvents: "none"
@@ -782,12 +876,12 @@ function ShapeLayer({ layer, viewport, blockTime }) {
782
876
  );
783
877
  }
784
878
  const { fill: fillValue, def: fillDef } = resolveFill(
785
- layer.id,
879
+ defsId,
786
880
  fill,
787
881
  content.gradient,
788
882
  content.pattern
789
883
  );
790
- const { filterAttr, def: filterDef } = resolveShapeFilter(layer.id, content.filter);
884
+ const { filterAttr, def: filterDef } = resolveShapeFilter(defsId, content.filter);
791
885
  const dash = borderDashArray(content.borderStyle, content.strokeWidth);
792
886
  const shapeProps = {
793
887
  fill: fillValue,
@@ -811,10 +905,10 @@ function ShapeLayer({ layer, viewport, blockTime }) {
811
905
  content.shape === "rect" && /* @__PURE__ */ jsx5(
812
906
  "rect",
813
907
  {
814
- x,
815
- y,
816
- width,
817
- height,
908
+ x: paintX,
909
+ y: paintY,
910
+ width: paintWidth,
911
+ height: paintHeight,
818
912
  rx: content.borderRadius,
819
913
  ry: content.borderRadius,
820
914
  ...shapeProps
@@ -847,6 +941,7 @@ function ShapeLayer({ layer, viewport, blockTime }) {
847
941
  }
848
942
 
849
943
  // src/layers/PathLayer.tsx
944
+ import { useId as useId3 } from "react";
850
945
  import { markerPath, shapePath } from "@bendyline/squisq/doc";
851
946
  import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
852
947
  function effectivePath(layer, viewport) {
@@ -860,23 +955,28 @@ function effectivePath(layer, viewport) {
860
955
  const derived = shapePath(content.shapeKind, rawX + anchor.x, rawY + anchor.y, w, h);
861
956
  return derived ?? content.d;
862
957
  }
863
- function effectiveMarker(explicit, arrow, end) {
958
+ function readLegacyArrow(content) {
959
+ return content.arrow;
960
+ }
961
+ function effectiveMarker(explicit, legacyArrow, end) {
864
962
  if (explicit) return explicit;
865
- const wants = arrow === "both" || arrow === end;
963
+ const wants = legacyArrow === "both" || legacyArrow === end;
866
964
  return wants ? "arrow" : "none";
867
965
  }
868
966
  function PathLayer({ layer, viewport, blockTime }) {
869
967
  const { content, animation, id } = layer;
968
+ const defsId = `${useId3().replace(/:/g, "")}-${id}`;
870
969
  const d = effectivePath(layer, viewport);
871
970
  const stroke = content.stroke ?? "#1e293b";
872
971
  const strokeWidth = content.strokeWidth ?? 2;
873
- const { fill, def: fillDef } = resolveFill(id, content.fill ?? "none", content.gradient);
972
+ const { fill, def: fillDef } = resolveFill(defsId, content.fill ?? "none", content.gradient);
874
973
  const dash = content.borderStyle ? borderDashArray(content.borderStyle, strokeWidth) : content.dasharray;
875
974
  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");
975
+ const startId = `marker-start-${defsId}`;
976
+ const endId = `marker-end-${defsId}`;
977
+ const legacyArrow = readLegacyArrow(content);
978
+ const start = markerPath(effectiveMarker(content.startMarker, legacyArrow, "start"), "start");
979
+ const end = markerPath(effectiveMarker(content.endMarker, legacyArrow, "end"), "end");
880
980
  return /* @__PURE__ */ jsxs4(
881
981
  "g",
882
982
  {
@@ -938,7 +1038,7 @@ function MarkerDef({
938
1038
  }
939
1039
 
940
1040
  // src/layers/MapLayer.tsx
941
- import { useState as useState2, useEffect as useEffect3 } from "react";
1041
+ import { useId as useId4, useState as useState2, useEffect as useEffect3 } from "react";
942
1042
 
943
1043
  // src/utils/mapTileUtils.ts
944
1044
  var TILE_PROVIDERS = {
@@ -1124,6 +1224,7 @@ function drawAttribution(ctx, text, width, height) {
1124
1224
  import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
1125
1225
  function MapLayer({ layer, basePath, viewport, blockTime }) {
1126
1226
  const { content, position, animation } = layer;
1227
+ const clipId = `map-clip-${useId4().replace(/:/g, "")}-${layer.id}`;
1127
1228
  const [mapImageUrl, setMapImageUrl] = useState2(null);
1128
1229
  const [isLoading, setIsLoading] = useState2(true);
1129
1230
  const [error, setError] = useState2(null);
@@ -1168,11 +1269,12 @@ function MapLayer({ layer, basePath, viewport, blockTime }) {
1168
1269
  cancelled = true;
1169
1270
  };
1170
1271
  }, [
1171
- content.center.lat,
1172
- content.center.lng,
1272
+ content.center,
1173
1273
  content.zoom,
1174
1274
  content.style,
1175
1275
  content.staticSrc,
1276
+ content.markers,
1277
+ content.showAttribution,
1176
1278
  width,
1177
1279
  height,
1178
1280
  basePath
@@ -1237,8 +1339,8 @@ function MapLayer({ layer, basePath, viewport, blockTime }) {
1237
1339
  style: animStyle.style,
1238
1340
  "data-layer-id": layer.id,
1239
1341
  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(
1342
+ /* @__PURE__ */ jsx7("defs", { children: /* @__PURE__ */ jsx7("clipPath", { id: clipId, children: /* @__PURE__ */ jsx7("rect", { x: finalX, y: finalY, width, height }) }) }),
1343
+ /* @__PURE__ */ jsx7("g", { clipPath: `url(#${clipId})`, children: /* @__PURE__ */ jsx7(
1242
1344
  "image",
1243
1345
  {
1244
1346
  href: mapImageUrl,
@@ -1258,6 +1360,7 @@ function MapLayer({ layer, basePath, viewport, blockTime }) {
1258
1360
  // src/layers/VideoLayer.tsx
1259
1361
  import { useRef as useRef2, useEffect as useEffect4 } from "react";
1260
1362
  import { jsx as jsx8 } from "react/jsx-runtime";
1363
+ var VIDEO_SYNC_DRIFT_SECONDS = 0.2;
1261
1364
  function VideoLayer({ layer, basePath, viewport, blockTime, isPlaying }) {
1262
1365
  const { content, position } = layer;
1263
1366
  const videoRef = useRef2(null);
@@ -1297,16 +1400,22 @@ function VideoLayer({ layer, basePath, viewport, blockTime, isPlaying }) {
1297
1400
  video.removeEventListener("timeupdate", handleTimeUpdate);
1298
1401
  video.pause();
1299
1402
  };
1300
- }, [content.src, content.clipStart, content.clipEnd]);
1403
+ }, [src, content.clipStart, content.clipEnd]);
1301
1404
  useEffect4(() => {
1302
1405
  const video = videoRef.current;
1303
1406
  if (!video || !hasStartedRef.current) return;
1407
+ const targetTime = gated ? content.clipStart : Math.min(content.clipEnd, content.clipStart + Math.max(0, blockTime - startAt));
1408
+ if (Math.abs(video.currentTime - targetTime) > VIDEO_SYNC_DRIFT_SECONDS) {
1409
+ video.currentTime = targetTime;
1410
+ }
1304
1411
  if (gated) {
1305
1412
  video.pause();
1306
- video.currentTime = content.clipStart;
1307
1413
  return;
1308
1414
  }
1309
- if (video.currentTime >= content.clipEnd) return;
1415
+ if (targetTime >= content.clipEnd) {
1416
+ video.pause();
1417
+ return;
1418
+ }
1310
1419
  if (isPlaying) {
1311
1420
  const playPromise = video.play();
1312
1421
  if (playPromise) {
@@ -1316,7 +1425,7 @@ function VideoLayer({ layer, basePath, viewport, blockTime, isPlaying }) {
1316
1425
  } else {
1317
1426
  video.pause();
1318
1427
  }
1319
- }, [isPlaying, gated, content.clipStart, content.clipEnd]);
1428
+ }, [isPlaying, gated, blockTime, startAt, src, content.clipStart, content.clipEnd]);
1320
1429
  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
1430
  "video",
1322
1431
  {
@@ -1425,9 +1534,137 @@ function TableLayer({ layer, viewport, blockTime }) {
1425
1534
  ) });
1426
1535
  }
1427
1536
 
1428
- // src/BlockRenderer.tsx
1537
+ // src/layers/TreeLayer.tsx
1538
+ import { useState as useState3 } from "react";
1429
1539
  import { jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
1430
- var VIEWPORT = {
1540
+ function faClass(token, fallback) {
1541
+ const name = token && token.trim() ? token.trim() : fallback;
1542
+ const colon = name.indexOf(":");
1543
+ if (colon > 0) {
1544
+ const family = name.slice(0, colon).replace(/^fa-/, "");
1545
+ return `fa-${family} fa-${name.slice(colon + 1)}`;
1546
+ }
1547
+ return `fa-solid fa-${name}`;
1548
+ }
1549
+ function TreeLayer({ layer, viewport, blockTime }) {
1550
+ const { content, position, animation } = layer;
1551
+ const { items, style } = content;
1552
+ const x = resolveValue(position.x, viewport.width);
1553
+ const y = resolveValue(position.y, viewport.height);
1554
+ const width = position.width ? resolveValue(position.width, viewport.width) : viewport.width;
1555
+ const height = position.height ? resolveValue(position.height, viewport.height) : viewport.height;
1556
+ const offset = getAnchorOffset(position.anchor, width, height);
1557
+ const animStyle = animation ? getAnimationStyle(animation, blockTime) : {};
1558
+ return /* @__PURE__ */ jsx10(
1559
+ "foreignObject",
1560
+ {
1561
+ x: x + offset.x,
1562
+ y: y + offset.y,
1563
+ width,
1564
+ height,
1565
+ style: animStyle,
1566
+ children: /* @__PURE__ */ jsx10(
1567
+ "div",
1568
+ {
1569
+ ...{ xmlns: "http://www.w3.org/1999/xhtml" },
1570
+ className: "squisq-treelayer",
1571
+ style: {
1572
+ width: `${width}px`,
1573
+ height: `${height}px`,
1574
+ display: "flex",
1575
+ flexDirection: "column",
1576
+ justifyContent: "center",
1577
+ padding: "24px 32px",
1578
+ boxSizing: "border-box",
1579
+ fontFamily: style.fontFamily ?? "system-ui, sans-serif",
1580
+ fontSize: `${style.fontSize}px`,
1581
+ lineHeight: 1.7,
1582
+ overflow: "hidden"
1583
+ },
1584
+ children: /* @__PURE__ */ jsx10(TreeList, { items, depth: 0, style })
1585
+ }
1586
+ )
1587
+ }
1588
+ );
1589
+ }
1590
+ function TreeList({
1591
+ items,
1592
+ depth,
1593
+ style
1594
+ }) {
1595
+ return /* @__PURE__ */ jsx10(
1596
+ "ul",
1597
+ {
1598
+ style: {
1599
+ listStyle: "none",
1600
+ margin: 0,
1601
+ padding: 0,
1602
+ paddingLeft: depth === 0 ? 0 : `${style.indentPx}px`,
1603
+ borderLeft: depth === 0 ? "none" : `1px solid ${style.connectorColor}`
1604
+ },
1605
+ children: items.map((item) => /* @__PURE__ */ jsx10(TreeRow, { item, style }, item.id))
1606
+ }
1607
+ );
1608
+ }
1609
+ function TreeRow({
1610
+ item,
1611
+ style
1612
+ }) {
1613
+ const hasChildren = item.children.length > 0;
1614
+ const [collapsed, setCollapsed] = useState3(false);
1615
+ const isDir = item.isDir || hasChildren;
1616
+ const iconCls = isDir ? faClass(style.folderIcon, collapsed ? "folder" : "folder-open") : faClass(style.fileIcon, "file");
1617
+ return /* @__PURE__ */ jsxs7("li", { style: { position: "relative" }, children: [
1618
+ /* @__PURE__ */ jsxs7("div", { style: { display: "flex", alignItems: "baseline", gap: "8px", padding: "1px 0" }, children: [
1619
+ hasChildren ? /* @__PURE__ */ jsx10(
1620
+ "button",
1621
+ {
1622
+ type: "button",
1623
+ "aria-label": collapsed ? "Expand" : "Collapse",
1624
+ onClick: () => setCollapsed((c) => !c),
1625
+ style: {
1626
+ flex: "0 0 auto",
1627
+ width: "1em",
1628
+ border: "none",
1629
+ background: "transparent",
1630
+ cursor: "pointer",
1631
+ color: style.connectorColor,
1632
+ padding: 0,
1633
+ fontSize: "0.8em"
1634
+ },
1635
+ children: /* @__PURE__ */ jsx10(
1636
+ "i",
1637
+ {
1638
+ className: `fa-solid ${collapsed ? "fa-chevron-right" : "fa-chevron-down"}`,
1639
+ "aria-hidden": "true"
1640
+ }
1641
+ )
1642
+ }
1643
+ ) : /* @__PURE__ */ jsx10("span", { style: { flex: "0 0 auto", width: "1em" } }),
1644
+ /* @__PURE__ */ jsx10(
1645
+ "i",
1646
+ {
1647
+ className: iconCls,
1648
+ "aria-hidden": "true",
1649
+ style: { flex: "0 0 auto", color: style.iconColor, width: "1.2em", textAlign: "center" }
1650
+ }
1651
+ ),
1652
+ /* @__PURE__ */ jsx10(
1653
+ "span",
1654
+ {
1655
+ style: { color: isDir ? style.dirColor : style.rowColor, fontWeight: isDir ? 600 : 400 },
1656
+ children: item.label
1657
+ }
1658
+ ),
1659
+ item.comment ? /* @__PURE__ */ jsx10("span", { style: { color: style.commentColor, fontSize: "0.85em", fontStyle: "italic" }, children: item.comment }) : null
1660
+ ] }),
1661
+ hasChildren && !collapsed ? /* @__PURE__ */ jsx10(TreeList, { items: item.children, depth: 1, style }) : null
1662
+ ] });
1663
+ }
1664
+
1665
+ // src/BlockRenderer.tsx
1666
+ import { jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
1667
+ var DEFAULT_VIEWPORT = {
1431
1668
  width: 1920,
1432
1669
  height: 1080
1433
1670
  };
@@ -1438,12 +1675,13 @@ function BlockRenderer({
1438
1675
  isEntering = false,
1439
1676
  isExiting = false,
1440
1677
  transition,
1441
- viewport = VIEWPORT,
1442
- isPlaying
1678
+ viewport = DEFAULT_VIEWPORT,
1679
+ isPlaying,
1680
+ animationsEnabled = true
1443
1681
  }) {
1444
1682
  let transitionClass = "";
1445
1683
  const transitionStyle = {};
1446
- const activeTransition = transition ?? block.transition;
1684
+ const activeTransition = animationsEnabled ? transition ?? block.transition : void 0;
1447
1685
  if (activeTransition && isEntering) {
1448
1686
  transitionClass = getTransitionClass(activeTransition.type, true, activeTransition.direction);
1449
1687
  transitionStyle["--transition-duration"] = `${resolveTransitionDuration(activeTransition)}s`;
@@ -1451,8 +1689,9 @@ function BlockRenderer({
1451
1689
  transitionClass = getTransitionClass(activeTransition.type, false, activeTransition.direction);
1452
1690
  transitionStyle["--transition-duration"] = `${resolveTransitionDuration(activeTransition)}s`;
1453
1691
  }
1454
- const clipId = `vb-clip-${block.id}`;
1455
- return /* @__PURE__ */ jsxs7(
1692
+ const instanceId = useId5().replace(/:/g, "");
1693
+ const clipId = `vb-clip-${instanceId}-${block.id}`;
1694
+ return /* @__PURE__ */ jsxs8(
1456
1695
  "svg",
1457
1696
  {
1458
1697
  className: `block-svg ${transitionClass}`,
@@ -1462,15 +1701,16 @@ function BlockRenderer({
1462
1701
  overflow: "hidden",
1463
1702
  "data-block-id": block.id,
1464
1703
  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(
1704
+ /* @__PURE__ */ jsx11("defs", { children: /* @__PURE__ */ jsx11("clipPath", { id: clipId, children: /* @__PURE__ */ jsx11("rect", { x: "0", y: "0", width: viewport.width, height: viewport.height }) }) }),
1705
+ /* @__PURE__ */ jsx11("g", { clipPath: `url(#${clipId})`, children: (block.layers ?? []).map((layer) => /* @__PURE__ */ jsx11(
1467
1706
  LayerRenderer,
1468
1707
  {
1469
1708
  layer,
1470
1709
  basePath,
1471
1710
  viewport,
1472
1711
  blockTime,
1473
- isPlaying
1712
+ isPlaying,
1713
+ animationsEnabled
1474
1714
  },
1475
1715
  layer.id
1476
1716
  )) })
@@ -1478,23 +1718,48 @@ function BlockRenderer({
1478
1718
  }
1479
1719
  );
1480
1720
  }
1481
- function LayerRenderer({ layer, basePath, viewport, blockTime, isPlaying }) {
1482
- switch (layer.type) {
1721
+ function LayerRenderer({
1722
+ layer,
1723
+ basePath,
1724
+ viewport,
1725
+ blockTime,
1726
+ isPlaying,
1727
+ animationsEnabled
1728
+ }) {
1729
+ const renderedLayer = animationsEnabled || !layer.animation ? layer : { ...layer, animation: void 0 };
1730
+ switch (renderedLayer.type) {
1483
1731
  case "image":
1484
- return /* @__PURE__ */ jsx10(ImageLayer, { layer, basePath, viewport, blockTime });
1732
+ return /* @__PURE__ */ jsx11(
1733
+ ImageLayer,
1734
+ {
1735
+ layer: renderedLayer,
1736
+ basePath,
1737
+ viewport,
1738
+ blockTime,
1739
+ animationsEnabled
1740
+ }
1741
+ );
1485
1742
  case "text":
1486
- return /* @__PURE__ */ jsx10(TextLayer, { layer, viewport, blockTime });
1743
+ return /* @__PURE__ */ jsx11(TextLayer, { layer: renderedLayer, viewport, blockTime });
1487
1744
  case "shape":
1488
- return /* @__PURE__ */ jsx10(ShapeLayer, { layer, viewport, blockTime });
1745
+ return /* @__PURE__ */ jsx11(ShapeLayer, { layer: renderedLayer, viewport, blockTime });
1489
1746
  case "path":
1490
- return /* @__PURE__ */ jsx10(PathLayer, { layer, viewport, blockTime });
1747
+ return /* @__PURE__ */ jsx11(PathLayer, { layer: renderedLayer, viewport, blockTime });
1491
1748
  case "map":
1492
- return /* @__PURE__ */ jsx10(MapLayer, { layer, basePath, viewport, blockTime });
1749
+ return /* @__PURE__ */ jsx11(
1750
+ MapLayer,
1751
+ {
1752
+ layer: renderedLayer,
1753
+ basePath,
1754
+ viewport,
1755
+ blockTime
1756
+ }
1757
+ );
1493
1758
  case "video":
1494
- return /* @__PURE__ */ jsx10(
1759
+ return /* @__PURE__ */ jsx11(
1495
1760
  VideoLayer,
1496
1761
  {
1497
- layer,
1762
+ layer: renderedLayer,
1498
1763
  basePath,
1499
1764
  viewport,
1500
1765
  blockTime,
@@ -1502,9 +1767,11 @@ function LayerRenderer({ layer, basePath, viewport, blockTime, isPlaying }) {
1502
1767
  }
1503
1768
  );
1504
1769
  case "table":
1505
- return /* @__PURE__ */ jsx10(TableLayer, { layer, viewport, blockTime });
1770
+ return /* @__PURE__ */ jsx11(TableLayer, { layer: renderedLayer, viewport, blockTime });
1771
+ case "tree":
1772
+ return /* @__PURE__ */ jsx11(TreeLayer, { layer: renderedLayer, viewport, blockTime });
1506
1773
  default:
1507
- console.warn(`Unknown layer type: ${layer.type}`);
1774
+ console.warn(`Unknown layer type: ${renderedLayer.type}`);
1508
1775
  return null;
1509
1776
  }
1510
1777
  }
@@ -1515,7 +1782,7 @@ import { getCaptionAtTime } from "@bendyline/squisq/schemas";
1515
1782
  // src/SocialCaptionOverlay.tsx
1516
1783
  import { useMemo as useMemo4 } from "react";
1517
1784
  import { resolveFontFamily } from "@bendyline/squisq/schemas";
1518
- import { jsx as jsx11 } from "react/jsx-runtime";
1785
+ import { jsx as jsx12 } from "react/jsx-runtime";
1519
1786
  var TARGET_CHUNK_SIZE = 4;
1520
1787
  var MIN_CHUNK_SIZE = 2;
1521
1788
  var MAX_CHUNK_SIZE = 6;
@@ -1574,7 +1841,7 @@ function SocialCaptionOverlay({
1574
1841
  [captions]
1575
1842
  );
1576
1843
  if (!enabled || chunks.length === 0) {
1577
- return /* @__PURE__ */ jsx11(
1844
+ return /* @__PURE__ */ jsx12(
1578
1845
  "div",
1579
1846
  {
1580
1847
  className: "social-caption-overlay",
@@ -1636,7 +1903,7 @@ function SocialCaptionOverlay({
1636
1903
  const viewportHeight = viewport?.height ?? 720;
1637
1904
  const baseFontSize = Math.round(viewportHeight * 0.055);
1638
1905
  const fontSize = Math.max(24, Math.min(72, baseFontSize));
1639
- return /* @__PURE__ */ jsx11(
1906
+ return /* @__PURE__ */ jsx12(
1640
1907
  "div",
1641
1908
  {
1642
1909
  className: "social-caption-overlay",
@@ -1653,7 +1920,7 @@ function SocialCaptionOverlay({
1653
1920
  opacity: 1,
1654
1921
  transition: "opacity 0.15s ease-in-out"
1655
1922
  },
1656
- children: /* @__PURE__ */ jsx11(
1923
+ children: /* @__PURE__ */ jsx12(
1657
1924
  "div",
1658
1925
  {
1659
1926
  style: {
@@ -1662,7 +1929,7 @@ function SocialCaptionOverlay({
1662
1929
  },
1663
1930
  children: activeChunk.words.map((word, i) => {
1664
1931
  const isActive = i === activeWordIndex;
1665
- return /* @__PURE__ */ jsx11(
1932
+ return /* @__PURE__ */ jsx12(
1666
1933
  "span",
1667
1934
  {
1668
1935
  style: {
@@ -1687,7 +1954,7 @@ function SocialCaptionOverlay({
1687
1954
  }
1688
1955
 
1689
1956
  // src/CaptionOverlay.tsx
1690
- import { jsx as jsx12 } from "react/jsx-runtime";
1957
+ import { jsx as jsx13 } from "react/jsx-runtime";
1691
1958
  function CaptionOverlay({
1692
1959
  captions,
1693
1960
  currentTime,
@@ -1698,7 +1965,7 @@ function CaptionOverlay({
1698
1965
  viewport
1699
1966
  }) {
1700
1967
  if (captionStyle === "social") {
1701
- return /* @__PURE__ */ jsx12(
1968
+ return /* @__PURE__ */ jsx13(
1702
1969
  SocialCaptionOverlay,
1703
1970
  {
1704
1971
  captions,
@@ -1711,7 +1978,7 @@ function CaptionOverlay({
1711
1978
  }
1712
1979
  const phrase = enabled && captions ? getCaptionAtTime(captions, currentTime) : null;
1713
1980
  const captionText = phrase?.text ?? null;
1714
- return /* @__PURE__ */ jsx12(
1981
+ return /* @__PURE__ */ jsx13(
1715
1982
  "div",
1716
1983
  {
1717
1984
  className: "caption-overlay",
@@ -1730,7 +1997,7 @@ function CaptionOverlay({
1730
1997
  padding: "0 4px",
1731
1998
  boxSizing: "border-box"
1732
1999
  },
1733
- children: captionText && /* @__PURE__ */ jsx12(
2000
+ children: captionText && /* @__PURE__ */ jsx13(
1734
2001
  "div",
1735
2002
  {
1736
2003
  style: {
@@ -1740,7 +2007,7 @@ function CaptionOverlay({
1740
2007
  borderRadius: "4px",
1741
2008
  backdropFilter: "blur(4px)"
1742
2009
  },
1743
- children: /* @__PURE__ */ jsx12(
2010
+ children: /* @__PURE__ */ jsx13(
1744
2011
  "span",
1745
2012
  {
1746
2013
  style: {
@@ -1786,22 +2053,40 @@ function useAutoSurface(enabled) {
1786
2053
  }
1787
2054
 
1788
2055
  // 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);
2056
+ import { useState as useState4, useEffect as useEffect5, useRef as useRef3, useCallback as useCallback2 } from "react";
2057
+ function resolveAudioUrl(src, basePath) {
2058
+ if (!src || /^(?:[a-z][a-z0-9+.-]*:|\/\/|\/)/i.test(src)) return src;
2059
+ if (!basePath) return src;
2060
+ return `${basePath.replace(/\/$/, "")}/${src.replace(/^\//, "")}`;
2061
+ }
2062
+ function useAudioSync(audioRef, audioTrack, basePath = "", enabled = true) {
2063
+ const [currentTime, setCurrentTime] = useState4(0);
2064
+ const [isPlaying, setIsPlaying] = useState4(false);
2065
+ const [currentSegment, setCurrentSegment] = useState4(0);
2066
+ const [isEnded, setIsEnded] = useState4(false);
2067
+ const [isAudioReady, setIsAudioReady] = useState4(false);
2068
+ const [totalDuration, setTotalDuration] = useState4(0);
1797
2069
  const segmentStarts = useRef3([]);
1798
2070
  const pendingSeekTime = useRef3(null);
1799
2071
  const shouldPlayAfterLoad = useRef3(false);
1800
2072
  const blobUrls = useRef3(/* @__PURE__ */ new Map());
1801
2073
  const loadingPromises = useRef3(/* @__PURE__ */ new Map());
2074
+ const abortControllers = useRef3(/* @__PURE__ */ new Set());
2075
+ const loadGeneration = useRef3(0);
1802
2076
  const fallbackMode = useRef3(false);
1803
2077
  useEffect5(() => {
1804
- if (!audioTrack?.segments) {
2078
+ loadGeneration.current += 1;
2079
+ pendingSeekTime.current = null;
2080
+ shouldPlayAfterLoad.current = false;
2081
+ fallbackMode.current = false;
2082
+ setCurrentTime(0);
2083
+ setCurrentSegment(0);
2084
+ setIsPlaying(false);
2085
+ setIsEnded(false);
2086
+ setIsAudioReady(false);
2087
+ if (!enabled || !audioTrack?.segments) {
2088
+ segmentStarts.current = [];
2089
+ setTotalDuration(0);
1805
2090
  return;
1806
2091
  }
1807
2092
  let time = 0;
@@ -1811,27 +2096,35 @@ function useAudioSync(audioRef, audioTrack, basePath = "") {
1811
2096
  return start;
1812
2097
  });
1813
2098
  setTotalDuration(time);
1814
- }, [audioTrack]);
2099
+ }, [audioTrack, enabled]);
1815
2100
  const preloadAudio = useCallback2(
1816
2101
  async (src) => {
1817
- const audioUrl = basePath ? `${basePath}/${src}` : src;
2102
+ const audioUrl = resolveAudioUrl(src, basePath);
1818
2103
  if (blobUrls.current.has(src)) {
1819
2104
  return blobUrls.current.get(src);
1820
2105
  }
1821
2106
  if (loadingPromises.current.has(src)) {
1822
2107
  return loadingPromises.current.get(src);
1823
2108
  }
2109
+ const controller = new AbortController();
2110
+ abortControllers.current.add(controller);
2111
+ const generation = loadGeneration.current;
1824
2112
  const loadPromise = (async () => {
1825
2113
  try {
1826
- const response = await fetch(audioUrl);
2114
+ const response = await fetch(audioUrl, { signal: controller.signal });
1827
2115
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
1828
2116
  const blob = await response.blob();
1829
2117
  const blobUrl = URL.createObjectURL(blob);
2118
+ if (controller.signal.aborted || generation !== loadGeneration.current) {
2119
+ URL.revokeObjectURL(blobUrl);
2120
+ return audioUrl;
2121
+ }
1830
2122
  blobUrls.current.set(src, blobUrl);
1831
2123
  return blobUrl;
1832
2124
  } catch {
1833
2125
  return audioUrl;
1834
2126
  } finally {
2127
+ abortControllers.current.delete(controller);
1835
2128
  loadingPromises.current.delete(src);
1836
2129
  }
1837
2130
  })();
@@ -1841,19 +2134,26 @@ function useAudioSync(audioRef, audioTrack, basePath = "") {
1841
2134
  [basePath]
1842
2135
  );
1843
2136
  useEffect5(() => {
1844
- if (!audioTrack?.segments) return;
2137
+ if (!enabled || !audioTrack?.segments) return;
1845
2138
  audioTrack.segments.forEach((segment) => {
1846
2139
  preloadAudio(segment.src);
1847
2140
  });
1848
2141
  const currentBlobUrls = blobUrls.current;
2142
+ const currentAbortControllers = abortControllers.current;
2143
+ const currentLoadingPromises = loadingPromises.current;
1849
2144
  return () => {
2145
+ loadGeneration.current += 1;
2146
+ currentAbortControllers.forEach((controller) => controller.abort());
2147
+ currentAbortControllers.clear();
2148
+ currentLoadingPromises.clear();
1850
2149
  currentBlobUrls.forEach((url) => {
1851
2150
  URL.revokeObjectURL(url);
1852
2151
  });
1853
2152
  currentBlobUrls.clear();
1854
2153
  };
1855
- }, [audioTrack, preloadAudio]);
2154
+ }, [audioTrack, preloadAudio, enabled]);
1856
2155
  useEffect5(() => {
2156
+ if (!enabled) return;
1857
2157
  const audio = audioRef.current;
1858
2158
  if (!audio) return;
1859
2159
  const handleTimeUpdate = () => {
@@ -1890,8 +2190,9 @@ function useAudioSync(audioRef, audioTrack, basePath = "") {
1890
2190
  audio.removeEventListener("ended", handleEnded);
1891
2191
  audio.removeEventListener("error", handleError);
1892
2192
  };
1893
- }, [audioRef, currentSegment, audioTrack]);
2193
+ }, [audioRef, currentSegment, audioTrack, enabled]);
1894
2194
  useEffect5(() => {
2195
+ if (!enabled) return;
1895
2196
  const audio = audioRef.current;
1896
2197
  if (!audio || !audioTrack?.segments) return;
1897
2198
  const segment = audioTrack.segments[currentSegment];
@@ -1913,29 +2214,37 @@ function useAudioSync(audioRef, audioTrack, basePath = "") {
1913
2214
  const currentSrc = audio.src;
1914
2215
  const cachedBlobUrl = blobUrls.current.get(segment.src);
1915
2216
  const isSameSource = currentSrc && (currentSrc === cachedBlobUrl || currentSrc.endsWith(segment.src));
2217
+ let cancelled = false;
2218
+ let handleCanPlay = null;
1916
2219
  if (!isSameSource) {
1917
2220
  const loadAndPlay = async () => {
1918
2221
  const blobUrl = await preloadAudio(segment.src);
1919
- const handleCanPlay = () => {
2222
+ if (cancelled) return;
2223
+ handleCanPlay = () => {
2224
+ if (cancelled) return;
1920
2225
  setIsAudioReady(true);
1921
2226
  applyPendingSeek();
1922
- audio.removeEventListener("canplay", handleCanPlay);
2227
+ if (handleCanPlay) audio.removeEventListener("canplay", handleCanPlay);
1923
2228
  };
1924
2229
  audio.addEventListener("canplay", handleCanPlay);
1925
2230
  audio.src = blobUrl;
1926
2231
  audio.load();
1927
2232
  await Promise.resolve();
1928
2233
  if (audio.readyState >= 3) {
1929
- audio.removeEventListener("canplay", handleCanPlay);
2234
+ if (handleCanPlay) audio.removeEventListener("canplay", handleCanPlay);
1930
2235
  setIsAudioReady(true);
1931
2236
  applyPendingSeek();
1932
2237
  }
1933
2238
  };
1934
- loadAndPlay();
2239
+ void loadAndPlay();
1935
2240
  } else {
1936
2241
  applyPendingSeek();
1937
2242
  }
1938
- }, [audioRef, currentSegment, audioTrack, preloadAudio]);
2243
+ return () => {
2244
+ cancelled = true;
2245
+ if (handleCanPlay) audio.removeEventListener("canplay", handleCanPlay);
2246
+ };
2247
+ }, [audioRef, currentSegment, audioTrack, preloadAudio, enabled]);
1939
2248
  const play = useCallback2(() => {
1940
2249
  const audio = audioRef.current;
1941
2250
  if (audio) {
@@ -2072,8 +2381,8 @@ import {
2072
2381
  resolvePersistentLayers,
2073
2382
  VIEWPORT_PRESETS
2074
2383
  } from "@bendyline/squisq/doc";
2075
- function useDocPlayback(script, currentTime, viewport = VIEWPORT_PRESETS.landscape, renderMode = false, theme) {
2076
- void renderMode;
2384
+ function useDocPlayback(script, currentTime, options = {}) {
2385
+ const { viewport = VIEWPORT_PRESETS.landscape, theme, onSeek } = options;
2077
2386
  const blocks = useMemo6(() => {
2078
2387
  if (!script?.blocks) {
2079
2388
  return [];
@@ -2136,8 +2445,19 @@ function useDocPlayback(script, currentTime, viewport = VIEWPORT_PRESETS.landsca
2136
2445
  const outgoingBlockRef = useRef4(null);
2137
2446
  const activeBlockIdRef = useRef4(null);
2138
2447
  const lastRenderedBlockRef = useRef4(null);
2448
+ const suppressOutgoingTargetRef = useRef4(null);
2449
+ const suppressOutgoingForNextBlock = useCallback3((blockId) => {
2450
+ if (activeBlockIdRef.current === blockId) {
2451
+ outgoingBlockRef.current = null;
2452
+ suppressOutgoingTargetRef.current = null;
2453
+ return;
2454
+ }
2455
+ suppressOutgoingTargetRef.current = blockId;
2456
+ }, []);
2139
2457
  if (currentBlock && currentBlock.id !== activeBlockIdRef.current) {
2140
- outgoingBlockRef.current = lastRenderedBlockRef.current;
2458
+ const suppressOutgoing = suppressOutgoingTargetRef.current === currentBlock.id;
2459
+ outgoingBlockRef.current = suppressOutgoing ? null : lastRenderedBlockRef.current;
2460
+ suppressOutgoingTargetRef.current = null;
2141
2461
  activeBlockIdRef.current = currentBlock.id;
2142
2462
  }
2143
2463
  lastRenderedBlockRef.current = currentBlock;
@@ -2151,10 +2471,10 @@ function useDocPlayback(script, currentTime, viewport = VIEWPORT_PRESETS.landsca
2151
2471
  if (!script || index < 0 || index >= blocks.length) return;
2152
2472
  const targetBlock = blocks[index];
2153
2473
  if (targetBlock) {
2154
- return targetBlock.startTime;
2474
+ onSeek?.(targetBlock.startTime);
2155
2475
  }
2156
2476
  },
2157
- [script, blocks]
2477
+ [script, blocks, onSeek]
2158
2478
  );
2159
2479
  const nextBlock = useCallback3(() => {
2160
2480
  if (currentBlockIndex < blocks.length - 1) {
@@ -2178,13 +2498,14 @@ function useDocPlayback(script, currentTime, viewport = VIEWPORT_PRESETS.landsca
2178
2498
  nextBlock,
2179
2499
  prevBlock,
2180
2500
  goToBlock,
2501
+ suppressOutgoingForNextBlock,
2181
2502
  /** Expanded blocks (templates converted to full blocks with layers) */
2182
2503
  blocks
2183
2504
  };
2184
2505
  }
2185
2506
 
2186
2507
  // src/hooks/useViewportOrientation.ts
2187
- import { useState as useState4, useEffect as useEffect6, useMemo as useMemo7 } from "react";
2508
+ import { useState as useState5, useEffect as useEffect6, useMemo as useMemo7 } from "react";
2188
2509
  import {
2189
2510
  VIEWPORT_PRESETS as VIEWPORT_PRESETS2
2190
2511
  } from "@bendyline/squisq/doc";
@@ -2195,7 +2516,7 @@ function getOrientationFromWindow(width, height) {
2195
2516
  } else if (ratio < 0.83) {
2196
2517
  return "portrait";
2197
2518
  } else {
2198
- return "landscape";
2519
+ return "square";
2199
2520
  }
2200
2521
  }
2201
2522
  function getViewportForOrientation(orientation) {
@@ -2210,7 +2531,7 @@ function getViewportForOrientation(orientation) {
2210
2531
  }
2211
2532
  }
2212
2533
  function useViewportOrientation() {
2213
- const [windowSize, setWindowSize] = useState4(() => ({
2534
+ const [windowSize, setWindowSize] = useState5(() => ({
2214
2535
  width: typeof window !== "undefined" ? window.innerWidth : 1920,
2215
2536
  height: typeof window !== "undefined" ? window.innerHeight : 1080
2216
2537
  }));
@@ -2246,7 +2567,7 @@ function useViewportOrientation() {
2246
2567
  }
2247
2568
 
2248
2569
  // src/hooks/useSlideSwipe.ts
2249
- import { useCallback as useCallback4, useEffect as useEffect7, useRef as useRef5, useState as useState5 } from "react";
2570
+ import { useCallback as useCallback4, useEffect as useEffect7, useRef as useRef5, useState as useState6 } from "react";
2250
2571
  var DISTANCE_RATIO = 0.3;
2251
2572
  var FLICK_VELOCITY = 0.5;
2252
2573
  var MIN_FLICK_DISTANCE = 12;
@@ -2270,8 +2591,8 @@ function decideSwipe({
2270
2591
  }
2271
2592
  function useSlideSwipe(opts) {
2272
2593
  const settleMs = opts.settleMs ?? DEFAULT_SETTLE_MS;
2273
- const [offsetPx, setOffsetPx] = useState5(0);
2274
- const [phase, setPhase] = useState5("idle");
2594
+ const [offsetPx, setOffsetPx] = useState6(0);
2595
+ const [phase, setPhase] = useState6("idle");
2275
2596
  const optsRef = useRef5(opts);
2276
2597
  optsRef.current = opts;
2277
2598
  const dragRef = useRef5(null);
@@ -2402,7 +2723,7 @@ import {
2402
2723
  import { parseMarkdown as parseMarkdown2 } from "@bendyline/squisq/markdown";
2403
2724
 
2404
2725
  // src/DocProgressBar.tsx
2405
- import { useRef as useRef6, useState as useState6, useCallback as useCallback5 } from "react";
2726
+ import { useRef as useRef6, useState as useState7, useCallback as useCallback5 } from "react";
2406
2727
 
2407
2728
  // src/types.ts
2408
2729
  function formatTime(seconds) {
@@ -2412,7 +2733,7 @@ function formatTime(seconds) {
2412
2733
  }
2413
2734
 
2414
2735
  // src/DocProgressBar.tsx
2415
- import { jsx as jsx13, jsxs as jsxs8 } from "react/jsx-runtime";
2736
+ import { jsx as jsx14, jsxs as jsxs9 } from "react/jsx-runtime";
2416
2737
  function DocProgressBar({
2417
2738
  state,
2418
2739
  actions,
@@ -2421,7 +2742,7 @@ function DocProgressBar({
2421
2742
  getBlockTitle
2422
2743
  }) {
2423
2744
  const progressBarRef = useRef6(null);
2424
- const [hoverPosition, setHoverPosition] = useState6(null);
2745
+ const [hoverPosition, setHoverPosition] = useState7(null);
2425
2746
  const playProgress = state.totalDuration > 0 ? Math.max(0, Math.min(1, state.currentTime / state.totalDuration)) : 0;
2426
2747
  const handleProgressHover = useCallback5((e) => {
2427
2748
  const bar = progressBarRef.current;
@@ -2434,6 +2755,31 @@ function DocProgressBar({
2434
2755
  const handleProgressLeave = useCallback5(() => {
2435
2756
  setHoverPosition(null);
2436
2757
  }, []);
2758
+ const handleProgressKeyDown = useCallback5(
2759
+ (e) => {
2760
+ let next = null;
2761
+ switch (e.key) {
2762
+ case "ArrowLeft":
2763
+ case "ArrowDown":
2764
+ next = state.currentTime - 5;
2765
+ break;
2766
+ case "ArrowRight":
2767
+ case "ArrowUp":
2768
+ next = state.currentTime + 5;
2769
+ break;
2770
+ case "Home":
2771
+ next = 0;
2772
+ break;
2773
+ case "End":
2774
+ next = state.totalDuration;
2775
+ break;
2776
+ }
2777
+ if (next == null) return;
2778
+ e.preventDefault();
2779
+ actions.seekTo(Math.max(0, Math.min(state.totalDuration, next)));
2780
+ },
2781
+ [actions, state.currentTime, state.totalDuration]
2782
+ );
2437
2783
  const getBlockAtTimeLocal = useCallback5(
2438
2784
  (time) => {
2439
2785
  for (let i = expandedBlocks.length - 1; i >= 0; i--) {
@@ -2446,10 +2792,12 @@ function DocProgressBar({
2446
2792
  },
2447
2793
  [expandedBlocks]
2448
2794
  );
2449
- return /* @__PURE__ */ jsxs8(
2795
+ return /* @__PURE__ */ jsxs9(
2450
2796
  "div",
2451
2797
  {
2452
2798
  ref: progressBarRef,
2799
+ role: "group",
2800
+ "aria-label": "Playback timeline",
2453
2801
  style: {
2454
2802
  flex: 1,
2455
2803
  height: "24px",
@@ -2467,9 +2815,17 @@ function DocProgressBar({
2467
2815
  onMouseMove: handleProgressHover,
2468
2816
  onMouseLeave: handleProgressLeave,
2469
2817
  children: [
2470
- /* @__PURE__ */ jsx13(
2818
+ /* @__PURE__ */ jsx14(
2471
2819
  "div",
2472
2820
  {
2821
+ role: "slider",
2822
+ tabIndex: 0,
2823
+ "aria-label": "Playback position",
2824
+ "aria-valuemin": 0,
2825
+ "aria-valuemax": state.totalDuration,
2826
+ "aria-valuenow": Math.max(0, Math.min(state.totalDuration, state.currentTime)),
2827
+ "aria-valuetext": `${formatTime(state.currentTime)} of ${formatTime(state.totalDuration)}`,
2828
+ onKeyDown: handleProgressKeyDown,
2473
2829
  style: {
2474
2830
  position: "absolute",
2475
2831
  left: 0,
@@ -2480,7 +2836,7 @@ function DocProgressBar({
2480
2836
  }
2481
2837
  }
2482
2838
  ),
2483
- /* @__PURE__ */ jsx13(
2839
+ /* @__PURE__ */ jsx14(
2484
2840
  "div",
2485
2841
  {
2486
2842
  "data-testid": "doc-progress-fill",
@@ -2494,9 +2850,10 @@ function DocProgressBar({
2494
2850
  }
2495
2851
  }
2496
2852
  ),
2497
- blockMarkers.map((marker, i) => /* @__PURE__ */ jsx13(
2498
- "div",
2853
+ blockMarkers.map((marker, i) => /* @__PURE__ */ jsx14(
2854
+ "button",
2499
2855
  {
2856
+ type: "button",
2500
2857
  style: {
2501
2858
  position: "absolute",
2502
2859
  left: `${marker.position}%`,
@@ -2506,11 +2863,13 @@ function DocProgressBar({
2506
2863
  borderRadius: "50%",
2507
2864
  background: marker.index === state.currentBlockIndex ? "#ffffff" : "rgba(255,255,255,0.5)",
2508
2865
  border: "2px solid #5b9bd5",
2866
+ padding: 0,
2509
2867
  cursor: "pointer",
2510
2868
  zIndex: 2,
2511
2869
  transition: "transform 0.15s, background 0.15s"
2512
2870
  },
2513
2871
  title: marker.title,
2872
+ "aria-label": `Seek to ${marker.title}`,
2514
2873
  onClick: (e) => {
2515
2874
  e.stopPropagation();
2516
2875
  actions.seekTo(marker.block.startTime);
@@ -2524,7 +2883,7 @@ function DocProgressBar({
2524
2883
  },
2525
2884
  `${marker.block.id}-${i}`
2526
2885
  )),
2527
- hoverPosition !== null && /* @__PURE__ */ jsxs8(
2886
+ hoverPosition !== null && /* @__PURE__ */ jsxs9(
2528
2887
  "div",
2529
2888
  {
2530
2889
  style: {
@@ -2541,19 +2900,19 @@ function DocProgressBar({
2541
2900
  zIndex: 10
2542
2901
  },
2543
2902
  children: [
2544
- /* @__PURE__ */ jsx13("div", { style: { color: "white", fontSize: "12px", fontFamily: "monospace" }, children: formatTime(hoverPosition * state.totalDuration) }),
2903
+ /* @__PURE__ */ jsx14("div", { style: { color: "white", fontSize: "12px", fontFamily: "monospace" }, children: formatTime(hoverPosition * state.totalDuration) }),
2545
2904
  (() => {
2546
2905
  const hoverTime = hoverPosition * state.totalDuration;
2547
2906
  const slideInfo = getBlockAtTimeLocal(hoverTime);
2548
2907
  if (slideInfo && getBlockTitle) {
2549
- return /* @__PURE__ */ jsx13("div", { style: { color: "rgba(255,255,255,0.7)", fontSize: "11px", marginTop: "2px" }, children: getBlockTitle(slideInfo.block) });
2908
+ return /* @__PURE__ */ jsx14("div", { style: { color: "rgba(255,255,255,0.7)", fontSize: "11px", marginTop: "2px" }, children: getBlockTitle(slideInfo.block) });
2550
2909
  }
2551
2910
  return null;
2552
2911
  })()
2553
2912
  ]
2554
2913
  }
2555
2914
  ),
2556
- hoverPosition !== null && /* @__PURE__ */ jsx13(
2915
+ hoverPosition !== null && /* @__PURE__ */ jsx14(
2557
2916
  "div",
2558
2917
  {
2559
2918
  style: {
@@ -2575,7 +2934,7 @@ function DocProgressBar({
2575
2934
  }
2576
2935
 
2577
2936
  // src/DocControlsOverlay.tsx
2578
- import { jsx as jsx14, jsxs as jsxs9 } from "react/jsx-runtime";
2937
+ import { jsx as jsx15, jsxs as jsxs10 } from "react/jsx-runtime";
2579
2938
  function DocControlsOverlay({
2580
2939
  state,
2581
2940
  actions,
@@ -2583,7 +2942,7 @@ function DocControlsOverlay({
2583
2942
  expandedBlocks,
2584
2943
  getBlockTitle
2585
2944
  }) {
2586
- return /* @__PURE__ */ jsxs9(
2945
+ return /* @__PURE__ */ jsxs10(
2587
2946
  "div",
2588
2947
  {
2589
2948
  className: "doc-player__controls",
@@ -2600,7 +2959,7 @@ function DocControlsOverlay({
2600
2959
  zIndex: 100
2601
2960
  },
2602
2961
  children: [
2603
- /* @__PURE__ */ jsx14(
2962
+ /* @__PURE__ */ jsx15(
2604
2963
  "button",
2605
2964
  {
2606
2965
  onClick: actions.restart,
@@ -2616,10 +2975,10 @@ function DocControlsOverlay({
2616
2975
  },
2617
2976
  title: "Restart",
2618
2977
  "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" }) })
2978
+ 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
2979
  }
2621
2980
  ),
2622
- /* @__PURE__ */ jsx14(
2981
+ /* @__PURE__ */ jsx15(
2623
2982
  "button",
2624
2983
  {
2625
2984
  onClick: actions.toggle,
@@ -2638,15 +2997,15 @@ function DocControlsOverlay({
2638
2997
  height: "40px"
2639
2998
  },
2640
2999
  "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" }) })
3000
+ 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
3001
  }
2643
3002
  ),
2644
- /* @__PURE__ */ jsxs9("span", { style: { color: "white", fontSize: "12px", minWidth: "80px", fontFamily: "monospace" }, children: [
3003
+ /* @__PURE__ */ jsxs10("span", { style: { color: "white", fontSize: "12px", minWidth: "80px", fontFamily: "monospace" }, children: [
2645
3004
  formatTime(state.currentTime),
2646
3005
  " / ",
2647
3006
  formatTime(state.totalDuration)
2648
3007
  ] }),
2649
- /* @__PURE__ */ jsx14(
3008
+ /* @__PURE__ */ jsx15(
2650
3009
  DocProgressBar,
2651
3010
  {
2652
3011
  state,
@@ -2656,12 +3015,12 @@ function DocControlsOverlay({
2656
3015
  getBlockTitle
2657
3016
  }
2658
3017
  ),
2659
- /* @__PURE__ */ jsxs9("span", { style: { color: "rgba(255,255,255,0.5)", fontSize: "11px", whiteSpace: "nowrap" }, children: [
3018
+ /* @__PURE__ */ jsxs10("span", { style: { color: "rgba(255,255,255,0.5)", fontSize: "11px", whiteSpace: "nowrap" }, children: [
2660
3019
  state.currentBlockIndex + 1,
2661
3020
  "/",
2662
3021
  state.totalBlocks
2663
3022
  ] }),
2664
- state.hasCaptions && /* @__PURE__ */ jsxs9(
3023
+ state.hasCaptions && /* @__PURE__ */ jsxs10(
2665
3024
  "button",
2666
3025
  {
2667
3026
  onClick: () => actions.cycleCaptionMode(),
@@ -2680,12 +3039,12 @@ function DocControlsOverlay({
2680
3039
  title: state.captionMode === "off" ? "Captions: Off (click for Standard)" : state.captionMode === "standard" ? "Captions: Standard (click for Social)" : "Captions: Social (click to turn off)",
2681
3040
  "aria-label": "Cycle caption style",
2682
3041
  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" })
3042
+ /* @__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" }) }),
3043
+ state.captionMode !== "off" && /* @__PURE__ */ jsx15("span", { style: { fontSize: "10px", textTransform: "uppercase", letterSpacing: "0.5px" }, children: state.captionMode === "standard" ? "CC" : "SM" })
2685
3044
  ]
2686
3045
  }
2687
3046
  ),
2688
- actions.toggleFullscreen && /* @__PURE__ */ jsx14(
3047
+ actions.toggleFullscreen && /* @__PURE__ */ jsx15(
2689
3048
  "button",
2690
3049
  {
2691
3050
  onClick: actions.toggleFullscreen,
@@ -2701,7 +3060,7 @@ function DocControlsOverlay({
2701
3060
  },
2702
3061
  title: state.isFullscreen ? "Exit fullscreen (F)" : "Fullscreen (F)",
2703
3062
  "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" }) })
3063
+ 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
3064
  }
2706
3065
  )
2707
3066
  ]
@@ -2710,8 +3069,29 @@ function DocControlsOverlay({
2710
3069
  }
2711
3070
 
2712
3071
  // src/DocControlsSlideshow.tsx
2713
- import { jsx as jsx15, jsxs as jsxs10 } from "react/jsx-runtime";
2714
- function DocControlsSlideshow({ state, slideNav }) {
3072
+ import { useCallback as useCallback6, useEffect as useEffect8, useId as useId6, useLayoutEffect, useRef as useRef7, useState as useState8 } from "react";
3073
+ import { jsx as jsx16, jsxs as jsxs11 } from "react/jsx-runtime";
3074
+ function DocControlsSlideshow({
3075
+ state,
3076
+ slideNav,
3077
+ slides = [],
3078
+ pickerOpen,
3079
+ onPickerOpenChange
3080
+ }) {
3081
+ const [uncontrolledPickerOpen, setUncontrolledPickerOpen] = useState8(false);
3082
+ const isPickerOpen = pickerOpen ?? uncontrolledPickerOpen;
3083
+ const setPickerOpen = useCallback6(
3084
+ (open) => {
3085
+ if (pickerOpen === void 0) setUncontrolledPickerOpen(open);
3086
+ onPickerOpenChange?.(open);
3087
+ },
3088
+ [pickerOpen, onPickerOpenChange]
3089
+ );
3090
+ const [pickerMaxHeight, setPickerMaxHeight] = useState8(280);
3091
+ const controlsRef = useRef7(null);
3092
+ const triggerRef = useRef7(null);
3093
+ const menuRef = useRef7(null);
3094
+ const menuId = useId6();
2715
3095
  const {
2716
3096
  currentBlockIndex,
2717
3097
  currentSlideLabel,
@@ -2722,9 +3102,68 @@ function DocControlsSlideshow({ state, slideNav }) {
2722
3102
  const isFirst = currentBlockIndex <= 0;
2723
3103
  const isLast = currentBlockIndex >= totalBlocks - 1;
2724
3104
  const counterText = totalBlocks > 0 ? currentSlideLabel ?? `${currentSlideNumber ?? currentBlockIndex + 1} / ${totalSlideNumber ?? totalBlocks}` : "\u2014";
2725
- return /* @__PURE__ */ jsxs10(
3105
+ useEffect8(() => {
3106
+ if (!isPickerOpen) return;
3107
+ const handlePointerDown = (event) => {
3108
+ if (!controlsRef.current?.contains(event.target)) setPickerOpen(false);
3109
+ };
3110
+ const handleKeyDown = (event) => {
3111
+ if (event.key !== "Escape") return;
3112
+ event.preventDefault();
3113
+ setPickerOpen(false);
3114
+ triggerRef.current?.focus();
3115
+ };
3116
+ document.addEventListener("pointerdown", handlePointerDown);
3117
+ document.addEventListener("keydown", handleKeyDown);
3118
+ return () => {
3119
+ document.removeEventListener("pointerdown", handlePointerDown);
3120
+ document.removeEventListener("keydown", handleKeyDown);
3121
+ };
3122
+ }, [isPickerOpen, setPickerOpen]);
3123
+ useLayoutEffect(() => {
3124
+ if (!isPickerOpen) return;
3125
+ const controls = controlsRef.current;
3126
+ const player = controls?.closest(".doc-player") ?? controls?.parentElement;
3127
+ if (!controls || !player) return;
3128
+ const updateMaxHeight = () => {
3129
+ const controlsBounds = controls.getBoundingClientRect();
3130
+ const playerBounds = player.getBoundingClientRect();
3131
+ const availableHeight = Math.floor(controlsBounds.top - playerBounds.top - 8 - 16);
3132
+ setPickerMaxHeight(Math.max(72, availableHeight));
3133
+ };
3134
+ updateMaxHeight();
3135
+ const resizeObserver = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(updateMaxHeight);
3136
+ resizeObserver?.observe(player);
3137
+ resizeObserver?.observe(controls);
3138
+ window.addEventListener("resize", updateMaxHeight);
3139
+ return () => {
3140
+ resizeObserver?.disconnect();
3141
+ window.removeEventListener("resize", updateMaxHeight);
3142
+ };
3143
+ }, [isPickerOpen]);
3144
+ useEffect8(() => {
3145
+ if (!isPickerOpen) return;
3146
+ menuRef.current?.querySelector('[aria-current="true"]')?.focus();
3147
+ }, [isPickerOpen]);
3148
+ const handleMenuKeyDown = (event) => {
3149
+ if (!["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) return;
3150
+ const items = Array.from(
3151
+ menuRef.current?.querySelectorAll('[role="menuitem"]') ?? []
3152
+ );
3153
+ if (items.length === 0) return;
3154
+ event.preventDefault();
3155
+ const focusedIndex = items.indexOf(document.activeElement);
3156
+ let nextIndex = focusedIndex;
3157
+ if (event.key === "Home") nextIndex = 0;
3158
+ else if (event.key === "End") nextIndex = items.length - 1;
3159
+ else if (event.key === "ArrowDown") nextIndex = (focusedIndex + 1) % items.length;
3160
+ else nextIndex = (focusedIndex - 1 + items.length) % items.length;
3161
+ items[nextIndex]?.focus();
3162
+ };
3163
+ return /* @__PURE__ */ jsxs11(
2726
3164
  "div",
2727
3165
  {
3166
+ ref: controlsRef,
2728
3167
  className: "doc-controls-slideshow",
2729
3168
  "data-testid": "slideshow-controls",
2730
3169
  style: {
@@ -2743,7 +3182,7 @@ function DocControlsSlideshow({ state, slideNav }) {
2743
3182
  WebkitBackdropFilter: "blur(8px)"
2744
3183
  },
2745
3184
  children: [
2746
- /* @__PURE__ */ jsx15(
3185
+ /* @__PURE__ */ jsx16(
2747
3186
  "button",
2748
3187
  {
2749
3188
  onClick: (e) => {
@@ -2772,27 +3211,138 @@ function DocControlsSlideshow({ state, slideNav }) {
2772
3211
  onMouseLeave: (e) => {
2773
3212
  e.currentTarget.style.background = "none";
2774
3213
  },
2775
- children: /* @__PURE__ */ jsx15("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx15("path", { d: "M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z" }) })
3214
+ children: /* @__PURE__ */ jsx16("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx16("path", { d: "M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z" }) })
2776
3215
  }
2777
3216
  ),
2778
- /* @__PURE__ */ jsx15(
2779
- "span",
3217
+ /* @__PURE__ */ jsx16(
3218
+ "button",
2780
3219
  {
3220
+ ref: triggerRef,
3221
+ type: "button",
2781
3222
  "data-testid": "slide-counter",
3223
+ "aria-label": `Choose slide, current ${counterText}`,
3224
+ "aria-haspopup": "menu",
3225
+ "aria-expanded": isPickerOpen,
3226
+ "aria-controls": isPickerOpen ? menuId : void 0,
3227
+ title: "Choose slide",
3228
+ disabled: slides.length === 0,
3229
+ onClick: (event) => {
3230
+ event.stopPropagation();
3231
+ setPickerOpen(!isPickerOpen);
3232
+ },
2782
3233
  style: {
3234
+ background: isPickerOpen ? "rgba(255,255,255,0.12)" : "none",
3235
+ border: "none",
2783
3236
  color: "rgba(255,255,255,0.9)",
3237
+ cursor: slides.length > 0 ? "pointer" : "default",
2784
3238
  fontSize: "13px",
2785
3239
  fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
2786
3240
  fontVariantNumeric: "tabular-nums",
2787
3241
  minWidth: "48px",
2788
3242
  textAlign: "center",
2789
- padding: "0 4px",
2790
- letterSpacing: "0.02em"
3243
+ padding: "6px 4px",
3244
+ letterSpacing: "0.02em",
3245
+ borderRadius: "4px",
3246
+ transition: "background 0.15s"
2791
3247
  },
2792
3248
  children: counterText
2793
3249
  }
2794
3250
  ),
2795
- /* @__PURE__ */ jsx15(
3251
+ isPickerOpen && /* @__PURE__ */ jsx16(
3252
+ "div",
3253
+ {
3254
+ ref: menuRef,
3255
+ id: menuId,
3256
+ role: "menu",
3257
+ "aria-label": "Choose a slide",
3258
+ "data-testid": "slide-picker",
3259
+ onClick: (event) => event.stopPropagation(),
3260
+ onKeyDown: handleMenuKeyDown,
3261
+ style: {
3262
+ position: "absolute",
3263
+ right: 0,
3264
+ bottom: "calc(100% + 8px)",
3265
+ width: "min(280px, calc(100vw - 32px))",
3266
+ maxHeight: `${pickerMaxHeight}px`,
3267
+ overflowY: "auto",
3268
+ padding: "6px",
3269
+ background: "rgba(20, 20, 20, 0.94)",
3270
+ border: "1px solid rgba(255,255,255,0.14)",
3271
+ borderRadius: "8px",
3272
+ boxShadow: "0 10px 30px rgba(0,0,0,0.38)",
3273
+ backdropFilter: "blur(12px)",
3274
+ WebkitBackdropFilter: "blur(12px)"
3275
+ },
3276
+ children: slides.map((slide, index) => {
3277
+ const isCurrent = index === currentBlockIndex;
3278
+ return /* @__PURE__ */ jsxs11(
3279
+ "button",
3280
+ {
3281
+ type: "button",
3282
+ role: "menuitem",
3283
+ "aria-current": isCurrent ? "true" : void 0,
3284
+ "data-testid": `slide-picker-item-${index}`,
3285
+ onClick: () => {
3286
+ slideNav.goToSlide(index);
3287
+ setPickerOpen(false);
3288
+ },
3289
+ style: {
3290
+ display: "grid",
3291
+ gridTemplateColumns: "42px minmax(0, 1fr)",
3292
+ alignItems: "center",
3293
+ gap: "8px",
3294
+ width: "100%",
3295
+ padding: "8px 10px",
3296
+ background: isCurrent ? "rgba(255,255,255,0.14)" : "transparent",
3297
+ border: "none",
3298
+ borderRadius: "5px",
3299
+ color: "rgba(255,255,255,0.94)",
3300
+ cursor: "pointer",
3301
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
3302
+ textAlign: "left"
3303
+ },
3304
+ onMouseEnter: (event) => {
3305
+ event.currentTarget.style.background = "rgba(255,255,255,0.1)";
3306
+ },
3307
+ onMouseLeave: (event) => {
3308
+ event.currentTarget.style.background = isCurrent ? "rgba(255,255,255,0.14)" : "transparent";
3309
+ },
3310
+ children: [
3311
+ /* @__PURE__ */ jsx16(
3312
+ "span",
3313
+ {
3314
+ style: {
3315
+ color: isCurrent ? "#fff" : "rgba(255,255,255,0.58)",
3316
+ fontSize: "12px",
3317
+ fontVariantNumeric: "tabular-nums",
3318
+ textAlign: "right"
3319
+ },
3320
+ children: slide.label
3321
+ }
3322
+ ),
3323
+ /* @__PURE__ */ jsx16(
3324
+ "span",
3325
+ {
3326
+ style: {
3327
+ minWidth: 0,
3328
+ overflow: "hidden",
3329
+ color: isCurrent ? "#fff" : "rgba(255,255,255,0.82)",
3330
+ fontSize: "13px",
3331
+ lineHeight: 1.3,
3332
+ textOverflow: "ellipsis",
3333
+ whiteSpace: "nowrap"
3334
+ },
3335
+ children: slide.summary
3336
+ }
3337
+ )
3338
+ ]
3339
+ },
3340
+ `${slide.id}-${index}`
3341
+ );
3342
+ })
3343
+ }
3344
+ ),
3345
+ /* @__PURE__ */ jsx16(
2796
3346
  "button",
2797
3347
  {
2798
3348
  onClick: (e) => {
@@ -2821,7 +3371,7 @@ function DocControlsSlideshow({ state, slideNav }) {
2821
3371
  onMouseLeave: (e) => {
2822
3372
  e.currentTarget.style.background = "none";
2823
3373
  },
2824
- children: /* @__PURE__ */ jsx15("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx15("path", { d: "M8.59 16.59L10 18l6-6-6-6-1.41 1.41L13.17 12z" }) })
3374
+ children: /* @__PURE__ */ jsx16("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx16("path", { d: "M8.59 16.59L10 18l6-6-6-6-1.41 1.41L13.17 12z" }) })
2825
3375
  }
2826
3376
  )
2827
3377
  ]
@@ -2830,18 +3380,18 @@ function DocControlsSlideshow({ state, slideNav }) {
2830
3380
  }
2831
3381
 
2832
3382
  // src/LinearDocView.tsx
2833
- import { useMemo as useMemo8 } from "react";
3383
+ import { useEffect as useEffect9, useMemo as useMemo8, useRef as useRef8 } from "react";
2834
3384
  import {
2835
3385
  applySurface,
2836
3386
  resolveFontFamily as resolveFontFamily2
2837
3387
  } from "@bendyline/squisq/schemas";
2838
3388
  import { VIEWPORT_PRESETS as VIEWPORT_PRESETS3 } from "@bendyline/squisq/schemas";
2839
3389
  import {
2840
- getLayers,
2841
- hasTemplate,
3390
+ materializeBlockLayers,
2842
3391
  markdownToDoc,
2843
3392
  DEFAULT_THEME as DEFAULT_THEME2,
2844
- deriveTemplateInputs
3393
+ deriveTemplateInputs,
3394
+ isTemplateBlock as isTemplateBlock2
2845
3395
  } from "@bendyline/squisq/doc";
2846
3396
  import { extractPlainText, parseMarkdown } from "@bendyline/squisq/markdown";
2847
3397
 
@@ -2853,7 +3403,7 @@ import {
2853
3403
  } from "@bendyline/squisq/markdown";
2854
3404
 
2855
3405
  // src/InlineVideoPlayer.tsx
2856
- import { jsx as jsx16 } from "react/jsx-runtime";
3406
+ import { jsx as jsx17 } from "react/jsx-runtime";
2857
3407
  function InlineVideoPlayer({
2858
3408
  src,
2859
3409
  basePath = "",
@@ -2868,7 +3418,7 @@ function InlineVideoPlayer({
2868
3418
  const resolvedPoster = useMediaUrl(poster ?? "", basePath);
2869
3419
  const posterUrl = poster ? resolvedPoster : void 0;
2870
3420
  if (!resolvedSrc) return null;
2871
- return /* @__PURE__ */ jsx16("span", { className: `squisq-inline-video-player ${className ?? ""}`.trim(), children: /* @__PURE__ */ jsx16(
3421
+ return /* @__PURE__ */ jsx17("span", { className: `squisq-inline-video-player ${className ?? ""}`.trim(), children: /* @__PURE__ */ jsx17(
2872
3422
  "video",
2873
3423
  {
2874
3424
  src: resolvedSrc,
@@ -2883,7 +3433,7 @@ function InlineVideoPlayer({
2883
3433
  }
2884
3434
 
2885
3435
  // src/InlineAudioPlayer.tsx
2886
- import { jsx as jsx17 } from "react/jsx-runtime";
3436
+ import { jsx as jsx18 } from "react/jsx-runtime";
2887
3437
  function InlineAudioPlayer({
2888
3438
  src,
2889
3439
  basePath = "",
@@ -2893,11 +3443,11 @@ function InlineAudioPlayer({
2893
3443
  }) {
2894
3444
  const resolvedSrc = useMediaUrl(src, basePath);
2895
3445
  if (!resolvedSrc) return null;
2896
- return /* @__PURE__ */ jsx17("span", { className: `squisq-inline-audio-player ${className ?? ""}`.trim(), children: /* @__PURE__ */ jsx17("audio", { src: resolvedSrc, controls, preload }) });
3446
+ return /* @__PURE__ */ jsx18("span", { className: `squisq-inline-audio-player ${className ?? ""}`.trim(), children: /* @__PURE__ */ jsx18("audio", { src: resolvedSrc, controls, preload }) });
2897
3447
  }
2898
3448
 
2899
3449
  // src/MarkdownRenderer.tsx
2900
- import { jsx as jsx18, jsxs as jsxs11 } from "react/jsx-runtime";
3450
+ import { jsx as jsx19, jsxs as jsxs12 } from "react/jsx-runtime";
2901
3451
  var DEFAULT_CTX = { htmlPolicy: "sanitize" };
2902
3452
  function renderInline(nodes, keyPrefix = "", ctx = DEFAULT_CTX) {
2903
3453
  return nodes.map((node, i) => {
@@ -2905,28 +3455,28 @@ function renderInline(nodes, keyPrefix = "", ctx = DEFAULT_CTX) {
2905
3455
  switch (node.type) {
2906
3456
  case "text": {
2907
3457
  if (!node.value.includes("\n")) {
2908
- return /* @__PURE__ */ jsx18(Fragment2, { children: node.value }, key);
3458
+ return /* @__PURE__ */ jsx19(Fragment2, { children: node.value }, key);
2909
3459
  }
2910
3460
  const parts = node.value.split("\n");
2911
- return /* @__PURE__ */ jsx18(Fragment2, { children: parts.map((part, j) => /* @__PURE__ */ jsxs11(Fragment2, { children: [
2912
- j > 0 && /* @__PURE__ */ jsx18("br", {}),
3461
+ return /* @__PURE__ */ jsx19(Fragment2, { children: parts.map((part, j) => /* @__PURE__ */ jsxs12(Fragment2, { children: [
3462
+ j > 0 && /* @__PURE__ */ jsx19("br", {}),
2913
3463
  part
2914
3464
  ] }, j)) }, key);
2915
3465
  }
2916
3466
  case "emphasis":
2917
- return /* @__PURE__ */ jsx18("em", { className: "squisq-md-em", children: renderInline(node.children, key, ctx) }, key);
3467
+ return /* @__PURE__ */ jsx19("em", { className: "squisq-md-em", children: renderInline(node.children, key, ctx) }, key);
2918
3468
  case "strong":
2919
- return /* @__PURE__ */ jsx18("strong", { className: "squisq-md-strong", children: renderInline(node.children, key, ctx) }, key);
3469
+ return /* @__PURE__ */ jsx19("strong", { className: "squisq-md-strong", children: renderInline(node.children, key, ctx) }, key);
2920
3470
  case "delete":
2921
- return /* @__PURE__ */ jsx18("del", { className: "squisq-md-del", children: renderInline(node.children, key, ctx) }, key);
3471
+ return /* @__PURE__ */ jsx19("del", { className: "squisq-md-del", children: renderInline(node.children, key, ctx) }, key);
2922
3472
  case "inlineCode":
2923
- return /* @__PURE__ */ jsx18("code", { className: "squisq-md-inline-code", children: node.value }, key);
3473
+ return /* @__PURE__ */ jsx19("code", { className: "squisq-md-inline-code", children: node.value }, key);
2924
3474
  case "link": {
2925
3475
  const href = sanitizeUrl(node.url, "link", { extraLinkSchemes: ctx.linkSchemes });
2926
3476
  if (!href) {
2927
- return /* @__PURE__ */ jsx18("span", { className: "squisq-md-link squisq-md-link--blocked", children: renderInline(node.children, key, ctx) }, key);
3477
+ return /* @__PURE__ */ jsx19("span", { className: "squisq-md-link squisq-md-link--blocked", children: renderInline(node.children, key, ctx) }, key);
2928
3478
  }
2929
- return /* @__PURE__ */ jsx18(
3479
+ return /* @__PURE__ */ jsx19(
2930
3480
  "a",
2931
3481
  {
2932
3482
  className: "squisq-md-link",
@@ -2940,42 +3490,32 @@ function renderInline(nodes, keyPrefix = "", ctx = DEFAULT_CTX) {
2940
3490
  );
2941
3491
  }
2942
3492
  case "image":
2943
- return /* @__PURE__ */ jsx18(MdImage, { src: node.url, alt: node.alt ?? "", title: node.title ?? void 0 }, key);
3493
+ return /* @__PURE__ */ jsx19(MdImage, { src: node.url, alt: node.alt ?? "", title: node.title ?? void 0 }, key);
2944
3494
  case "break":
2945
- return /* @__PURE__ */ jsx18("br", {}, key);
3495
+ return /* @__PURE__ */ jsx19("br", {}, key);
2946
3496
  case "inlineMath":
2947
- return /* @__PURE__ */ jsx18("code", { className: "squisq-md-inline-math", children: node.value }, key);
3497
+ return /* @__PURE__ */ jsx19("code", { className: "squisq-md-inline-math", children: node.value }, key);
2948
3498
  case "htmlInline":
2949
3499
  if (ctx.htmlPolicy === "strip") return null;
2950
- if (ctx.htmlPolicy === "trusted" && !containsMediaTag(node.htmlChildren) && !containsDangerousTag(node.htmlChildren) && !hasDangerousRawHtml(node.rawHtml)) {
2951
- return /* @__PURE__ */ jsx18(
2952
- "span",
2953
- {
2954
- className: "squisq-md-html-inline",
2955
- dangerouslySetInnerHTML: { __html: node.rawHtml }
2956
- },
2957
- key
2958
- );
2959
- }
2960
- return /* @__PURE__ */ jsx18("span", { className: "squisq-md-html-inline", children: renderHtmlNodes(resolveHtmlNodes(node.htmlChildren, ctx.htmlPolicy), `${key}h`) }, key);
3500
+ return /* @__PURE__ */ jsx19("span", { className: "squisq-md-html-inline", children: renderHtmlNodes(resolveHtmlNodes(node.htmlChildren, ctx.htmlPolicy), `${key}h`, ctx) }, key);
2961
3501
  case "footnoteReference":
2962
- return /* @__PURE__ */ jsx18("sup", { className: "squisq-md-footnote-ref", children: /* @__PURE__ */ jsxs11("a", { href: `#fn-${node.identifier}`, children: [
3502
+ return /* @__PURE__ */ jsx19("sup", { className: "squisq-md-footnote-ref", children: /* @__PURE__ */ jsxs12("a", { href: `#fn-${node.identifier}`, children: [
2963
3503
  "[",
2964
3504
  node.label ?? node.identifier,
2965
3505
  "]"
2966
3506
  ] }) }, key);
2967
3507
  case "linkReference":
2968
- return /* @__PURE__ */ jsx18("span", { className: "squisq-md-link-ref", children: renderInline(node.children, key, ctx) }, key);
3508
+ return /* @__PURE__ */ jsx19("span", { className: "squisq-md-link-ref", children: renderInline(node.children, key, ctx) }, key);
2969
3509
  case "imageReference":
2970
- return /* @__PURE__ */ jsxs11("span", { className: "squisq-md-image-ref", children: [
3510
+ return /* @__PURE__ */ jsxs12("span", { className: "squisq-md-image-ref", children: [
2971
3511
  "[",
2972
3512
  node.alt ?? node.identifier,
2973
3513
  "]"
2974
3514
  ] }, key);
2975
3515
  case "textDirective":
2976
- return /* @__PURE__ */ jsx18("span", { className: "squisq-md-text-directive", "data-directive": node.name, children: renderInline(node.children, key, ctx) }, key);
3516
+ return /* @__PURE__ */ jsx19("span", { className: "squisq-md-text-directive", "data-directive": node.name, children: renderInline(node.children, key, ctx) }, key);
2977
3517
  case "mention":
2978
- return /* @__PURE__ */ jsxs11(
3518
+ return /* @__PURE__ */ jsxs12(
2979
3519
  "span",
2980
3520
  {
2981
3521
  className: "squisq-md-mention mention",
@@ -2998,61 +3538,51 @@ function renderInline(nodes, keyPrefix = "", ctx = DEFAULT_CTX) {
2998
3538
  function renderBlock(node, key, ctx = DEFAULT_CTX) {
2999
3539
  switch (node.type) {
3000
3540
  case "paragraph":
3001
- return /* @__PURE__ */ jsx18("p", { className: "squisq-md-p", children: renderInline(node.children, key, ctx) }, key);
3541
+ return /* @__PURE__ */ jsx19("p", { className: "squisq-md-p", children: renderInline(node.children, key, ctx) }, key);
3002
3542
  case "heading": {
3003
3543
  const Tag = `h${node.depth}`;
3004
- return /* @__PURE__ */ jsx18(Tag, { className: `squisq-md-heading squisq-md-h${node.depth}`, children: renderInline(node.children, key, ctx) }, key);
3544
+ return /* @__PURE__ */ jsx19(Tag, { className: `squisq-md-heading squisq-md-h${node.depth}`, children: renderInline(node.children, key, ctx) }, key);
3005
3545
  }
3006
3546
  case "blockquote":
3007
- return /* @__PURE__ */ jsx18("blockquote", { className: "squisq-md-blockquote", children: renderBlocks(node.children, key, ctx) }, key);
3547
+ return /* @__PURE__ */ jsx19("blockquote", { className: "squisq-md-blockquote", children: renderBlocks(node.children, key, ctx) }, key);
3008
3548
  case "list":
3009
3549
  if (node.ordered) {
3010
- return /* @__PURE__ */ jsx18("ol", { className: "squisq-md-list squisq-md-ol", start: node.start ?? void 0, children: node.children.map((item, i) => renderListItem(item, `${key}li${i}`, ctx)) }, key);
3550
+ return /* @__PURE__ */ jsx19("ol", { className: "squisq-md-list squisq-md-ol", start: node.start ?? void 0, children: node.children.map((item, i) => renderListItem(item, `${key}li${i}`, ctx)) }, key);
3011
3551
  }
3012
- return /* @__PURE__ */ jsx18("ul", { className: "squisq-md-list squisq-md-ul", children: node.children.map((item, i) => renderListItem(item, `${key}li${i}`, ctx)) }, key);
3552
+ return /* @__PURE__ */ jsx19("ul", { className: "squisq-md-list squisq-md-ul", children: node.children.map((item, i) => renderListItem(item, `${key}li${i}`, ctx)) }, key);
3013
3553
  case "code":
3014
- return /* @__PURE__ */ jsx18("pre", { className: "squisq-md-code-block", children: /* @__PURE__ */ jsx18("code", { className: node.lang ? `language-${node.lang}` : void 0, children: node.value }) }, key);
3554
+ return /* @__PURE__ */ jsx19("pre", { className: "squisq-md-code-block", children: /* @__PURE__ */ jsx19("code", { className: node.lang ? `language-${node.lang}` : void 0, children: node.value }) }, key);
3015
3555
  case "thematicBreak":
3016
- return /* @__PURE__ */ jsx18("hr", { className: "squisq-md-hr" }, key);
3556
+ return /* @__PURE__ */ jsx19("hr", { className: "squisq-md-hr" }, key);
3017
3557
  case "table":
3018
3558
  return renderTable(node.children, node.align, key, ctx);
3019
3559
  case "htmlBlock":
3020
3560
  if (ctx.htmlPolicy === "strip") return null;
3021
- if (ctx.htmlPolicy === "trusted" && !containsMediaTag(node.htmlChildren) && !containsDangerousTag(node.htmlChildren) && !hasDangerousRawHtml(node.rawHtml)) {
3022
- return /* @__PURE__ */ jsx18(
3023
- "div",
3024
- {
3025
- className: "squisq-md-html-block",
3026
- dangerouslySetInnerHTML: { __html: node.rawHtml }
3027
- },
3028
- key
3029
- );
3030
- }
3031
- return /* @__PURE__ */ jsx18("div", { className: "squisq-md-html-block", children: renderHtmlNodes(resolveHtmlNodes(node.htmlChildren, ctx.htmlPolicy), `${key}h`) }, key);
3561
+ return /* @__PURE__ */ jsx19("div", { className: "squisq-md-html-block", children: renderHtmlNodes(resolveHtmlNodes(node.htmlChildren, ctx.htmlPolicy), `${key}h`, ctx) }, key);
3032
3562
  case "math":
3033
- return /* @__PURE__ */ jsx18("pre", { className: "squisq-md-math-block", children: /* @__PURE__ */ jsx18("code", { children: node.value }) }, key);
3563
+ return /* @__PURE__ */ jsx19("pre", { className: "squisq-md-math-block", children: /* @__PURE__ */ jsx19("code", { children: node.value }) }, key);
3034
3564
  case "definition":
3035
3565
  return null;
3036
3566
  case "footnoteDefinition":
3037
- return /* @__PURE__ */ jsxs11("div", { className: "squisq-md-footnote-def", id: `fn-${node.identifier}`, children: [
3038
- /* @__PURE__ */ jsx18("sup", { children: node.label ?? node.identifier }),
3567
+ return /* @__PURE__ */ jsxs12("div", { className: "squisq-md-footnote-def", id: `fn-${node.identifier}`, children: [
3568
+ /* @__PURE__ */ jsx19("sup", { children: node.label ?? node.identifier }),
3039
3569
  renderBlocks(node.children, key, ctx)
3040
3570
  ] }, key);
3041
3571
  case "containerDirective":
3042
- return /* @__PURE__ */ jsxs11(
3572
+ return /* @__PURE__ */ jsxs12(
3043
3573
  "div",
3044
3574
  {
3045
3575
  className: `squisq-md-directive squisq-md-directive-${node.name}`,
3046
3576
  "data-directive": node.name,
3047
3577
  children: [
3048
- node.label && /* @__PURE__ */ jsx18("div", { className: "squisq-md-directive-label", children: node.label }),
3578
+ node.label && /* @__PURE__ */ jsx19("div", { className: "squisq-md-directive-label", children: node.label }),
3049
3579
  renderBlocks(node.children, key, ctx)
3050
3580
  ]
3051
3581
  },
3052
3582
  key
3053
3583
  );
3054
3584
  case "leafDirective":
3055
- return /* @__PURE__ */ jsx18(
3585
+ return /* @__PURE__ */ jsx19(
3056
3586
  "div",
3057
3587
  {
3058
3588
  className: `squisq-md-directive squisq-md-directive-${node.name}`,
@@ -3062,11 +3592,11 @@ function renderBlock(node, key, ctx = DEFAULT_CTX) {
3062
3592
  key
3063
3593
  );
3064
3594
  case "definitionList":
3065
- return /* @__PURE__ */ jsx18("dl", { className: "squisq-md-dl", children: node.children.map((child, i) => {
3595
+ return /* @__PURE__ */ jsx19("dl", { className: "squisq-md-dl", children: node.children.map((child, i) => {
3066
3596
  if (child.type === "definitionTerm") {
3067
- return /* @__PURE__ */ jsx18("dt", { className: "squisq-md-dt", children: renderInline(child.children, `${key}dt${i}`, ctx) }, `${key}dt${i}`);
3597
+ return /* @__PURE__ */ jsx19("dt", { className: "squisq-md-dt", children: renderInline(child.children, `${key}dt${i}`, ctx) }, `${key}dt${i}`);
3068
3598
  }
3069
- return /* @__PURE__ */ jsx18("dd", { className: "squisq-md-dd", children: renderBlocks(child.children, `${key}dd${i}`, ctx) }, `${key}dd${i}`);
3599
+ return /* @__PURE__ */ jsx19("dd", { className: "squisq-md-dd", children: renderBlocks(child.children, `${key}dd${i}`, ctx) }, `${key}dd${i}`);
3070
3600
  }) }, key);
3071
3601
  default:
3072
3602
  return null;
@@ -3074,15 +3604,15 @@ function renderBlock(node, key, ctx = DEFAULT_CTX) {
3074
3604
  }
3075
3605
  function renderListItem(item, key, ctx = DEFAULT_CTX) {
3076
3606
  const isTask = item.checked !== null && item.checked !== void 0;
3077
- return /* @__PURE__ */ jsxs11("li", { className: `squisq-md-li${isTask ? " squisq-md-task" : ""}`, children: [
3078
- isTask && /* @__PURE__ */ jsx18("input", { type: "checkbox", checked: !!item.checked, readOnly: true, className: "squisq-md-checkbox" }),
3607
+ return /* @__PURE__ */ jsxs12("li", { className: `squisq-md-li${isTask ? " squisq-md-task" : ""}`, children: [
3608
+ isTask && /* @__PURE__ */ jsx19("input", { type: "checkbox", checked: !!item.checked, readOnly: true, className: "squisq-md-checkbox" }),
3079
3609
  renderBlocks(item.children, key, ctx)
3080
3610
  ] }, key);
3081
3611
  }
3082
3612
  function renderTable(rows, align, key, ctx = DEFAULT_CTX) {
3083
3613
  const [headerRow, ...bodyRows] = rows;
3084
- return /* @__PURE__ */ jsxs11("table", { className: "squisq-md-table", children: [
3085
- headerRow && /* @__PURE__ */ jsx18("thead", { children: /* @__PURE__ */ jsx18("tr", { children: headerRow.children.map((cell, ci) => /* @__PURE__ */ jsx18(
3614
+ return /* @__PURE__ */ jsxs12("table", { className: "squisq-md-table", children: [
3615
+ headerRow && /* @__PURE__ */ jsx19("thead", { children: /* @__PURE__ */ jsx19("tr", { children: headerRow.children.map((cell, ci) => /* @__PURE__ */ jsx19(
3086
3616
  "th",
3087
3617
  {
3088
3618
  className: "squisq-md-th",
@@ -3091,7 +3621,7 @@ function renderTable(rows, align, key, ctx = DEFAULT_CTX) {
3091
3621
  },
3092
3622
  `${key}th${ci}`
3093
3623
  )) }) }),
3094
- bodyRows.length > 0 && /* @__PURE__ */ jsx18("tbody", { children: bodyRows.map((row, ri) => /* @__PURE__ */ jsx18("tr", { children: row.children.map((cell, ci) => /* @__PURE__ */ jsx18(
3624
+ bodyRows.length > 0 && /* @__PURE__ */ jsx19("tbody", { children: bodyRows.map((row, ri) => /* @__PURE__ */ jsx19("tr", { children: row.children.map((cell, ci) => /* @__PURE__ */ jsx19(
3095
3625
  "td",
3096
3626
  {
3097
3627
  className: "squisq-md-td",
@@ -3109,22 +3639,13 @@ function MdImage({ src, alt, title }) {
3109
3639
  const safeSrc = sanitizeUrl(src, "media");
3110
3640
  const resolved = useMediaUrl(safeSrc ?? "", ".");
3111
3641
  if (!safeSrc) return null;
3112
- return /* @__PURE__ */ jsx18("img", { className: "squisq-md-image", src: resolved, alt, title });
3642
+ return /* @__PURE__ */ jsx19("img", { className: "squisq-md-image", src: resolved, alt, title });
3113
3643
  }
3114
3644
  function resolveHtmlNodes(nodes, htmlPolicy) {
3115
3645
  if (htmlPolicy === "strip") return [];
3116
3646
  if (htmlPolicy === "trusted") return nodes;
3117
3647
  return sanitizeHtmlNodes2(nodes);
3118
3648
  }
3119
- function containsMediaTag(nodes) {
3120
- for (const node of nodes) {
3121
- if (node.type !== "htmlElement") continue;
3122
- const tagName = node.tagName.toLowerCase();
3123
- if (tagName === "video" || tagName === "audio") return true;
3124
- if (containsMediaTag(node.children)) return true;
3125
- }
3126
- return false;
3127
- }
3128
3649
  var DANGEROUS_HTML_TAGS = /* @__PURE__ */ new Set([
3129
3650
  "base",
3130
3651
  "embed",
@@ -3136,18 +3657,6 @@ var DANGEROUS_HTML_TAGS = /* @__PURE__ */ new Set([
3136
3657
  "style",
3137
3658
  "title"
3138
3659
  ]);
3139
- function containsDangerousTag(nodes) {
3140
- for (const node of nodes) {
3141
- if (node.type !== "htmlElement") continue;
3142
- if (DANGEROUS_HTML_TAGS.has(node.tagName.toLowerCase())) return true;
3143
- if (containsDangerousTag(node.children)) return true;
3144
- }
3145
- return false;
3146
- }
3147
- var DANGEROUS_RAW_HTML_RE = /<\s*\/?\s*(?:base|embed|iframe|link|meta|object|script|style|title)\b/i;
3148
- function hasDangerousRawHtml(rawHtml) {
3149
- return DANGEROUS_RAW_HTML_RE.test(rawHtml);
3150
- }
3151
3660
  var PASSTHROUGH_ATTRS = {
3152
3661
  // common
3153
3662
  class: "className",
@@ -3166,7 +3675,7 @@ var PASSTHROUGH_ATTRS = {
3166
3675
  target: "target",
3167
3676
  rel: "rel"
3168
3677
  };
3169
- function reactPropsFromAttrs(attrs) {
3678
+ function reactPropsFromAttrs(attrs, ctx) {
3170
3679
  const out = {};
3171
3680
  for (const [name, value] of Object.entries(attrs)) {
3172
3681
  const propName = PASSTHROUGH_ATTRS[name];
@@ -3175,21 +3684,33 @@ function reactPropsFromAttrs(attrs) {
3175
3684
  out["data-style"] = value;
3176
3685
  continue;
3177
3686
  }
3687
+ if (propName === "href") {
3688
+ const href = sanitizeUrl(value, "link", { extraLinkSchemes: ctx.linkSchemes });
3689
+ if (href) out.href = href;
3690
+ continue;
3691
+ }
3692
+ if (propName === "src") {
3693
+ const src = sanitizeUrl(value, "media");
3694
+ if (src) out.src = src;
3695
+ continue;
3696
+ }
3178
3697
  out[propName] = value;
3179
3698
  }
3180
3699
  return out;
3181
3700
  }
3182
- function renderHtmlElement(el, key) {
3701
+ function renderHtmlElement(el, key, ctx) {
3183
3702
  const tagName = el.tagName.toLowerCase();
3184
3703
  if (DANGEROUS_HTML_TAGS.has(tagName)) return null;
3185
3704
  if (tagName === "video") {
3186
- return /* @__PURE__ */ jsx18(
3705
+ const src = sanitizeUrl(el.attributes.src ?? "", "media") ?? "";
3706
+ const poster = sanitizeUrl(el.attributes.poster ?? "", "media") ?? void 0;
3707
+ return /* @__PURE__ */ jsx19(
3187
3708
  InlineVideoPlayer,
3188
3709
  {
3189
- src: el.attributes.src ?? "",
3710
+ src,
3190
3711
  width: el.attributes.width,
3191
3712
  height: el.attributes.height,
3192
- poster: el.attributes.poster,
3713
+ poster,
3193
3714
  controls: "controls" in el.attributes,
3194
3715
  preload: el.attributes.preload === "none" || el.attributes.preload === "metadata" || el.attributes.preload === "auto" ? el.attributes.preload : void 0
3195
3716
  },
@@ -3197,10 +3718,11 @@ function renderHtmlElement(el, key) {
3197
3718
  );
3198
3719
  }
3199
3720
  if (tagName === "audio") {
3200
- return /* @__PURE__ */ jsx18(
3721
+ const src = sanitizeUrl(el.attributes.src ?? "", "media") ?? "";
3722
+ return /* @__PURE__ */ jsx19(
3201
3723
  InlineAudioPlayer,
3202
3724
  {
3203
- src: el.attributes.src ?? "",
3725
+ src,
3204
3726
  controls: "controls" in el.attributes,
3205
3727
  preload: el.attributes.preload === "none" || el.attributes.preload === "metadata" || el.attributes.preload === "auto" ? el.attributes.preload : void 0
3206
3728
  },
@@ -3208,20 +3730,20 @@ function renderHtmlElement(el, key) {
3208
3730
  );
3209
3731
  }
3210
3732
  const Tag = tagName;
3211
- const props = reactPropsFromAttrs(el.attributes);
3733
+ const props = reactPropsFromAttrs(el.attributes, ctx);
3212
3734
  if (el.selfClosing) {
3213
- return /* @__PURE__ */ jsx18(Tag, { ...props }, key);
3735
+ return /* @__PURE__ */ jsx19(Tag, { ...props }, key);
3214
3736
  }
3215
- return /* @__PURE__ */ jsx18(Tag, { ...props, children: renderHtmlNodes(el.children, `${key}c`) }, key);
3737
+ return /* @__PURE__ */ jsx19(Tag, { ...props, children: renderHtmlNodes(el.children, `${key}c`, ctx) }, key);
3216
3738
  }
3217
- function renderHtmlNodes(nodes, keyPrefix) {
3739
+ function renderHtmlNodes(nodes, keyPrefix, ctx = DEFAULT_CTX) {
3218
3740
  return nodes.map((node, i) => {
3219
3741
  const key = `${keyPrefix}${i}`;
3220
3742
  switch (node.type) {
3221
3743
  case "htmlElement":
3222
- return renderHtmlElement(node, key);
3744
+ return renderHtmlElement(node, key, ctx);
3223
3745
  case "htmlText":
3224
- return /* @__PURE__ */ jsx18(Fragment2, { children: node.value }, key);
3746
+ return /* @__PURE__ */ jsx19(Fragment2, { children: node.value }, key);
3225
3747
  case "htmlComment":
3226
3748
  return null;
3227
3749
  default:
@@ -3236,25 +3758,16 @@ function MarkdownRenderer({
3236
3758
  linkSchemes
3237
3759
  }) {
3238
3760
  if (!nodes || nodes.length === 0) return null;
3239
- return /* @__PURE__ */ jsx18("div", { className: `squisq-md ${className || ""}`, children: renderBlocks(nodes, "", { htmlPolicy, linkSchemes }) });
3761
+ return /* @__PURE__ */ jsx19("div", { className: `squisq-md ${className || ""}`, children: renderBlocks(nodes, "", { htmlPolicy, linkSchemes }) });
3240
3762
  }
3241
3763
 
3242
3764
  // src/LinearDocView.tsx
3243
- import { jsx as jsx19, jsxs as jsxs12 } from "react/jsx-runtime";
3244
- var warnedUnknownTemplates = /* @__PURE__ */ new Set();
3765
+ import { jsx as jsx20, jsxs as jsxs13 } from "react/jsx-runtime";
3245
3766
  function isAnnotatedBlock(block) {
3246
- const annotation = block.sourceHeading?.templateAnnotation;
3247
- if (!annotation?.template) return false;
3248
- if (!hasTemplate(annotation.template)) {
3249
- if (!warnedUnknownTemplates.has(annotation.template)) {
3250
- warnedUnknownTemplates.add(annotation.template);
3251
- console.warn(
3252
- `[squisq] Unknown template "${annotation.template}" \u2014 rendering the block as plain markdown.`
3253
- );
3254
- }
3255
- return false;
3256
- }
3257
- return true;
3767
+ return !!block.sourceHeading?.templateAnnotation?.template || !block.sourceHeading && isTemplateBlock2(block);
3768
+ }
3769
+ function visualTemplateName(block) {
3770
+ return block.sourceHeading?.templateAnnotation?.template ?? block.template;
3258
3771
  }
3259
3772
  function countAll(blocks) {
3260
3773
  let count = 0;
@@ -3264,50 +3777,65 @@ function countAll(blocks) {
3264
3777
  }
3265
3778
  return count;
3266
3779
  }
3267
- function BlockSection({ block, basePath, viewport, renderContext, blockIndex }) {
3780
+ function BlockSection({
3781
+ block,
3782
+ basePath,
3783
+ viewport,
3784
+ renderContext,
3785
+ blockIndex,
3786
+ blockIndices,
3787
+ animationsEnabled
3788
+ }) {
3268
3789
  const isAnnotated = isAnnotatedBlock(block);
3269
3790
  const visualBlock = useMemo8(() => {
3270
3791
  if (!isAnnotated) return null;
3271
- const annotation = block.sourceHeading.templateAnnotation;
3272
- const headingText = extractPlainText(block.sourceHeading);
3273
- const templateBlock = {
3274
- id: block.id,
3275
- template: annotation.template,
3276
- startTime: 0,
3277
- duration: 1,
3278
- audioSegment: 0,
3279
- title: headingText,
3280
- ...deriveTemplateInputs(
3281
- annotation.template ?? "sectionHeader",
3282
- headingText,
3283
- block.contents,
3284
- {
3792
+ const annotation = block.sourceHeading?.templateAnnotation;
3793
+ const templateName = visualTemplateName(block) ?? "sectionHeader";
3794
+ const templateBlock = annotation ? (() => {
3795
+ const headingText = extractPlainText(block.sourceHeading);
3796
+ return {
3797
+ id: block.id,
3798
+ template: templateName,
3799
+ startTime: 0,
3800
+ duration: 1,
3801
+ audioSegment: 0,
3802
+ title: headingText,
3803
+ contents: block.contents,
3804
+ children: block.children,
3805
+ ...deriveTemplateInputs(templateName, headingText, block.contents, {
3285
3806
  placeholders: true
3286
- }
3287
- ) ?? {},
3288
- ...annotation.params,
3289
- ...block.templateOverrides
3807
+ }) ?? {},
3808
+ ...annotation.params,
3809
+ ...block.templateOverrides
3810
+ };
3811
+ })() : {
3812
+ ...block,
3813
+ startTime: block.startTime ?? 0,
3814
+ duration: block.duration ?? 1,
3815
+ audioSegment: block.audioSegment ?? 0,
3816
+ template: templateName
3290
3817
  };
3291
3818
  const ctx = {
3292
3819
  ...renderContext,
3293
3820
  blockIndex
3294
3821
  };
3295
- const layers = getLayers(templateBlock, ctx);
3822
+ const { layers } = materializeBlockLayers(templateBlock, ctx);
3296
3823
  return {
3297
3824
  ...block,
3298
3825
  layers,
3299
- template: annotation.template
3826
+ template: templateName
3300
3827
  };
3301
3828
  }, [block, isAnnotated, renderContext, blockIndex]);
3302
- return /* @__PURE__ */ jsxs12(
3829
+ return /* @__PURE__ */ jsxs13(
3303
3830
  "div",
3304
3831
  {
3305
3832
  className: "squisq-linear-section",
3306
3833
  "data-block-id": block.id,
3307
- "data-template": isAnnotated ? block.sourceHeading?.templateAnnotation?.template : void 0,
3834
+ "data-block-index": blockIndex,
3835
+ "data-template": isAnnotated ? visualTemplateName(block) : void 0,
3308
3836
  children: [
3309
- block.sourceHeading && !isAnnotated && /* @__PURE__ */ jsx19(MarkdownRenderer, { nodes: [block.sourceHeading] }),
3310
- isAnnotated && visualBlock && /* @__PURE__ */ jsx19("div", { className: "squisq-linear-card", children: /* @__PURE__ */ jsx19(
3837
+ block.sourceHeading && !isAnnotated && /* @__PURE__ */ jsx20(MarkdownRenderer, { nodes: [block.sourceHeading] }),
3838
+ isAnnotated && visualBlock && /* @__PURE__ */ jsx20("div", { className: "squisq-linear-card", children: /* @__PURE__ */ jsx20(
3311
3839
  "div",
3312
3840
  {
3313
3841
  className: "squisq-linear-card-svg",
@@ -3317,26 +3845,29 @@ function BlockSection({ block, basePath, viewport, renderContext, blockIndex })
3317
3845
  overflow: "hidden",
3318
3846
  marginBottom: "1em"
3319
3847
  },
3320
- children: /* @__PURE__ */ jsx19(
3848
+ children: /* @__PURE__ */ jsx20(
3321
3849
  BlockRenderer,
3322
3850
  {
3323
3851
  block: visualBlock,
3324
3852
  blockTime: 0,
3325
3853
  basePath,
3326
- viewport
3854
+ viewport,
3855
+ animationsEnabled
3327
3856
  }
3328
3857
  )
3329
3858
  }
3330
3859
  ) }),
3331
- !isAnnotated && block.contents && block.contents.length > 0 && /* @__PURE__ */ jsx19(MarkdownRenderer, { nodes: block.contents }),
3332
- block.children && block.children.length > 0 && /* @__PURE__ */ jsx19("div", { className: "squisq-linear-children", children: block.children.map((child, i) => /* @__PURE__ */ jsx19(
3860
+ !isAnnotated && block.contents && block.contents.length > 0 && /* @__PURE__ */ jsx20(MarkdownRenderer, { nodes: block.contents }),
3861
+ block.children && block.children.length > 0 && /* @__PURE__ */ jsx20("div", { className: "squisq-linear-children", children: block.children.map((child, i) => /* @__PURE__ */ jsx20(
3333
3862
  BlockSection,
3334
3863
  {
3335
3864
  block: child,
3336
3865
  basePath,
3337
3866
  viewport,
3338
3867
  renderContext,
3339
- blockIndex: blockIndex + i + 1
3868
+ blockIndex: blockIndices.get(child) ?? blockIndex + i + 1,
3869
+ blockIndices,
3870
+ animationsEnabled
3340
3871
  },
3341
3872
  child.id
3342
3873
  )) })
@@ -3352,9 +3883,12 @@ function LinearDocView({
3352
3883
  className,
3353
3884
  theme,
3354
3885
  surface,
3886
+ animationsEnabled = true,
3355
3887
  thinMargins = false,
3356
- imageDisplayMode = "inline"
3888
+ imageDisplayMode = "inline",
3889
+ globalKeyboardShortcuts = false
3357
3890
  }) {
3891
+ const scrollRef = useRef8(null);
3358
3892
  const activeViewport = viewport ?? VIEWPORT_PRESETS3.landscape;
3359
3893
  const markdownDoc = useMemo8(
3360
3894
  () => !doc && markdown !== void 0 ? markdownToDoc(parseMarkdown(markdown)) : void 0,
@@ -3365,6 +3899,18 @@ function LinearDocView({
3365
3899
  () => resolvedDoc ? countAll(resolvedDoc.blocks) : 0,
3366
3900
  [resolvedDoc]
3367
3901
  );
3902
+ const blockIndices = useMemo8(() => {
3903
+ const indices = /* @__PURE__ */ new Map();
3904
+ let index = 0;
3905
+ const visit = (blocks) => {
3906
+ for (const block of blocks) {
3907
+ indices.set(block, index++);
3908
+ if (block.children) visit(block.children);
3909
+ }
3910
+ };
3911
+ if (resolvedDoc) visit(resolvedDoc.blocks);
3912
+ return indices;
3913
+ }, [resolvedDoc]);
3368
3914
  const autoSurface = useAutoSurface(surface === "auto");
3369
3915
  const resolvedSurface = surface === "auto" ? autoSurface : surface;
3370
3916
  const renderContext = useMemo8(() => {
@@ -3376,12 +3922,37 @@ function LinearDocView({
3376
3922
  totalBlocks,
3377
3923
  // Theme atmosphere (vignette/grain/gradient persistent layers) shows
3378
3924
  // on the inline template cards so they match the player's look.
3379
- persistentLayers: effectiveTheme.persistentLayers
3925
+ persistentLayers: effectiveTheme.persistentLayers,
3926
+ customTemplates: resolvedDoc?.customTemplates
3380
3927
  };
3381
- }, [activeViewport, totalBlocks, theme, resolvedSurface]);
3928
+ }, [activeViewport, resolvedDoc?.customTemplates, totalBlocks, theme, resolvedSurface]);
3382
3929
  const activeTheme = renderContext.theme;
3930
+ useEffect9(() => {
3931
+ if (!globalKeyboardShortcuts) return;
3932
+ const handleKeyDown = (event) => {
3933
+ if (event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey || event.key !== "ArrowDown" && event.key !== "ArrowUp") {
3934
+ return;
3935
+ }
3936
+ const target = event.target instanceof Element ? event.target : null;
3937
+ if (target?.closest(
3938
+ 'input, textarea, select, [contenteditable]:not([contenteditable="false"]), [role="textbox"], [role="combobox"], [role="listbox"], [role="menu"], [role="dialog"], [aria-modal="true"], .monaco-editor'
3939
+ )) {
3940
+ return;
3941
+ }
3942
+ const scroller = scrollRef.current;
3943
+ if (!scroller) return;
3944
+ event.preventDefault();
3945
+ const distance = Math.max(64, Math.round(scroller.clientHeight * 0.12));
3946
+ scroller.scrollBy({
3947
+ top: event.key === "ArrowDown" ? distance : -distance,
3948
+ behavior: "smooth"
3949
+ });
3950
+ };
3951
+ document.addEventListener("keydown", handleKeyDown);
3952
+ return () => document.removeEventListener("keydown", handleKeyDown);
3953
+ }, [globalKeyboardShortcuts]);
3383
3954
  if (!resolvedDoc) {
3384
- return /* @__PURE__ */ jsx19("div", { className: `squisq-linear squisq-linear--empty ${className || ""}` });
3955
+ return /* @__PURE__ */ jsx20("div", { ref: scrollRef, className: `squisq-linear squisq-linear--empty ${className || ""}` });
3385
3956
  }
3386
3957
  const bgColor = activeTheme.colors.background;
3387
3958
  const textColor = activeTheme.colors.text;
@@ -3390,9 +3961,10 @@ function LinearDocView({
3390
3961
  const bodyFont = resolveFontFamily2(activeTheme.typography.bodyFont, "system-ui, sans-serif");
3391
3962
  const titleFont = resolveFontFamily2(activeTheme.typography.titleFont, "Georgia, serif");
3392
3963
  const lineHt = activeTheme.typography.lineHeight ?? 1.7;
3393
- return /* @__PURE__ */ jsx19(
3964
+ return /* @__PURE__ */ jsx20(
3394
3965
  "div",
3395
3966
  {
3967
+ ref: scrollRef,
3396
3968
  className: `squisq-linear ${className || ""}`,
3397
3969
  style: {
3398
3970
  width: "100%",
@@ -3406,7 +3978,7 @@ function LinearDocView({
3406
3978
  overflowX: "hidden",
3407
3979
  background: bgColor
3408
3980
  },
3409
- children: /* @__PURE__ */ jsxs12(
3981
+ children: /* @__PURE__ */ jsxs13(
3410
3982
  "div",
3411
3983
  {
3412
3984
  className: `squisq-linear-content squisq-md${thinMargins ? " squisq-linear-content--thin" : ""}${imageDisplayMode === "thumbnail" ? " squisq-linear-content--thumbnail-images" : ""}`,
@@ -3431,7 +4003,7 @@ function LinearDocView({
3431
4003
  "--squisq-linear-bg": bgColor
3432
4004
  },
3433
4005
  children: [
3434
- /* @__PURE__ */ jsx19("style", { children: `
4006
+ /* @__PURE__ */ jsx20("style", { children: `
3435
4007
  .squisq-linear-content h1,
3436
4008
  .squisq-linear-content h2,
3437
4009
  .squisq-linear-content h3,
@@ -3449,6 +4021,9 @@ function LinearDocView({
3449
4021
  .squisq-linear-content p {
3450
4022
  margin-bottom: 0.75em;
3451
4023
  }
4024
+ .squisq-linear-content p + p {
4025
+ margin-top: 1.25em;
4026
+ }
3452
4027
  .squisq-linear-content ul,
3453
4028
  .squisq-linear-content ol {
3454
4029
  padding-left: 2em;
@@ -3543,14 +4118,16 @@ function LinearDocView({
3543
4118
  background: color-mix(in srgb, var(--squisq-linear-primary) 8%, transparent);
3544
4119
  }
3545
4120
  ` }),
3546
- resolvedDoc.blocks.map((block, i) => /* @__PURE__ */ jsx19(
4121
+ resolvedDoc.blocks.map((block, i) => /* @__PURE__ */ jsx20(
3547
4122
  BlockSection,
3548
4123
  {
3549
4124
  block,
3550
4125
  basePath,
3551
4126
  viewport: activeViewport,
3552
4127
  renderContext,
3553
- blockIndex: i
4128
+ blockIndex: blockIndices.get(block) ?? i,
4129
+ blockIndices,
4130
+ animationsEnabled
3554
4131
  },
3555
4132
  block.id
3556
4133
  ))
@@ -3562,7 +4139,7 @@ function LinearDocView({
3562
4139
  }
3563
4140
 
3564
4141
  // src/DocPlayer.tsx
3565
- import { jsx as jsx20, jsxs as jsxs13 } from "react/jsx-runtime";
4142
+ import { jsx as jsx21, jsxs as jsxs14 } from "react/jsx-runtime";
3566
4143
  var SMALL_WORDS = /* @__PURE__ */ new Set([
3567
4144
  "a",
3568
4145
  "an",
@@ -3583,7 +4160,7 @@ var SMALL_WORDS = /* @__PURE__ */ new Set([
3583
4160
  function buildSegmentTitleMap(doc) {
3584
4161
  const map = /* @__PURE__ */ new Map();
3585
4162
  for (const block of doc.blocks) {
3586
- if (isTemplateBlock2(block) && block.template === "sectionHeader" && "title" in block) {
4163
+ if (isTemplateBlock3(block) && block.template === "sectionHeader" && "title" in block) {
3587
4164
  const segIdx = block.audioSegment;
3588
4165
  if (!map.has(segIdx)) {
3589
4166
  map.set(segIdx, block.title);
@@ -3624,14 +4201,15 @@ function DocPlayer(props) {
3624
4201
  );
3625
4202
  const resolvedDoc = doc ?? markdownDoc;
3626
4203
  if (!resolvedDoc) {
3627
- return /* @__PURE__ */ jsx20("div", { className: "doc-player doc-player--empty" });
4204
+ return /* @__PURE__ */ jsx21("div", { className: "doc-player doc-player--empty" });
3628
4205
  }
3629
- return /* @__PURE__ */ jsx20(DocPlayerContent, { ...props, doc: resolvedDoc });
4206
+ return /* @__PURE__ */ jsx21(DocPlayerContent, { ...props, doc: resolvedDoc });
3630
4207
  }
3631
4208
  function DocPlayerContent({
3632
4209
  doc,
3633
4210
  basePath = ".",
3634
4211
  renderMode = false,
4212
+ animationsEnabled = true,
3635
4213
  autoPlay = false,
3636
4214
  onEnded,
3637
4215
  onTimeUpdate,
@@ -3643,23 +4221,27 @@ function DocPlayerContent({
3643
4221
  onCaptionsToggle,
3644
4222
  onPlaybackStateChange,
3645
4223
  onControlsReady,
4224
+ onRenderAPIReady,
3646
4225
  isFullscreen = false,
3647
4226
  onFullscreenToggle,
3648
4227
  onBlockMarkers,
3649
4228
  forceViewport,
3650
4229
  displayMode = "video",
3651
4230
  showCoverSlide = true,
4231
+ coverVisible,
3652
4232
  theme,
3653
4233
  surface,
3654
4234
  captionStyle = "standard",
3655
- enableSwipe = true
4235
+ enableSwipe = true,
4236
+ globalKeyboardShortcuts = false
3656
4237
  }) {
3657
4238
  const isSlideshowMode = displayMode === "slideshow";
3658
4239
  const isLinearMode = displayMode === "linear";
3659
- const audioRef = useRef7(null);
3660
- const containerRef = useRef7(null);
3661
- const [tapFeedback, setTapFeedback] = useState7(null);
3662
- const tapFeedbackTimer = useRef7();
4240
+ const audioRef = useRef9(null);
4241
+ const containerRef = useRef9(null);
4242
+ const playerId = `squisq-player-${useId7().replace(/:/g, "")}`;
4243
+ const [tapFeedback, setTapFeedback] = useState9(null);
4244
+ const tapFeedbackTimer = useRef9();
3663
4245
  const { viewport, orientation } = useViewportOrientation();
3664
4246
  const activeViewport = forceViewport || (renderMode ? VIEWPORT_PRESETS4.landscape : viewport);
3665
4247
  const isDebugMode = useMemo9(() => {
@@ -3667,9 +4249,9 @@ function DocPlayerContent({
3667
4249
  const params = new URLSearchParams(window.location.search);
3668
4250
  return params.get("debug") === "true";
3669
4251
  }, []);
3670
- const internalAudio = useAudioSync(audioRef, doc.audio, basePath);
4252
+ const internalAudio = useAudioSync(audioRef, doc.audio, basePath, !externalAudioController);
3671
4253
  const audio = externalAudioController || internalAudio;
3672
- useEffect8(() => {
4254
+ useEffect10(() => {
3673
4255
  if (warnedMissingStyles || !isDevEnvironment()) return;
3674
4256
  const el = containerRef.current;
3675
4257
  if (!el || typeof getComputedStyle !== "function") return;
@@ -3698,21 +4280,28 @@ function DocPlayerContent({
3698
4280
  restart
3699
4281
  } = audio;
3700
4282
  const mediaSchedule = useMemo9(() => resolveMediaSchedule(doc), [doc]);
3701
- const currentTimeRef = useRef7(currentTime);
4283
+ const currentTimeRef = useRef9(currentTime);
3702
4284
  currentTimeRef.current = currentTime;
3703
- const totalDurationRef = useRef7(totalDuration);
4285
+ const totalDurationRef = useRef9(totalDuration);
3704
4286
  totalDurationRef.current = totalDuration;
3705
- const expandedBlocksLenRef = useRef7(0);
3706
- const handleContainerClick = useCallback6(
4287
+ const expandedBlocksLenRef = useRef9(0);
4288
+ const handleContainerClick = useCallback7(
3707
4289
  (e) => {
3708
- if (renderMode || isSlideshowMode || isLinearMode) return;
4290
+ if (renderMode || isLinearMode) return;
3709
4291
  const target = e.target;
4292
+ if (isSlideshowMode) {
4293
+ if (!target.closest('button, a, input, textarea, select, [contenteditable="true"]')) {
4294
+ containerRef.current?.focus({ preventScroll: true });
4295
+ }
4296
+ return;
4297
+ }
3710
4298
  if (target.closest(
3711
- "button, a, input, .doc-player__controls, .doc-player__scrubber, .doc-controls-sidebar, .doc-controls-slideshow"
4299
+ 'button, a, input, textarea, select, [contenteditable="true"], .doc-player__controls, .doc-player__scrubber, .doc-controls-sidebar, .doc-controls-slideshow'
3712
4300
  ))
3713
4301
  return;
4302
+ containerRef.current?.focus({ preventScroll: true });
3714
4303
  toggle();
3715
- const nextState = isPlaying ? "play" : "pause";
4304
+ const nextState = isPlaying ? "pause" : "play";
3716
4305
  setTapFeedback(nextState);
3717
4306
  clearTimeout(tapFeedbackTimer.current);
3718
4307
  tapFeedbackTimer.current = setTimeout(() => setTapFeedback(null), 600);
@@ -3736,8 +4325,13 @@ function DocPlayerContent({
3736
4325
  docProgress,
3737
4326
  nextBlock: _nextBlock,
3738
4327
  prevBlock: _prevBlock,
3739
- blocks: expandedBlocks
3740
- } = useDocPlayback(doc, currentTime, activeViewport, renderMode, effectiveTheme);
4328
+ blocks: expandedBlocks,
4329
+ suppressOutgoingForNextBlock
4330
+ } = useDocPlayback(doc, currentTime, {
4331
+ viewport: activeViewport,
4332
+ theme: effectiveTheme,
4333
+ onSeek: seekTo
4334
+ });
3741
4335
  const coverBlock = useMemo9(() => {
3742
4336
  const startBlockConfig = doc.startBlock;
3743
4337
  if (!showCoverSlide) return null;
@@ -3755,9 +4349,13 @@ function DocPlayerContent({
3755
4349
  };
3756
4350
  }, [doc.startBlock, activeViewport, effectiveTheme, showCoverSlide]);
3757
4351
  const hasManagedCover = !!coverBlock;
3758
- const [slideshowCoverVisible, setSlideshowCoverVisible] = useState7(false);
3759
- const slideshowCoverInitKeyRef = useRef7("");
3760
- useEffect8(() => {
4352
+ const [slideshowCoverVisible, setSlideshowCoverVisible] = useState9(false);
4353
+ const [isSlideshowPickerOpen, setIsSlideshowPickerOpen] = useState9(false);
4354
+ const slideshowCoverInitKeyRef = useRef9("");
4355
+ useEffect10(() => {
4356
+ slideshowCoverInitKeyRef.current = "";
4357
+ }, [doc]);
4358
+ useEffect10(() => {
3761
4359
  const initKey = `${isSlideshowMode}:${hasManagedCover}:${renderMode}`;
3762
4360
  if (slideshowCoverInitKeyRef.current === initKey) return;
3763
4361
  slideshowCoverInitKeyRef.current = initKey;
@@ -3768,14 +4366,21 @@ function DocPlayerContent({
3768
4366
  setSlideshowCoverVisible(false);
3769
4367
  }
3770
4368
  }, [isSlideshowMode, hasManagedCover, renderMode, pause]);
3771
- const [coverForced, setCoverForced] = useState7(false);
3772
- const [coverGraceActive, setCoverGraceActive] = useState7(false);
3773
- const coverGraceTimer = useRef7();
3774
- const coverWasShowing = useRef7(false);
3775
- const hasPlayedOnce = useRef7(false);
4369
+ const [coverForced, setCoverForced] = useState9(false);
4370
+ const [coverGraceActive, setCoverGraceActive] = useState9(false);
4371
+ const coverGraceTimer = useRef9();
4372
+ const coverWasShowing = useRef9(false);
4373
+ const hasPlayedOnce = useRef9(false);
4374
+ useEffect10(() => {
4375
+ hasPlayedOnce.current = false;
4376
+ coverWasShowing.current = false;
4377
+ clearTimeout(coverGraceTimer.current);
4378
+ setCoverGraceActive(false);
4379
+ setCoverForced(false);
4380
+ }, [doc]);
3776
4381
  const atRest = !!(coverBlock && !isSlideshowMode && !isPlaying && currentTime === 0 && !hasPlayedOnce.current && !renderMode && !autoPlay);
3777
4382
  if (atRest) coverWasShowing.current = true;
3778
- useEffect8(() => {
4383
+ useEffect10(() => {
3779
4384
  if (isPlaying && coverWasShowing.current && coverBlock && !renderMode && !isSlideshowMode) {
3780
4385
  coverWasShowing.current = false;
3781
4386
  hasPlayedOnce.current = true;
@@ -3783,88 +4388,96 @@ function DocPlayerContent({
3783
4388
  coverGraceTimer.current = setTimeout(() => setCoverGraceActive(false), 3e3);
3784
4389
  }
3785
4390
  }, [isPlaying, coverBlock, renderMode, isSlideshowMode]);
3786
- useEffect8(() => () => clearTimeout(coverGraceTimer.current), []);
4391
+ useEffect10(() => () => clearTimeout(coverGraceTimer.current), []);
3787
4392
  const showVideoCoverBlock = !isSlideshowMode && !isLinearMode && !!coverBlock && (coverForced || coverGraceActive || !isPlaying && currentTime === 0 && !hasPlayedOnce.current && !renderMode && !autoPlay);
3788
- const showSlideshowCover = !!(isSlideshowMode && !isLinearMode && !renderMode && coverBlock && slideshowCoverVisible);
3789
- const showCoverBlock = showVideoCoverBlock || showSlideshowCover;
4393
+ const effectiveSlideshowCoverVisible = coverVisible ?? slideshowCoverVisible;
4394
+ const showSlideshowCover = !!(isSlideshowMode && !isLinearMode && !renderMode && coverBlock && effectiveSlideshowCoverVisible);
4395
+ const showCoverBlock = coverVisible === void 0 ? showVideoCoverBlock || showSlideshowCover : !!coverBlock && coverVisible;
3790
4396
  const slideshowHasCover = !!(isSlideshowMode && !renderMode && coverBlock);
3791
- const slideshowSlideIndex = slideshowHasCover ? slideshowCoverVisible ? 0 : currentBlockIndex + 1 : currentBlockIndex;
4397
+ const slideshowSlideIndex = slideshowHasCover ? effectiveSlideshowCoverVisible ? 0 : currentBlockIndex + 1 : currentBlockIndex;
3792
4398
  const slideshowTotalSlides = slideshowHasCover ? expandedBlocks.length + 1 : expandedBlocks.length;
3793
- const hasAutoPlayed = useRef7(false);
3794
- useEffect8(() => {
4399
+ const hasAutoPlayed = useRef9(false);
4400
+ useEffect10(() => {
4401
+ hasAutoPlayed.current = false;
4402
+ }, [doc]);
4403
+ useEffect10(() => {
3795
4404
  if (isAudioReady && autoPlay && !hasAutoPlayed.current) {
3796
4405
  hasAutoPlayed.current = true;
3797
4406
  play();
3798
4407
  }
3799
4408
  }, [isAudioReady, autoPlay, play]);
3800
- useEffect8(() => {
4409
+ useEffect10(() => {
3801
4410
  onTimeUpdate?.(currentTime);
3802
4411
  }, [currentTime, onTimeUpdate]);
3803
- useEffect8(() => {
4412
+ useEffect10(() => {
3804
4413
  if (isEnded) {
3805
4414
  onEnded?.();
3806
4415
  }
3807
4416
  }, [isEnded, onEnded]);
3808
- useEffect8(() => {
3809
- if ((renderMode || isDebugMode) && typeof window !== "undefined") {
3810
- const w = window;
3811
- w.seekTo = (time) => {
3812
- seekTo(time);
3813
- return new Promise((resolve) => {
3814
- requestAnimationFrame(() => {
3815
- let blockStartTime = 0;
3816
- for (let i = expandedBlocks.length - 1; i >= 0; i--) {
3817
- if (time >= expandedBlocks[i].startTime) {
3818
- blockStartTime = expandedBlocks[i].startTime;
3819
- break;
3820
- }
4417
+ const liveRenderAPIRef = useRef9(null);
4418
+ const stableRenderAPIRef = useRef9(null);
4419
+ if (!stableRenderAPIRef.current) {
4420
+ const current = () => {
4421
+ const api = liveRenderAPIRef.current;
4422
+ if (!api) throw new Error("Squisq render API is not currently available.");
4423
+ return api;
4424
+ };
4425
+ stableRenderAPIRef.current = {
4426
+ seekTo: (time) => current().seekTo(time),
4427
+ getDuration: () => current().getDuration(),
4428
+ getBlocks: () => current().getBlocks(),
4429
+ getAudioSegments: () => current().getAudioSegments(),
4430
+ getCaptions: () => current().getCaptions(),
4431
+ getChapters: () => current().getChapters(),
4432
+ showCover: () => current().showCover(),
4433
+ hideCover: () => current().hideCover(),
4434
+ hasCoverBlock: () => current().hasCoverBlock()
4435
+ };
4436
+ }
4437
+ const stableRenderAPI = stableRenderAPIRef.current;
4438
+ useEffect10(() => {
4439
+ if (!renderMode && !isDebugMode) {
4440
+ liveRenderAPIRef.current = null;
4441
+ return;
4442
+ }
4443
+ const root = containerRef.current;
4444
+ if (!root) {
4445
+ liveRenderAPIRef.current = null;
4446
+ return;
4447
+ }
4448
+ const renderSeekTo = (time) => {
4449
+ seekTo(time);
4450
+ return new Promise((resolve) => {
4451
+ requestAnimationFrame(() => {
4452
+ let blockStartTime = 0;
4453
+ for (let i = expandedBlocks.length - 1; i >= 0; i--) {
4454
+ if (time >= expandedBlocks[i].startTime) {
4455
+ blockStartTime = expandedBlocks[i].startTime;
4456
+ break;
3821
4457
  }
3822
- const elapsedMs = (time - blockStartTime) * 1e3;
3823
- document.getAnimations().forEach((anim) => {
3824
- const target = anim.effect?.target;
3825
- if (!target) return;
3826
- if (target.closest(".doc-player__block--active")) {
3827
- anim.currentTime = Math.max(0, elapsedMs);
3828
- } else if (target.closest(".doc-player__block--previous")) {
3829
- anim.currentTime = Math.max(0, elapsedMs);
3830
- }
3831
- });
3832
- const blockElapsed = time - blockStartTime;
3833
- const videoSeekPromises = [];
3834
- const activeBlockEl = document.querySelector(".doc-player__block--active");
3835
- if (activeBlockEl) {
3836
- const videos = activeBlockEl.querySelectorAll("video[data-clip-start]");
3837
- videos.forEach((el) => {
3838
- const video = el;
3839
- const clipStart = parseFloat(video.dataset.clipStart || "0");
3840
- const clipEnd = parseFloat(video.dataset.clipEnd || "0");
3841
- const startAt = parseFloat(video.dataset.startAt || "0");
3842
- const targetTime = Math.min(
3843
- clipStart + Math.max(0, blockElapsed - startAt),
3844
- clipEnd
3845
- );
3846
- video.pause();
3847
- video.currentTime = targetTime;
3848
- videoSeekPromises.push(
3849
- new Promise((r) => {
3850
- if (Math.abs(video.currentTime - targetTime) < 0.1) {
3851
- r();
3852
- } else {
3853
- video.addEventListener("seeked", () => r(), { once: true });
3854
- setTimeout(r, 200);
3855
- }
3856
- })
3857
- );
3858
- });
4458
+ }
4459
+ const elapsedMs = (time - blockStartTime) * 1e3;
4460
+ (root.getAnimations?.() ?? []).forEach((anim) => {
4461
+ const target = anim.effect?.target;
4462
+ if (!target) return;
4463
+ if (target.closest(".doc-player__block--active")) {
4464
+ anim.currentTime = Math.max(0, elapsedMs);
4465
+ } else if (target.closest(".doc-player__block--previous")) {
4466
+ anim.currentTime = Math.max(0, elapsedMs);
3859
4467
  }
3860
- document.querySelectorAll("video[data-clip-id]").forEach((el) => {
4468
+ });
4469
+ const blockElapsed = time - blockStartTime;
4470
+ const videoSeekPromises = [];
4471
+ const activeBlockEl = root.querySelector(".doc-player__block--active");
4472
+ if (activeBlockEl) {
4473
+ const videos = activeBlockEl.querySelectorAll("video[data-clip-start]");
4474
+ videos.forEach((el) => {
3861
4475
  const video = el;
3862
- const absStart = parseFloat(video.dataset.absStart || "0");
3863
- const absEnd = parseFloat(video.dataset.absEnd || "0");
3864
- const sourceIn = parseFloat(video.dataset.sourceIn || "0");
4476
+ const clipStart = parseFloat(video.dataset.clipStart || "0");
4477
+ const clipEnd = parseFloat(video.dataset.clipEnd || "0");
4478
+ const startAt = parseFloat(video.dataset.startAt || "0");
4479
+ const targetTime = Math.min(clipStart + Math.max(0, blockElapsed - startAt), clipEnd);
3865
4480
  video.pause();
3866
- if (time < absStart || time >= absEnd) return;
3867
- const targetTime = sourceIn + (time - absStart);
3868
4481
  video.currentTime = targetTime;
3869
4482
  videoSeekPromises.push(
3870
4483
  new Promise((r) => {
@@ -3877,82 +4490,111 @@ function DocPlayerContent({
3877
4490
  })
3878
4491
  );
3879
4492
  });
3880
- Promise.all(videoSeekPromises).then(() => {
3881
- requestAnimationFrame(() => resolve());
3882
- });
4493
+ }
4494
+ root.querySelectorAll("video[data-clip-id]").forEach((el) => {
4495
+ const video = el;
4496
+ const absStart = parseFloat(video.dataset.absStart || "0");
4497
+ const absEnd = parseFloat(video.dataset.absEnd || "0");
4498
+ const sourceIn = parseFloat(video.dataset.sourceIn || "0");
4499
+ video.pause();
4500
+ if (time < absStart || time >= absEnd) return;
4501
+ const targetTime = sourceIn + (time - absStart);
4502
+ video.currentTime = targetTime;
4503
+ videoSeekPromises.push(
4504
+ new Promise((r) => {
4505
+ if (Math.abs(video.currentTime - targetTime) < 0.1) {
4506
+ r();
4507
+ } else {
4508
+ video.addEventListener("seeked", () => r(), { once: true });
4509
+ setTimeout(r, 200);
4510
+ }
4511
+ })
4512
+ );
4513
+ });
4514
+ Promise.all(videoSeekPromises).then(() => {
4515
+ requestAnimationFrame(() => resolve());
3883
4516
  });
3884
4517
  });
3885
- };
3886
- w.getDuration = () => {
3887
- const mediaDuration = getDocPlaybackDuration(doc);
3888
- if (totalDuration > 0) return Math.max(totalDuration, mediaDuration);
3889
- return mediaDuration;
3890
- };
3891
- w.getBlocks = () => expandedBlocks.map((s) => ({
3892
- id: s.id,
3893
- template: s.template ?? "raw",
3894
- startTime: s.startTime,
3895
- duration: s.duration
3896
- }));
3897
- w.getAudioSegments = () => doc.audio.segments.map((seg) => ({
3898
- src: seg.src,
3899
- name: seg.name,
3900
- duration: seg.duration,
3901
- startTime: seg.startTime
4518
+ });
4519
+ };
4520
+ const getDuration = () => {
4521
+ const mediaDuration = getDocPlaybackDuration(doc);
4522
+ if (totalDuration > 0) return Math.max(totalDuration, mediaDuration);
4523
+ return mediaDuration;
4524
+ };
4525
+ const getBlocks = () => expandedBlocks.map((s) => ({
4526
+ id: s.id,
4527
+ template: s.template ?? "raw",
4528
+ startTime: s.startTime,
4529
+ duration: s.duration
4530
+ }));
4531
+ const getAudioSegments = () => doc.audio.segments.map((seg) => ({
4532
+ src: seg.src,
4533
+ name: seg.name,
4534
+ duration: seg.duration,
4535
+ startTime: seg.startTime
4536
+ }));
4537
+ const getCaptions = () => doc.captions?.phrases?.map((p) => ({
4538
+ text: p.text,
4539
+ startTime: p.startTime,
4540
+ endTime: p.endTime
4541
+ })) || [];
4542
+ const getChapters = () => {
4543
+ const titleMap = buildSegmentTitleMap(doc);
4544
+ return doc.audio.segments.map((seg, i) => ({
4545
+ title: titleMap.get(i) || seg.name,
4546
+ startTime: seg.startTime,
4547
+ duration: seg.duration
3902
4548
  }));
3903
- w.getCaptions = () => doc.captions?.phrases?.map((p) => ({
3904
- text: p.text,
3905
- startTime: p.startTime,
3906
- endTime: p.endTime
3907
- })) || [];
3908
- w.getChapters = () => {
3909
- const titleMap = buildSegmentTitleMap(doc);
3910
- return doc.audio.segments.map((seg, i) => ({
3911
- title: titleMap.get(i) || seg.name,
3912
- startTime: seg.startTime,
3913
- duration: seg.duration
3914
- }));
3915
- };
3916
- w.showCover = () => {
3917
- setCoverForced(true);
3918
- return new Promise((resolve) => requestAnimationFrame(() => resolve()));
3919
- };
3920
- w.hideCover = () => {
3921
- setCoverForced(false);
3922
- return new Promise((resolve) => requestAnimationFrame(() => resolve()));
3923
- };
3924
- w.hasCoverBlock = () => !!coverBlock;
3925
- }
4549
+ };
4550
+ const showCover = () => {
4551
+ setCoverForced(true);
4552
+ return new Promise((resolve) => requestAnimationFrame(() => resolve()));
4553
+ };
4554
+ const hideCover = () => {
4555
+ setCoverForced(false);
4556
+ return new Promise((resolve) => requestAnimationFrame(() => resolve()));
4557
+ };
4558
+ const hasCoverBlock = () => !!coverBlock;
4559
+ const api = {
4560
+ seekTo: renderSeekTo,
4561
+ getDuration,
4562
+ getBlocks,
4563
+ getAudioSegments,
4564
+ getCaptions,
4565
+ getChapters,
4566
+ showCover,
4567
+ hideCover,
4568
+ hasCoverBlock
4569
+ };
4570
+ liveRenderAPIRef.current = api;
3926
4571
  return () => {
3927
- if (typeof window !== "undefined") {
3928
- const w = window;
3929
- delete w.seekTo;
3930
- delete w.getDuration;
3931
- delete w.getBlocks;
3932
- delete w.getAudioSegments;
3933
- delete w.getCaptions;
3934
- delete w.getChapters;
3935
- delete w.showCover;
3936
- delete w.hideCover;
3937
- delete w.hasCoverBlock;
3938
- }
4572
+ if (liveRenderAPIRef.current === api) liveRenderAPIRef.current = null;
3939
4573
  };
3940
- }, [renderMode, isDebugMode, seekTo, totalDuration, expandedBlocks, coverBlock]);
4574
+ }, [renderMode, isDebugMode, seekTo, totalDuration, expandedBlocks, coverBlock, doc]);
4575
+ useEffect10(() => {
4576
+ if (!renderMode && !isDebugMode || !containerRef.current) {
4577
+ onRenderAPIReady?.(null);
4578
+ return;
4579
+ }
4580
+ onRenderAPIReady?.(stableRenderAPI);
4581
+ return () => onRenderAPIReady?.(null);
4582
+ }, [renderMode, isDebugMode, onRenderAPIReady, stableRenderAPI]);
3941
4583
  const defaultMode = captionsEnabledProp === false ? "off" : captionStyle || "standard";
3942
- const [captionMode, setCaptionMode] = useState7(defaultMode);
3943
- useEffect8(() => {
4584
+ const [captionMode, setCaptionMode] = useState9(defaultMode);
4585
+ useEffect10(() => {
3944
4586
  setCaptionMode(defaultMode);
3945
4587
  }, [defaultMode]);
3946
4588
  const captionsEnabled = captionMode !== "off";
3947
4589
  const activeCaptionStyle = captionMode === "social" ? "social" : "standard";
3948
- const setCaptionsEnabled = useCallback6(
4590
+ const setCaptionsEnabled = useCallback7(
3949
4591
  (enabled) => {
3950
4592
  setCaptionMode(enabled ? captionStyle || "standard" : "off");
3951
4593
  onCaptionsToggle?.(enabled);
3952
4594
  },
3953
4595
  [onCaptionsToggle, captionStyle]
3954
4596
  );
3955
- const cycleCaptionMode = useCallback6(() => {
4597
+ const cycleCaptionMode = useCallback7(() => {
3956
4598
  setCaptionMode((prev) => {
3957
4599
  const next = prev === "off" ? "standard" : prev === "standard" ? "social" : "off";
3958
4600
  onCaptionsToggle?.(next !== "off");
@@ -3966,6 +4608,7 @@ function DocPlayerContent({
3966
4608
  isPlaying,
3967
4609
  currentTime,
3968
4610
  totalDuration,
4611
+ isCoverVisible: showCoverBlock,
3969
4612
  currentBlockIndex: slideshowSlideIndex,
3970
4613
  totalBlocks: slideshowTotalSlides,
3971
4614
  docProgress,
@@ -3985,6 +4628,7 @@ function DocPlayerContent({
3985
4628
  isPlaying,
3986
4629
  currentTime,
3987
4630
  totalDuration,
4631
+ showCoverBlock,
3988
4632
  slideshowSlideIndex,
3989
4633
  slideshowTotalSlides,
3990
4634
  docProgress,
@@ -4078,23 +4722,39 @@ function DocPlayerContent({
4078
4722
  [currentBlockIndex, expandedBlocks, seekTo, pause, slideshowHasCover, slideshowCoverVisible]
4079
4723
  );
4080
4724
  const swipeEnabled = isSlideshowMode && !isLinearMode && !renderMode && enableSwipe;
4725
+ const armContextFreeSwipeEntry = useCallback7(
4726
+ (destinationSlideIndex) => {
4727
+ const destinationBlockIndex = destinationSlideIndex - (slideshowHasCover ? 1 : 0);
4728
+ const destinationBlock = expandedBlocks[destinationBlockIndex];
4729
+ if (destinationBlock) suppressOutgoingForNextBlock(destinationBlock.id);
4730
+ },
4731
+ [expandedBlocks, slideshowHasCover, suppressOutgoingForNextBlock]
4732
+ );
4733
+ const handleSwipeNext = useCallback7(() => {
4734
+ armContextFreeSwipeEntry(slideshowSlideIndex + 1);
4735
+ slideNavActions.nextSlide();
4736
+ }, [armContextFreeSwipeEntry, slideshowSlideIndex, slideNavActions]);
4737
+ const handleSwipePrev = useCallback7(() => {
4738
+ armContextFreeSwipeEntry(slideshowSlideIndex - 1);
4739
+ slideNavActions.prevSlide();
4740
+ }, [armContextFreeSwipeEntry, slideshowSlideIndex, slideNavActions]);
4081
4741
  const swipe = useSlideSwipe({
4082
4742
  enabled: swipeEnabled,
4083
4743
  containerRef,
4084
4744
  canGoNext: slideshowSlideIndex < slideshowTotalSlides - 1,
4085
4745
  canGoPrev: slideshowSlideIndex > 0,
4086
- onNext: slideNavActions.nextSlide,
4087
- onPrev: slideNavActions.prevSlide
4746
+ onNext: handleSwipeNext,
4747
+ onPrev: handleSwipePrev
4088
4748
  });
4089
- useEffect8(() => {
4749
+ useEffect10(() => {
4090
4750
  onPlaybackStateChange?.(playbackState);
4091
4751
  }, [playbackState, onPlaybackStateChange]);
4092
- useEffect8(() => {
4752
+ useEffect10(() => {
4093
4753
  onControlsReady?.({ play, pause, ...playbackActions });
4094
4754
  }, [play, pause, playbackActions, onControlsReady]);
4095
- const getBlockTitle = useCallback6((block) => {
4755
+ const getBlockTitle = useCallback7((block) => {
4096
4756
  const docBlock = block;
4097
- if (isTemplateBlock2(docBlock)) {
4757
+ if (isTemplateBlock3(docBlock)) {
4098
4758
  const props = docBlock;
4099
4759
  if (typeof props.title === "string") return props.title;
4100
4760
  if (typeof props.stat === "string") return props.stat;
@@ -4116,6 +4776,22 @@ function DocPlayerContent({
4116
4776
  }
4117
4777
  return block.id.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
4118
4778
  }, []);
4779
+ const slideshowPickerItems = useMemo9(() => {
4780
+ const blockItems = expandedBlocks.map((block, index) => ({
4781
+ id: block.id,
4782
+ label: String(index + 1),
4783
+ summary: getBlockTitle(block)
4784
+ }));
4785
+ if (!slideshowHasCover || !coverBlock) return blockItems;
4786
+ return [
4787
+ {
4788
+ id: "__cover__",
4789
+ label: "Cover",
4790
+ summary: getBlockTitle(coverBlock)
4791
+ },
4792
+ ...blockItems
4793
+ ];
4794
+ }, [coverBlock, expandedBlocks, getBlockTitle, slideshowHasCover]);
4119
4795
  const blockMarkers = useMemo9(() => {
4120
4796
  if (!totalDuration || !expandedBlocks.length) return [];
4121
4797
  let prevSegment = -1;
@@ -4131,16 +4807,26 @@ function DocPlayerContent({
4131
4807
  };
4132
4808
  });
4133
4809
  }, [expandedBlocks, totalDuration, getBlockTitle]);
4134
- useEffect8(() => {
4810
+ useEffect10(() => {
4135
4811
  if (blockMarkers.length > 0) {
4136
4812
  onBlockMarkers?.(blockMarkers);
4137
4813
  }
4138
4814
  }, [blockMarkers, onBlockMarkers]);
4139
4815
  expandedBlocksLenRef.current = isSlideshowMode ? slideshowTotalSlides : expandedBlocks.length;
4140
- const handleKeyDown = useCallback6(
4141
- (e) => {
4142
- const activeEl = document.activeElement;
4143
- if (activeEl && (activeEl.tagName === "INPUT" || activeEl.tagName === "TEXTAREA" || activeEl.tagName === "SELECT")) {
4816
+ const handleKeyboardShortcut = useCallback7(
4817
+ (e, global) => {
4818
+ if (e.defaultPrevented || e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return;
4819
+ const target = e.target instanceof Element ? e.target : null;
4820
+ const isEditableTarget = !!target?.closest(
4821
+ 'input, textarea, select, [contenteditable]:not([contenteditable="false"]), [role="textbox"], [role="combobox"], [role="listbox"], [role="slider"], [role="spinbutton"], .monaco-editor'
4822
+ );
4823
+ const isOpenInteractionTarget = !!target?.closest(
4824
+ '[role="menu"], [role="dialog"], [aria-modal="true"]'
4825
+ );
4826
+ const isSlideshowToolbarTarget = isSlideshowMode && !!target?.closest(".doc-controls-slideshow") && !target.closest('[role="menu"]');
4827
+ if (isEditableTarget || global && isOpenInteractionTarget || !global && !!target?.closest(
4828
+ 'input, textarea, select, button, a, [contenteditable]:not([contenteditable="false"]), [role="textbox"]'
4829
+ ) && !isSlideshowToolbarTarget) {
4144
4830
  return;
4145
4831
  }
4146
4832
  if (isLinearMode) return;
@@ -4153,10 +4839,13 @@ function DocPlayerContent({
4153
4839
  slideNavActions.nextSlide();
4154
4840
  break;
4155
4841
  case "ArrowLeft":
4156
- case "ArrowUp":
4157
4842
  e.preventDefault();
4158
4843
  slideNavActions.prevSlide();
4159
4844
  break;
4845
+ case "ArrowUp":
4846
+ e.preventDefault();
4847
+ setIsSlideshowPickerOpen(true);
4848
+ break;
4160
4849
  case "Home":
4161
4850
  e.preventDefault();
4162
4851
  slideNavActions.goToSlide(0);
@@ -4173,9 +4862,11 @@ function DocPlayerContent({
4173
4862
  toggle();
4174
4863
  break;
4175
4864
  case "ArrowRight":
4865
+ e.preventDefault();
4176
4866
  seekTo(Math.min(currentTimeRef.current + 10, totalDurationRef.current));
4177
4867
  break;
4178
4868
  case "ArrowLeft":
4869
+ e.preventDefault();
4179
4870
  seekTo(Math.max(currentTimeRef.current - 10, 0));
4180
4871
  break;
4181
4872
  }
@@ -4183,16 +4874,24 @@ function DocPlayerContent({
4183
4874
  },
4184
4875
  [isSlideshowMode, isLinearMode, toggle, seekTo, slideNavActions]
4185
4876
  );
4186
- useEffect8(() => {
4187
- if (renderMode) return;
4188
- window.addEventListener("keydown", handleKeyDown);
4189
- return () => window.removeEventListener("keydown", handleKeyDown);
4190
- }, [handleKeyDown, renderMode]);
4877
+ const handleKeyDown = useCallback7(
4878
+ (e) => handleKeyboardShortcut(e, false),
4879
+ [handleKeyboardShortcut]
4880
+ );
4881
+ useEffect10(() => {
4882
+ if (!globalKeyboardShortcuts || renderMode || isLinearMode) return;
4883
+ const handleDocumentKeyDown = (event) => {
4884
+ handleKeyboardShortcut(event, true);
4885
+ };
4886
+ document.addEventListener("keydown", handleDocumentKeyDown);
4887
+ return () => document.removeEventListener("keydown", handleDocumentKeyDown);
4888
+ }, [globalKeyboardShortcuts, handleKeyboardShortcut, isLinearMode, renderMode]);
4191
4889
  if (isLinearMode) {
4192
- return /* @__PURE__ */ jsx20(
4890
+ return /* @__PURE__ */ jsx21(
4193
4891
  "div",
4194
4892
  {
4195
4893
  ref: containerRef,
4894
+ "data-player-id": playerId,
4196
4895
  className: "doc-player doc-player--linear",
4197
4896
  style: {
4198
4897
  position: "relative",
@@ -4200,23 +4899,28 @@ function DocPlayerContent({
4200
4899
  height: "100%",
4201
4900
  overflow: "hidden"
4202
4901
  },
4203
- children: /* @__PURE__ */ jsx20(
4902
+ children: /* @__PURE__ */ jsx21(
4204
4903
  LinearDocView,
4205
4904
  {
4206
4905
  doc,
4207
4906
  basePath,
4208
4907
  viewport: activeViewport,
4209
4908
  theme,
4210
- surface
4909
+ surface,
4910
+ animationsEnabled
4211
4911
  }
4212
4912
  )
4213
4913
  }
4214
4914
  );
4215
4915
  }
4216
- return /* @__PURE__ */ jsxs13(
4916
+ return /* @__PURE__ */ jsxs14(
4217
4917
  "div",
4218
4918
  {
4219
4919
  ref: containerRef,
4920
+ "data-player-id": playerId,
4921
+ tabIndex: renderMode ? -1 : 0,
4922
+ "aria-label": "Document player",
4923
+ onKeyDown: renderMode ? void 0 : handleKeyDown,
4220
4924
  className: `doc-player${swipeEnabled ? " doc-player--swipe" : ""}${swipe.phase === "dragging" ? " doc-player--grabbing" : ""}`,
4221
4925
  onClick: handleContainerClick,
4222
4926
  onPointerDown: swipe.onPointerDown,
@@ -4232,33 +4936,35 @@ function DocPlayerContent({
4232
4936
  touchAction: swipeEnabled ? "pan-y" : void 0
4233
4937
  },
4234
4938
  children: [
4235
- /* @__PURE__ */ jsx20("audio", { ref: audioRef, preload: "auto", muted }),
4236
- /* @__PURE__ */ jsx20(
4939
+ /* @__PURE__ */ jsx21("audio", { ref: audioRef, preload: "auto", muted }),
4940
+ /* @__PURE__ */ jsx21(
4237
4941
  MediaClipLayer,
4238
4942
  {
4239
4943
  schedule: mediaSchedule,
4240
4944
  currentTime,
4241
4945
  isPlaying,
4242
4946
  basePath,
4243
- renderMode
4947
+ renderMode,
4948
+ muted
4244
4949
  }
4245
4950
  ),
4246
- /* @__PURE__ */ jsxs13("div", { className: "doc-player__viewport", children: [
4247
- showCoverBlock && coverBlock && /* @__PURE__ */ jsx20("div", { className: "doc-player__block doc-player__block--cover", children: /* @__PURE__ */ jsx20(
4951
+ /* @__PURE__ */ jsxs14("div", { className: "doc-player__viewport", children: [
4952
+ showCoverBlock && coverBlock && /* @__PURE__ */ jsx21("div", { className: "doc-player__block doc-player__block--cover", children: /* @__PURE__ */ jsx21(
4248
4953
  BlockRenderer,
4249
4954
  {
4250
4955
  block: coverBlock,
4251
4956
  blockTime: 0,
4252
4957
  basePath,
4253
4958
  isEntering: false,
4254
- viewport: activeViewport
4959
+ viewport: activeViewport,
4960
+ animationsEnabled
4255
4961
  }
4256
4962
  ) }),
4257
- !showCoverBlock && previousBlock && isExiting && // Keyed by block id so each block is its own DOM subtree: React never
4963
+ animationsEnabled && !showCoverBlock && previousBlock && isExiting && // Keyed by block id so each block is its own DOM subtree: React never
4258
4964
  // reconciles one block's layers onto another's (templates reuse layer
4259
4965
  // ids like `title`/`background`), which would otherwise reuse stale
4260
4966
  // DOM / skip entrance animations mid-transition.
4261
- /* @__PURE__ */ jsx20("div", { className: "doc-player__block doc-player__block--previous", children: /* @__PURE__ */ jsx20(
4967
+ /* @__PURE__ */ jsx21("div", { className: "doc-player__block doc-player__block--previous", children: /* @__PURE__ */ jsx21(
4262
4968
  BlockRenderer,
4263
4969
  {
4264
4970
  block: previousBlock,
@@ -4266,29 +4972,31 @@ function DocPlayerContent({
4266
4972
  basePath,
4267
4973
  isExiting: true,
4268
4974
  transition: currentBlock?.transition,
4269
- viewport: activeViewport
4975
+ viewport: activeViewport,
4976
+ animationsEnabled
4270
4977
  }
4271
4978
  ) }, previousBlock.id),
4272
- !showCoverBlock && currentBlock && /* @__PURE__ */ jsx20(
4979
+ !showCoverBlock && currentBlock && /* @__PURE__ */ jsx21(
4273
4980
  "div",
4274
4981
  {
4275
4982
  className: `doc-player__block doc-player__block--active${swipe.phase !== "idle" ? ` doc-player__block--${swipe.phase}` : ""}`,
4276
4983
  style: swipe.phase !== "idle" ? { transform: `translateX(${swipe.offsetPx}px)` } : void 0,
4277
- children: /* @__PURE__ */ jsx20(
4984
+ children: /* @__PURE__ */ jsx21(
4278
4985
  BlockRenderer,
4279
4986
  {
4280
4987
  block: currentBlock,
4281
4988
  blockTime,
4282
4989
  basePath,
4283
- isEntering,
4990
+ isEntering: animationsEnabled && isEntering,
4284
4991
  viewport: activeViewport,
4285
- isPlaying
4992
+ isPlaying,
4993
+ animationsEnabled
4286
4994
  }
4287
4995
  )
4288
4996
  },
4289
4997
  currentBlock.id
4290
4998
  ),
4291
- hasCaptions && (renderMode ? captionsEnabled : true) && /* @__PURE__ */ jsx20(
4999
+ hasCaptions && (renderMode ? captionsEnabled : true) && /* @__PURE__ */ jsx21(
4292
5000
  CaptionOverlay,
4293
5001
  {
4294
5002
  captions: doc.captions,
@@ -4300,7 +5008,7 @@ function DocPlayerContent({
4300
5008
  viewport: activeViewport
4301
5009
  }
4302
5010
  ),
4303
- isDebugMode && /* @__PURE__ */ jsxs13(
5011
+ isDebugMode && /* @__PURE__ */ jsxs14(
4304
5012
  "div",
4305
5013
  {
4306
5014
  className: "doc-player__debug",
@@ -4321,27 +5029,27 @@ function DocPlayerContent({
4321
5029
  textAlign: "left"
4322
5030
  },
4323
5031
  children: [
4324
- /* @__PURE__ */ jsx20("div", { style: { color: "#ffcc00", fontWeight: "bold", marginBottom: "4px" }, children: "DEBUG MODE" }),
4325
- /* @__PURE__ */ jsxs13("div", { children: [
4326
- /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "template:" }),
5032
+ /* @__PURE__ */ jsx21("div", { style: { color: "#ffcc00", fontWeight: "bold", marginBottom: "4px" }, children: "DEBUG MODE" }),
5033
+ /* @__PURE__ */ jsxs14("div", { children: [
5034
+ /* @__PURE__ */ jsx21("span", { style: { color: "#888" }, children: "template:" }),
4327
5035
  " ",
4328
- /* @__PURE__ */ jsx20("span", { style: { color: "#ff6b6b" }, children: currentBlock?.template ?? "raw" })
5036
+ /* @__PURE__ */ jsx21("span", { style: { color: "#ff6b6b" }, children: currentBlock?.template ?? "raw" })
4329
5037
  ] }),
4330
- /* @__PURE__ */ jsxs13("div", { children: [
4331
- /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "block:" }),
5038
+ /* @__PURE__ */ jsxs14("div", { children: [
5039
+ /* @__PURE__ */ jsx21("span", { style: { color: "#888" }, children: "block:" }),
4332
5040
  " ",
4333
5041
  currentBlockIndex + 1,
4334
5042
  "/",
4335
5043
  expandedBlocks.length,
4336
5044
  " ",
4337
- /* @__PURE__ */ jsxs13("span", { style: { color: "#666" }, children: [
5045
+ /* @__PURE__ */ jsxs14("span", { style: { color: "#666" }, children: [
4338
5046
  "(",
4339
5047
  currentBlock?.id || "none",
4340
5048
  ")"
4341
5049
  ] })
4342
5050
  ] }),
4343
- /* @__PURE__ */ jsxs13("div", { children: [
4344
- /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "time:" }),
5051
+ /* @__PURE__ */ jsxs14("div", { children: [
5052
+ /* @__PURE__ */ jsx21("span", { style: { color: "#888" }, children: "time:" }),
4345
5053
  " ",
4346
5054
  currentTime.toFixed(2),
4347
5055
  "s /",
@@ -4349,7 +5057,7 @@ function DocPlayerContent({
4349
5057
  totalDuration.toFixed(1),
4350
5058
  "s",
4351
5059
  " ",
4352
- /* @__PURE__ */ jsxs13("span", { style: { color: "#666" }, children: [
5060
+ /* @__PURE__ */ jsxs14("span", { style: { color: "#666" }, children: [
4353
5061
  "(progress: ",
4354
5062
  (docProgress * 100).toFixed(1),
4355
5063
  "%, scriptDur: ",
@@ -4357,8 +5065,8 @@ function DocPlayerContent({
4357
5065
  ")"
4358
5066
  ] })
4359
5067
  ] }),
4360
- /* @__PURE__ */ jsxs13("div", { children: [
4361
- /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "blockTime:" }),
5068
+ /* @__PURE__ */ jsxs14("div", { children: [
5069
+ /* @__PURE__ */ jsx21("span", { style: { color: "#888" }, children: "blockTime:" }),
4362
5070
  " ",
4363
5071
  blockTime.toFixed(2),
4364
5072
  "s /",
@@ -4366,58 +5074,58 @@ function DocPlayerContent({
4366
5074
  (currentBlock?.duration || 0).toFixed(1),
4367
5075
  "s"
4368
5076
  ] }),
4369
- /* @__PURE__ */ jsxs13("div", { children: [
4370
- /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "segment:" }),
5077
+ /* @__PURE__ */ jsxs14("div", { children: [
5078
+ /* @__PURE__ */ jsx21("span", { style: { color: "#888" }, children: "segment:" }),
4371
5079
  " ",
4372
5080
  currentSegment,
4373
5081
  "/",
4374
5082
  doc.audio.segments.length - 1,
4375
5083
  " ",
4376
- /* @__PURE__ */ jsxs13("span", { style: { color: "#666" }, children: [
5084
+ /* @__PURE__ */ jsxs14("span", { style: { color: "#666" }, children: [
4377
5085
  "(",
4378
5086
  doc.audio.segments[currentSegment]?.name || "none",
4379
5087
  ")"
4380
5088
  ] })
4381
5089
  ] }),
4382
- /* @__PURE__ */ jsxs13("div", { children: [
4383
- /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "viewport:" }),
5090
+ /* @__PURE__ */ jsxs14("div", { children: [
5091
+ /* @__PURE__ */ jsx21("span", { style: { color: "#888" }, children: "viewport:" }),
4384
5092
  " ",
4385
5093
  activeViewport.name || `${activeViewport.width}x${activeViewport.height}`,
4386
5094
  " ",
4387
- /* @__PURE__ */ jsxs13("span", { style: { color: "#666" }, children: [
5095
+ /* @__PURE__ */ jsxs14("span", { style: { color: "#666" }, children: [
4388
5096
  "(",
4389
5097
  orientation,
4390
5098
  ")"
4391
5099
  ] })
4392
5100
  ] }),
4393
- /* @__PURE__ */ jsxs13("div", { children: [
4394
- /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "playing:" }),
5101
+ /* @__PURE__ */ jsxs14("div", { children: [
5102
+ /* @__PURE__ */ jsx21("span", { style: { color: "#888" }, children: "playing:" }),
4395
5103
  " ",
4396
- /* @__PURE__ */ jsx20("span", { style: { color: isPlaying ? "#4ade80" : "#f87171" }, children: isPlaying ? "yes" : "no" }),
4397
- showCoverBlock && /* @__PURE__ */ jsx20("span", { style: { color: "#60a5fa" }, children: " (cover)" })
5104
+ /* @__PURE__ */ jsx21("span", { style: { color: isPlaying ? "#4ade80" : "#f87171" }, children: isPlaying ? "yes" : "no" }),
5105
+ showCoverBlock && /* @__PURE__ */ jsx21("span", { style: { color: "#60a5fa" }, children: " (cover)" })
4398
5106
  ] }),
4399
5107
  hasCaptions && (() => {
4400
5108
  const debugPhrase = getCaptionAtTime2(doc.captions, currentTime);
4401
5109
  const debugEnabled = captionsEnabled && (isPlaying || currentTime > 0);
4402
- return /* @__PURE__ */ jsxs13(Fragment3, { children: [
4403
- /* @__PURE__ */ jsxs13("div", { children: [
4404
- /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "captions:" }),
5110
+ return /* @__PURE__ */ jsxs14(Fragment3, { children: [
5111
+ /* @__PURE__ */ jsxs14("div", { children: [
5112
+ /* @__PURE__ */ jsx21("span", { style: { color: "#888" }, children: "captions:" }),
4405
5113
  " ",
4406
5114
  doc.captions?.phrases.length || 0,
4407
5115
  " phrases",
4408
5116
  " ",
4409
- /* @__PURE__ */ jsxs13("span", { style: { color: captionsEnabled ? "#4ade80" : "#666" }, children: [
5117
+ /* @__PURE__ */ jsxs14("span", { style: { color: captionsEnabled ? "#4ade80" : "#666" }, children: [
4410
5118
  "(",
4411
5119
  captionsEnabled ? "on" : "off",
4412
5120
  ")"
4413
5121
  ] })
4414
5122
  ] }),
4415
- /* @__PURE__ */ jsxs13("div", { children: [
4416
- /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "cc.enabled:" }),
5123
+ /* @__PURE__ */ jsxs14("div", { children: [
5124
+ /* @__PURE__ */ jsx21("span", { style: { color: "#888" }, children: "cc.enabled:" }),
4417
5125
  " ",
4418
- /* @__PURE__ */ jsx20("span", { style: { color: debugEnabled ? "#4ade80" : "#f87171" }, children: String(debugEnabled) }),
5126
+ /* @__PURE__ */ jsx21("span", { style: { color: debugEnabled ? "#4ade80" : "#f87171" }, children: String(debugEnabled) }),
4419
5127
  " ",
4420
- /* @__PURE__ */ jsxs13("span", { style: { color: "#666" }, children: [
5128
+ /* @__PURE__ */ jsxs14("span", { style: { color: "#666" }, children: [
4421
5129
  "(playing=",
4422
5130
  String(isPlaying),
4423
5131
  " t>0=",
@@ -4425,15 +5133,15 @@ function DocPlayerContent({
4425
5133
  ")"
4426
5134
  ] })
4427
5135
  ] }),
4428
- /* @__PURE__ */ jsxs13("div", { children: [
4429
- /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "cc.phrase:" }),
5136
+ /* @__PURE__ */ jsxs14("div", { children: [
5137
+ /* @__PURE__ */ jsx21("span", { style: { color: "#888" }, children: "cc.phrase:" }),
4430
5138
  " ",
4431
- /* @__PURE__ */ jsx20("span", { style: { color: debugPhrase ? "#4ade80" : "#f87171" }, children: debugPhrase ? `"${debugPhrase.text.slice(0, 30)}..."` : "null" })
5139
+ /* @__PURE__ */ jsx21("span", { style: { color: debugPhrase ? "#4ade80" : "#f87171" }, children: debugPhrase ? `"${debugPhrase.text.slice(0, 30)}..."` : "null" })
4432
5140
  ] }),
4433
- debugPhrase && /* @__PURE__ */ jsxs13("div", { children: [
4434
- /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "cc.range:" }),
5141
+ debugPhrase && /* @__PURE__ */ jsxs14("div", { children: [
5142
+ /* @__PURE__ */ jsx21("span", { style: { color: "#888" }, children: "cc.range:" }),
4435
5143
  " ",
4436
- /* @__PURE__ */ jsxs13("span", { style: { color: "#60a5fa" }, children: [
5144
+ /* @__PURE__ */ jsxs14("span", { style: { color: "#60a5fa" }, children: [
4437
5145
  debugPhrase.startTime.toFixed(2),
4438
5146
  "-",
4439
5147
  debugPhrase.endTime.toFixed(2)
@@ -4445,7 +5153,7 @@ function DocPlayerContent({
4445
5153
  }
4446
5154
  )
4447
5155
  ] }),
4448
- !isAvailable && unavailableMessage && /* @__PURE__ */ jsxs13(
5156
+ !isAvailable && unavailableMessage && /* @__PURE__ */ jsxs14(
4449
5157
  "div",
4450
5158
  {
4451
5159
  className: "doc-player__unavailable",
@@ -4466,12 +5174,12 @@ function DocPlayerContent({
4466
5174
  zIndex: 50
4467
5175
  },
4468
5176
  children: [
4469
- /* @__PURE__ */ jsx20("span", { style: { fontSize: "32px" }, children: "\u{1F50A}" }),
4470
- /* @__PURE__ */ jsx20("span", { children: unavailableMessage })
5177
+ /* @__PURE__ */ jsx21("span", { style: { fontSize: "32px" }, children: "\u{1F50A}" }),
5178
+ /* @__PURE__ */ jsx21("span", { children: unavailableMessage })
4471
5179
  ]
4472
5180
  }
4473
5181
  ),
4474
- !renderMode && !isSlideshowMode && showControls && /* @__PURE__ */ jsx20(
5182
+ !renderMode && !isSlideshowMode && showControls && /* @__PURE__ */ jsx21(
4475
5183
  DocControlsOverlay,
4476
5184
  {
4477
5185
  state: playbackState,
@@ -4481,7 +5189,7 @@ function DocPlayerContent({
4481
5189
  getBlockTitle
4482
5190
  }
4483
5191
  ),
4484
- !renderMode && !isSlideshowMode && !showControls && showScrubber && /* @__PURE__ */ jsx20(
5192
+ !renderMode && !isSlideshowMode && !showControls && showScrubber && /* @__PURE__ */ jsx21(
4485
5193
  "div",
4486
5194
  {
4487
5195
  className: "doc-player__scrubber",
@@ -4496,7 +5204,7 @@ function DocPlayerContent({
4496
5204
  alignItems: "center",
4497
5205
  zIndex: 100
4498
5206
  },
4499
- children: /* @__PURE__ */ jsx20(
5207
+ children: /* @__PURE__ */ jsx21(
4500
5208
  DocProgressBar,
4501
5209
  {
4502
5210
  state: playbackState,
@@ -4508,15 +5216,24 @@ function DocPlayerContent({
4508
5216
  )
4509
5217
  }
4510
5218
  ),
4511
- !renderMode && isSlideshowMode && /* @__PURE__ */ jsx20(DocControlsSlideshow, { state: playbackState, slideNav: slideNavActions }),
4512
- !isSlideshowMode && tapFeedback && /* @__PURE__ */ jsx20("div", { className: "doc-player__tap-feedback", children: /* @__PURE__ */ jsx20("svg", { viewBox: "0 0 24 24", fill: "white", width: "48", height: "48", children: tapFeedback === "pause" ? /* @__PURE__ */ jsx20("path", { d: "M6 19h4V5H6v14zm8-14v14h4V5h-4z" }) : /* @__PURE__ */ jsx20("path", { d: "M8 5v14l11-7z" }) }) }, Date.now())
5219
+ !renderMode && isSlideshowMode && showControls && /* @__PURE__ */ jsx21(
5220
+ DocControlsSlideshow,
5221
+ {
5222
+ state: playbackState,
5223
+ slideNav: slideNavActions,
5224
+ slides: slideshowPickerItems,
5225
+ pickerOpen: isSlideshowPickerOpen,
5226
+ onPickerOpenChange: setIsSlideshowPickerOpen
5227
+ }
5228
+ ),
5229
+ !isSlideshowMode && tapFeedback && /* @__PURE__ */ jsx21("div", { className: "doc-player__tap-feedback", children: /* @__PURE__ */ jsx21("svg", { viewBox: "0 0 24 24", fill: "white", width: "48", height: "48", children: tapFeedback === "pause" ? /* @__PURE__ */ jsx21("path", { d: "M6 19h4V5H6v14zm8-14v14h4V5h-4z" }) : /* @__PURE__ */ jsx21("path", { d: "M8 5v14l11-7z" }) }) }, tapFeedback)
4513
5230
  ]
4514
5231
  }
4515
5232
  );
4516
5233
  }
4517
5234
 
4518
5235
  // src/DocControlsBottom.tsx
4519
- import { jsx as jsx21, jsxs as jsxs14 } from "react/jsx-runtime";
5236
+ import { jsx as jsx22, jsxs as jsxs15 } from "react/jsx-runtime";
4520
5237
  function DocControlsBottom({
4521
5238
  state,
4522
5239
  actions,
@@ -4524,32 +5241,32 @@ function DocControlsBottom({
4524
5241
  expandedBlocks,
4525
5242
  getBlockTitle
4526
5243
  }) {
4527
- return /* @__PURE__ */ jsxs14("div", { className: "doc-controls-bottom", children: [
4528
- /* @__PURE__ */ jsx21(
5244
+ return /* @__PURE__ */ jsxs15("div", { className: "doc-controls-bottom", children: [
5245
+ /* @__PURE__ */ jsx22(
4529
5246
  "button",
4530
5247
  {
4531
5248
  className: "bottom-ctrl-btn",
4532
5249
  onClick: actions.restart,
4533
5250
  title: "Restart",
4534
5251
  "aria-label": "Restart from beginning",
4535
- children: /* @__PURE__ */ jsx21("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx21("path", { d: "M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z" }) })
5252
+ children: /* @__PURE__ */ jsx22("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx22("path", { d: "M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z" }) })
4536
5253
  }
4537
5254
  ),
4538
- /* @__PURE__ */ jsx21(
5255
+ /* @__PURE__ */ jsx22(
4539
5256
  "button",
4540
5257
  {
4541
5258
  className: "bottom-ctrl-btn bottom-play-btn",
4542
5259
  onClick: actions.toggle,
4543
5260
  "aria-label": state.isPlaying ? "Pause" : "Play",
4544
- children: state.isPlaying ? /* @__PURE__ */ jsx21("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx21("path", { d: "M6 19h4V5H6v14zm8-14v14h4V5h-4z" }) }) : /* @__PURE__ */ jsx21("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx21("path", { d: "M8 5v14l11-7z" }) })
5261
+ children: state.isPlaying ? /* @__PURE__ */ jsx22("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx22("path", { d: "M6 19h4V5H6v14zm8-14v14h4V5h-4z" }) }) : /* @__PURE__ */ jsx22("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx22("path", { d: "M8 5v14l11-7z" }) })
4545
5262
  }
4546
5263
  ),
4547
- /* @__PURE__ */ jsxs14("span", { className: "bottom-time", children: [
5264
+ /* @__PURE__ */ jsxs15("span", { className: "bottom-time", children: [
4548
5265
  formatTime(state.currentTime),
4549
5266
  " / ",
4550
5267
  formatTime(state.totalDuration)
4551
5268
  ] }),
4552
- /* @__PURE__ */ jsx21(
5269
+ /* @__PURE__ */ jsx22(
4553
5270
  DocProgressBar,
4554
5271
  {
4555
5272
  state,
@@ -4559,82 +5276,82 @@ function DocControlsBottom({
4559
5276
  getBlockTitle
4560
5277
  }
4561
5278
  ),
4562
- /* @__PURE__ */ jsxs14("span", { className: "bottom-segment", children: [
5279
+ /* @__PURE__ */ jsxs15("span", { className: "bottom-segment", children: [
4563
5280
  state.currentBlockIndex + 1,
4564
5281
  "/",
4565
5282
  state.totalBlocks
4566
5283
  ] }),
4567
- state.hasCaptions && /* @__PURE__ */ jsx21(
5284
+ state.hasCaptions && /* @__PURE__ */ jsx22(
4568
5285
  "button",
4569
5286
  {
4570
5287
  className: `bottom-ctrl-btn ${state.captionMode !== "off" ? "bottom-ctrl-btn--active" : ""}`,
4571
5288
  onClick: () => actions.cycleCaptionMode(),
4572
5289
  title: state.captionMode === "off" ? "Captions: Off" : state.captionMode === "standard" ? "Captions: Standard" : "Captions: Social",
4573
5290
  "aria-label": "Cycle caption style",
4574
- children: /* @__PURE__ */ jsx21("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx21("path", { d: "M19 4H5a2 2 0 00-2 2v12a2 2 0 002 2h14a2 2 0 002-2V6a2 2 0 00-2-2zm-8 7H9.5v-.5h-2v3h2V13H11v1a1 1 0 01-1 1H7a1 1 0 01-1-1v-4a1 1 0 011-1h3a1 1 0 011 1v1zm7 0h-1.5v-.5h-2v3h2V13H18v1a1 1 0 01-1 1h-3a1 1 0 01-1-1v-4a1 1 0 011-1h3a1 1 0 011 1v1z" }) })
5291
+ children: /* @__PURE__ */ jsx22("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx22("path", { d: "M19 4H5a2 2 0 00-2 2v12a2 2 0 002 2h14a2 2 0 002-2V6a2 2 0 00-2-2zm-8 7H9.5v-.5h-2v3h2V13H11v1a1 1 0 01-1 1H7a1 1 0 01-1-1v-4a1 1 0 011-1h3a1 1 0 011 1v1zm7 0h-1.5v-.5h-2v3h2V13H18v1a1 1 0 01-1 1h-3a1 1 0 01-1-1v-4a1 1 0 011-1h3a1 1 0 011 1v1z" }) })
4575
5292
  }
4576
5293
  )
4577
5294
  ] });
4578
5295
  }
4579
5296
 
4580
5297
  // src/DocControlsSidebar.tsx
4581
- import { jsx as jsx22, jsxs as jsxs15 } from "react/jsx-runtime";
5298
+ import { jsx as jsx23, jsxs as jsxs16 } from "react/jsx-runtime";
4582
5299
  function DocControlsSidebar({ state, actions }) {
4583
- return /* @__PURE__ */ jsxs15("div", { className: "doc-controls-sidebar", children: [
4584
- /* @__PURE__ */ jsx22(
5300
+ return /* @__PURE__ */ jsxs16("div", { className: "doc-controls-sidebar", children: [
5301
+ /* @__PURE__ */ jsx23(
4585
5302
  "button",
4586
5303
  {
4587
5304
  className: "sidebar-ctrl-btn",
4588
5305
  onClick: actions.restart,
4589
5306
  title: "Restart",
4590
5307
  "aria-label": "Restart from beginning",
4591
- children: /* @__PURE__ */ jsx22("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx22("path", { d: "M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z" }) })
5308
+ children: /* @__PURE__ */ jsx23("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx23("path", { d: "M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z" }) })
4592
5309
  }
4593
5310
  ),
4594
- /* @__PURE__ */ jsx22(
5311
+ /* @__PURE__ */ jsx23(
4595
5312
  "button",
4596
5313
  {
4597
5314
  className: "sidebar-ctrl-btn sidebar-play-btn",
4598
5315
  onClick: actions.toggle,
4599
5316
  "aria-label": state.isPlaying ? "Pause" : "Play",
4600
- children: state.isPlaying ? /* @__PURE__ */ jsx22("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "22", height: "22", children: /* @__PURE__ */ jsx22("path", { d: "M6 19h4V5H6v14zm8-14v14h4V5h-4z" }) }) : /* @__PURE__ */ jsx22("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "22", height: "22", children: /* @__PURE__ */ jsx22("path", { d: "M8 5v14l11-7z" }) })
5317
+ children: state.isPlaying ? /* @__PURE__ */ jsx23("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "22", height: "22", children: /* @__PURE__ */ jsx23("path", { d: "M6 19h4V5H6v14zm8-14v14h4V5h-4z" }) }) : /* @__PURE__ */ jsx23("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "22", height: "22", children: /* @__PURE__ */ jsx23("path", { d: "M8 5v14l11-7z" }) })
4601
5318
  }
4602
5319
  ),
4603
- /* @__PURE__ */ jsxs15("div", { className: "sidebar-time", children: [
4604
- /* @__PURE__ */ jsx22("div", { children: formatTime(state.currentTime) }),
4605
- /* @__PURE__ */ jsx22("div", { className: "sidebar-time-total", children: formatTime(state.totalDuration) })
5320
+ /* @__PURE__ */ jsxs16("div", { className: "sidebar-time", children: [
5321
+ /* @__PURE__ */ jsx23("div", { children: formatTime(state.currentTime) }),
5322
+ /* @__PURE__ */ jsx23("div", { className: "sidebar-time-total", children: formatTime(state.totalDuration) })
4606
5323
  ] }),
4607
- /* @__PURE__ */ jsxs15("div", { className: "sidebar-segment", children: [
5324
+ /* @__PURE__ */ jsxs16("div", { className: "sidebar-segment", children: [
4608
5325
  state.currentBlockIndex + 1,
4609
5326
  "/",
4610
5327
  state.totalBlocks
4611
5328
  ] }),
4612
- state.hasCaptions && /* @__PURE__ */ jsx22(
5329
+ state.hasCaptions && /* @__PURE__ */ jsx23(
4613
5330
  "button",
4614
5331
  {
4615
5332
  className: `sidebar-ctrl-btn ${state.captionMode !== "off" ? "sidebar-ctrl-btn--active" : ""}`,
4616
5333
  onClick: () => actions.cycleCaptionMode(),
4617
5334
  title: state.captionMode === "off" ? "Captions: Off" : state.captionMode === "standard" ? "Captions: Standard" : "Captions: Social",
4618
5335
  "aria-label": "Cycle caption style",
4619
- children: /* @__PURE__ */ jsx22("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx22("path", { d: "M19 4H5a2 2 0 00-2 2v12a2 2 0 002 2h14a2 2 0 002-2V6a2 2 0 00-2-2zm-8 7H9.5v-.5h-2v3h2V13H11v1a1 1 0 01-1 1H7a1 1 0 01-1-1v-4a1 1 0 011-1h3a1 1 0 011 1v1zm7 0h-1.5v-.5h-2v3h2V13H18v1a1 1 0 01-1 1h-3a1 1 0 01-1-1v-4a1 1 0 011-1h3a1 1 0 011 1v1z" }) })
5336
+ children: /* @__PURE__ */ jsx23("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx23("path", { d: "M19 4H5a2 2 0 00-2 2v12a2 2 0 002 2h14a2 2 0 002-2V6a2 2 0 00-2-2zm-8 7H9.5v-.5h-2v3h2V13H11v1a1 1 0 01-1 1H7a1 1 0 01-1-1v-4a1 1 0 011-1h3a1 1 0 011 1v1zm7 0h-1.5v-.5h-2v3h2V13H18v1a1 1 0 01-1 1h-3a1 1 0 01-1-1v-4a1 1 0 011-1h3a1 1 0 011 1v1z" }) })
4620
5337
  }
4621
5338
  ),
4622
- actions.toggleFullscreen && /* @__PURE__ */ jsx22(
5339
+ actions.toggleFullscreen && /* @__PURE__ */ jsx23(
4623
5340
  "button",
4624
5341
  {
4625
5342
  className: `sidebar-ctrl-btn ${state.isFullscreen ? "sidebar-ctrl-btn--active" : ""}`,
4626
5343
  onClick: actions.toggleFullscreen,
4627
5344
  title: state.isFullscreen ? "Exit fullscreen (F)" : "Fullscreen (F)",
4628
5345
  "aria-label": state.isFullscreen ? "Exit fullscreen" : "Enter fullscreen",
4629
- children: state.isFullscreen ? /* @__PURE__ */ jsx22("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx22("path", { d: "M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z" }) }) : /* @__PURE__ */ jsx22("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx22("path", { d: "M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" }) })
5346
+ children: state.isFullscreen ? /* @__PURE__ */ jsx23("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx23("path", { d: "M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z" }) }) : /* @__PURE__ */ jsx23("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx23("path", { d: "M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" }) })
4630
5347
  }
4631
5348
  )
4632
5349
  ] });
4633
5350
  }
4634
5351
 
4635
5352
  // src/DocPlayerWithSidebar.tsx
4636
- import { useRef as useRef8, useState as useState8, useCallback as useCallback7, useEffect as useEffect9 } from "react";
4637
- import { jsx as jsx23, jsxs as jsxs16 } from "react/jsx-runtime";
5353
+ import { useRef as useRef10, useState as useState10, useCallback as useCallback8, useEffect as useEffect11 } from "react";
5354
+ import { jsx as jsx24, jsxs as jsxs17 } from "react/jsx-runtime";
4638
5355
  var DEFAULT_STATE = {
4639
5356
  isPlaying: false,
4640
5357
  currentTime: 0,
@@ -4656,6 +5373,7 @@ function DocPlayerWithSidebar({
4656
5373
  onEnded,
4657
5374
  onTimeUpdate,
4658
5375
  audioController,
5376
+ animationsEnabled = true,
4659
5377
  muted,
4660
5378
  captionsEnabled,
4661
5379
  isFullscreen,
@@ -4664,11 +5382,11 @@ function DocPlayerWithSidebar({
4664
5382
  onPlayingChange,
4665
5383
  theme
4666
5384
  }) {
4667
- const stateRef = useRef8(DEFAULT_STATE);
4668
- const actionsRef = useRef8(null);
4669
- const wasPlayingRef = useRef8(false);
4670
- const [, setTick] = useState8(0);
4671
- const handleStateChange = useCallback7(
5385
+ const stateRef = useRef10(DEFAULT_STATE);
5386
+ const actionsRef = useRef10(null);
5387
+ const wasPlayingRef = useRef10(false);
5388
+ const [, setTick] = useState10(0);
5389
+ const handleStateChange = useCallback8(
4672
5390
  (state) => {
4673
5391
  stateRef.current = state;
4674
5392
  if (onPlayingChange && state.isPlaying !== wasPlayingRef.current) {
@@ -4678,7 +5396,7 @@ function DocPlayerWithSidebar({
4678
5396
  },
4679
5397
  [onPlayingChange]
4680
5398
  );
4681
- const handleControlsReady = useCallback7(
5399
+ const handleControlsReady = useCallback8(
4682
5400
  (controls) => {
4683
5401
  const isFirst = !actionsRef.current;
4684
5402
  actionsRef.current = controls;
@@ -4686,14 +5404,14 @@ function DocPlayerWithSidebar({
4686
5404
  },
4687
5405
  []
4688
5406
  );
4689
- useEffect9(() => {
5407
+ useEffect11(() => {
4690
5408
  const interval = setInterval(() => {
4691
5409
  setTick((t) => t + 1);
4692
5410
  }, 250);
4693
5411
  return () => clearInterval(interval);
4694
5412
  }, []);
4695
- return /* @__PURE__ */ jsxs16("div", { className: "doc-player-sidebar-layout", children: [
4696
- /* @__PURE__ */ jsx23("div", { className: "doc-player-sidebar-layout__video", children: /* @__PURE__ */ jsx23(
5413
+ return /* @__PURE__ */ jsxs17("div", { className: "doc-player-sidebar-layout", children: [
5414
+ /* @__PURE__ */ jsx24("div", { className: "doc-player-sidebar-layout__video", children: /* @__PURE__ */ jsx24(
4697
5415
  DocPlayer,
4698
5416
  {
4699
5417
  doc,
@@ -4703,6 +5421,7 @@ function DocPlayerWithSidebar({
4703
5421
  onEnded,
4704
5422
  onTimeUpdate,
4705
5423
  audioController,
5424
+ animationsEnabled,
4706
5425
  muted,
4707
5426
  captionsEnabled,
4708
5427
  showControls: isFullscreen,
@@ -4714,7 +5433,7 @@ function DocPlayerWithSidebar({
4714
5433
  forceViewport
4715
5434
  }
4716
5435
  ) }),
4717
- actionsRef.current && /* @__PURE__ */ jsx23(DocControlsSidebar, { state: stateRef.current, actions: actionsRef.current })
5436
+ actionsRef.current && /* @__PURE__ */ jsx24(DocControlsSidebar, { state: stateRef.current, actions: actionsRef.current })
4718
5437
  ] });
4719
5438
  }
4720
5439
 
@@ -4745,18 +5464,18 @@ import {
4745
5464
  arrayItemKind
4746
5465
  } from "@bendyline/squisq/jsonForm";
4747
5466
  import { parseMarkdown as parseMarkdown3 } from "@bendyline/squisq/markdown";
4748
- import { Fragment as Fragment5, jsx as jsx24, jsxs as jsxs17 } from "react/jsx-runtime";
5467
+ import { Fragment as Fragment5, jsx as jsx25, jsxs as jsxs18 } from "react/jsx-runtime";
4749
5468
  function TextViewer({ value }) {
4750
5469
  if (value === void 0 || value === null || value === "") {
4751
- return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-empty", children: "\u2014" });
5470
+ return /* @__PURE__ */ jsx25("span", { className: "squisq-jv-empty", children: "\u2014" });
4752
5471
  }
4753
- return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-value", children: String(value) });
5472
+ return /* @__PURE__ */ jsx25("span", { className: "squisq-jv-value", children: String(value) });
4754
5473
  }
4755
5474
  function MultilineViewer({ value }) {
4756
5475
  if (value === void 0 || value === null || value === "") {
4757
- return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-empty", children: "\u2014" });
5476
+ return /* @__PURE__ */ jsx25("span", { className: "squisq-jv-empty", children: "\u2014" });
4758
5477
  }
4759
- return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-value squisq-jv-value--multiline", children: String(value) });
5478
+ return /* @__PURE__ */ jsx25("span", { className: "squisq-jv-value squisq-jv-value--multiline", children: String(value) });
4760
5479
  }
4761
5480
  function RichTextViewer({ value }) {
4762
5481
  const nodes = useMemo11(() => {
@@ -4768,39 +5487,39 @@ function RichTextViewer({ value }) {
4768
5487
  return null;
4769
5488
  }
4770
5489
  }, [value]);
4771
- if (!nodes) return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-empty", children: "\u2014" });
4772
- return /* @__PURE__ */ jsx24("div", { className: "squisq-jv-richtext", children: /* @__PURE__ */ jsx24(MarkdownRenderer, { nodes }) });
5490
+ if (!nodes) return /* @__PURE__ */ jsx25("span", { className: "squisq-jv-empty", children: "\u2014" });
5491
+ return /* @__PURE__ */ jsx25("div", { className: "squisq-jv-richtext", children: /* @__PURE__ */ jsx25(MarkdownRenderer, { nodes }) });
4773
5492
  }
4774
5493
  function NumberViewer({ value }) {
4775
5494
  if (value === void 0 || value === null) {
4776
- return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-empty", children: "\u2014" });
5495
+ return /* @__PURE__ */ jsx25("span", { className: "squisq-jv-empty", children: "\u2014" });
4777
5496
  }
4778
- return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-value", children: String(value) });
5497
+ return /* @__PURE__ */ jsx25("span", { className: "squisq-jv-value", children: String(value) });
4779
5498
  }
4780
5499
  function BooleanViewer({ value }) {
4781
5500
  const on = Boolean(value);
4782
- return /* @__PURE__ */ jsx24("span", { className: `squisq-jv-toggle squisq-jv-toggle--${on ? "on" : "off"}`, children: on ? "On" : "Off" });
5501
+ return /* @__PURE__ */ jsx25("span", { className: `squisq-jv-toggle squisq-jv-toggle--${on ? "on" : "off"}`, children: on ? "On" : "Off" });
4783
5502
  }
4784
5503
  function EnumViewer({ value, schema }) {
4785
5504
  if (value === void 0 || value === null || value === "") {
4786
- return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-empty", children: "\u2014" });
5505
+ return /* @__PURE__ */ jsx25("span", { className: "squisq-jv-empty", children: "\u2014" });
4787
5506
  }
4788
5507
  const labels = schema.squisq?.enumLabels;
4789
5508
  const display = labels && typeof value === "string" ? labels[value] ?? value : String(value);
4790
- return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-value", children: display });
5509
+ return /* @__PURE__ */ jsx25("span", { className: "squisq-jv-value", children: display });
4791
5510
  }
4792
5511
  function ColorViewer({ value }) {
4793
5512
  if (typeof value !== "string" || value === "") {
4794
- return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-empty", children: "\u2014" });
5513
+ return /* @__PURE__ */ jsx25("span", { className: "squisq-jv-empty", children: "\u2014" });
4795
5514
  }
4796
- return /* @__PURE__ */ jsxs17("span", { className: "squisq-jv-color", children: [
4797
- /* @__PURE__ */ jsx24("span", { className: "squisq-jv-color__swatch", style: { background: value }, "aria-hidden": true }),
4798
- /* @__PURE__ */ jsx24("span", { className: "squisq-jv-color__hex", children: value })
5515
+ return /* @__PURE__ */ jsxs18("span", { className: "squisq-jv-color", children: [
5516
+ /* @__PURE__ */ jsx25("span", { className: "squisq-jv-color__swatch", style: { background: value }, "aria-hidden": true }),
5517
+ /* @__PURE__ */ jsx25("span", { className: "squisq-jv-color__hex", children: value })
4799
5518
  ] });
4800
5519
  }
4801
5520
  function DateViewer({ value, schema }) {
4802
5521
  if (typeof value !== "string" || value === "") {
4803
- return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-empty", children: "\u2014" });
5522
+ return /* @__PURE__ */ jsx25("span", { className: "squisq-jv-empty", children: "\u2014" });
4804
5523
  }
4805
5524
  const fmt = schema.format;
4806
5525
  let display = value;
@@ -4817,31 +5536,31 @@ function DateViewer({ value, schema }) {
4817
5536
  }
4818
5537
  } catch {
4819
5538
  }
4820
- return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-value", children: display });
5539
+ return /* @__PURE__ */ jsx25("span", { className: "squisq-jv-value", children: display });
4821
5540
  }
4822
5541
  function ChipBinViewer({ value, schema }) {
4823
5542
  if (!Array.isArray(value) || value.length === 0) {
4824
- return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-empty", children: "\u2014" });
5543
+ return /* @__PURE__ */ jsx25("span", { className: "squisq-jv-empty", children: "\u2014" });
4825
5544
  }
4826
5545
  const itemSchema = Array.isArray(schema.items) ? schema.items[0] : schema.items;
4827
5546
  const labels = itemSchema?.squisq?.enumLabels;
4828
- return /* @__PURE__ */ jsx24("div", { className: "squisq-jv-chip-bin", children: value.map((item, i) => {
5547
+ return /* @__PURE__ */ jsx25("div", { className: "squisq-jv-chip-bin", children: value.map((item, i) => {
4829
5548
  const label = labels && typeof item === "string" ? labels[item] ?? String(item) : String(item);
4830
- return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-chip", children: label }, i);
5549
+ return /* @__PURE__ */ jsx25("span", { className: "squisq-jv-chip", children: label }, i);
4831
5550
  }) });
4832
5551
  }
4833
5552
  function CardStackViewer(props) {
4834
5553
  const { value, schema, rootSchema, rootData, pointer, density } = props;
4835
5554
  if (!Array.isArray(value) || value.length === 0) {
4836
- return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-empty", children: "No items" });
5555
+ return /* @__PURE__ */ jsx25("span", { className: "squisq-jv-empty", children: "No items" });
4837
5556
  }
4838
5557
  const itemSchema = (Array.isArray(schema.items) ? schema.items[0] : schema.items) ?? {};
4839
5558
  const itemLabel = itemSchema.squisq?.itemLabel;
4840
- return /* @__PURE__ */ jsx24("div", { className: "squisq-jv-card-stack", children: value.map((item, i) => {
5559
+ return /* @__PURE__ */ jsx25("div", { className: "squisq-jv-card-stack", children: value.map((item, i) => {
4841
5560
  const title = resolveItemTitle(itemLabel, item, i);
4842
- return /* @__PURE__ */ jsxs17("div", { className: "squisq-jv-card", children: [
4843
- title ? /* @__PURE__ */ jsx24("h4", { className: "squisq-jv-card__title", children: title }) : null,
4844
- /* @__PURE__ */ jsx24(
5561
+ return /* @__PURE__ */ jsxs18("div", { className: "squisq-jv-card", children: [
5562
+ title ? /* @__PURE__ */ jsx25("h4", { className: "squisq-jv-card__title", children: title }) : null,
5563
+ /* @__PURE__ */ jsx25(
4845
5564
  RenderNode,
4846
5565
  {
4847
5566
  value: item,
@@ -4872,16 +5591,16 @@ function GroupViewer(props) {
4872
5591
  const help = schema.squisq?.help ?? schema.description;
4873
5592
  const obj = (value && typeof value === "object" ? value : {}) ?? {};
4874
5593
  const propEntries = Object.entries(schema.properties ?? {});
4875
- return /* @__PURE__ */ jsxs17("section", { className: "squisq-jv-group", children: [
4876
- title ? /* @__PURE__ */ jsx24("h3", { className: "squisq-jv-group__title", children: title }) : null,
4877
- help ? /* @__PURE__ */ jsx24("p", { className: "squisq-jv-group__help", children: help }) : null,
4878
- propEntries.map(([key, propSchema]) => /* @__PURE__ */ jsx24(Fragment4, { children: /* @__PURE__ */ jsx24(
5594
+ return /* @__PURE__ */ jsxs18("section", { className: "squisq-jv-group", children: [
5595
+ title ? /* @__PURE__ */ jsx25("h3", { className: "squisq-jv-group__title", children: title }) : null,
5596
+ help ? /* @__PURE__ */ jsx25("p", { className: "squisq-jv-group__help", children: help }) : null,
5597
+ propEntries.map(([key, propSchema]) => /* @__PURE__ */ jsx25(Fragment4, { children: /* @__PURE__ */ jsx25(
4879
5598
  RowOrSection,
4880
5599
  {
4881
5600
  label: propSchema.squisq?.label ?? propSchema.title ?? key,
4882
5601
  help: propSchema.squisq?.help ?? propSchema.description,
4883
5602
  kindHint: propSchema,
4884
- children: /* @__PURE__ */ jsx24(
5603
+ children: /* @__PURE__ */ jsx25(
4885
5604
  RenderNode,
4886
5605
  {
4887
5606
  value: obj[key],
@@ -4905,11 +5624,11 @@ function RowOrSection({
4905
5624
  }) {
4906
5625
  const composite = isCompositeKind(kindHint);
4907
5626
  if (composite) {
4908
- return /* @__PURE__ */ jsx24(Fragment5, { children });
5627
+ return /* @__PURE__ */ jsx25(Fragment5, { children });
4909
5628
  }
4910
- return /* @__PURE__ */ jsxs17("div", { className: "squisq-jv-row", children: [
4911
- /* @__PURE__ */ jsx24("div", { className: "squisq-jv-label", title: help, children: label }),
4912
- /* @__PURE__ */ jsx24("div", { children })
5629
+ return /* @__PURE__ */ jsxs18("div", { className: "squisq-jv-row", children: [
5630
+ /* @__PURE__ */ jsx25("div", { className: "squisq-jv-label", title: help, children: label }),
5631
+ /* @__PURE__ */ jsx25("div", { children })
4913
5632
  ] });
4914
5633
  }
4915
5634
  function isCompositeKind(schema) {
@@ -4930,11 +5649,11 @@ function TabsViewer(props) {
4930
5649
  const matchedIndex = pickMatchingBranch(branches, value);
4931
5650
  const branch = branches[matchedIndex];
4932
5651
  if (!branch) {
4933
- return /* @__PURE__ */ jsx24(TextViewer, { ...props });
5652
+ return /* @__PURE__ */ jsx25(TextViewer, { ...props });
4934
5653
  }
4935
- return /* @__PURE__ */ jsxs17("div", { className: "squisq-jv-tabs", children: [
4936
- /* @__PURE__ */ jsx24("span", { className: "squisq-jv-tabs__discriminator", children: branch.squisq?.label ?? branch.title ?? `Option ${matchedIndex + 1}` }),
4937
- /* @__PURE__ */ jsx24(
5654
+ return /* @__PURE__ */ jsxs18("div", { className: "squisq-jv-tabs", children: [
5655
+ /* @__PURE__ */ jsx25("span", { className: "squisq-jv-tabs__discriminator", children: branch.squisq?.label ?? branch.title ?? `Option ${matchedIndex + 1}` }),
5656
+ /* @__PURE__ */ jsx25(
4938
5657
  RenderNode,
4939
5658
  {
4940
5659
  value,
@@ -4998,7 +5717,7 @@ var VIEWERS = {
4998
5717
  };
4999
5718
 
5000
5719
  // src/jsonView/RenderNode.tsx
5001
- import { jsx as jsx25 } from "react/jsx-runtime";
5720
+ import { jsx as jsx26 } from "react/jsx-runtime";
5002
5721
  function RenderNode(props) {
5003
5722
  const resolved = resolveRef(props.schema, props.rootSchema) ?? props.schema;
5004
5723
  if (resolveFlag(resolved.squisq?.hidden, props.rootData)) return null;
@@ -5014,18 +5733,18 @@ function RenderNode(props) {
5014
5733
  };
5015
5734
  if (kind === "group" || kind === "card") {
5016
5735
  const Group = Viewer;
5017
- return /* @__PURE__ */ jsx25(Group, { ...viewerProps, suppressTitle: props.suppressTopGroupTitle });
5736
+ return /* @__PURE__ */ jsx26(Group, { ...viewerProps, suppressTitle: props.suppressTopGroupTitle });
5018
5737
  }
5019
- return /* @__PURE__ */ jsx25(Viewer, { ...viewerProps });
5738
+ return /* @__PURE__ */ jsx26(Viewer, { ...viewerProps });
5020
5739
  }
5021
5740
 
5022
5741
  // src/jsonView/JsonView.tsx
5023
- import { jsx as jsx26 } from "react/jsx-runtime";
5742
+ import { jsx as jsx27 } from "react/jsx-runtime";
5024
5743
  function JsonView(props) {
5025
5744
  const { schema, value, theme, surface, density = "comfortable", className } = props;
5026
5745
  const { style } = useJsonViewTokens(theme, surface);
5027
5746
  const cls = "squisq-json-view" + (density === "compact" ? " squisq-json-view--compact" : "") + (className ? ` ${className}` : "");
5028
- return /* @__PURE__ */ jsx26("div", { className: cls, style, children: /* @__PURE__ */ jsx26(
5747
+ return /* @__PURE__ */ jsx27("div", { className: cls, style, children: /* @__PURE__ */ jsx27(
5029
5748
  RenderNode,
5030
5749
  {
5031
5750
  value,
@@ -5061,7 +5780,7 @@ export {
5061
5780
  SocialCaptionOverlay,
5062
5781
  TableLayer,
5063
5782
  TextLayer,
5064
- VIEWPORT,
5783
+ TreeLayer,
5065
5784
  VideoLayer,
5066
5785
  formatTime,
5067
5786
  getAnimationStyle,