@bendyline/squisq-react 1.3.2 → 1.4.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 (46) hide show
  1. package/README.md +57 -23
  2. package/dist/index.d.ts +131 -26
  3. package/dist/index.js +1475 -755
  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 +49 -13
  8. package/dist/squisq-player.global.js.map +1 -1
  9. package/dist/standalone-source.js +1 -1
  10. package/dist/styles/index.css +2263 -0
  11. package/package.json +9 -5
  12. package/src/BlockRenderer.tsx +15 -7
  13. package/src/DocPlayer.tsx +222 -55
  14. package/src/DocPlayerWithSidebar.tsx +21 -9
  15. package/src/DocProgressBar.tsx +21 -3
  16. package/src/LinearDocView.tsx +69 -206
  17. package/src/MarkdownRenderer.tsx +182 -41
  18. package/src/MediaClipLayer.tsx +135 -0
  19. package/src/__tests__/DocPlayer.test.tsx +81 -0
  20. package/src/__tests__/DocPlayerStylesSentinel.test.tsx +41 -0
  21. package/src/__tests__/DocProgressBar.test.tsx +76 -0
  22. package/src/__tests__/LinearDocView.test.tsx +53 -1
  23. package/src/__tests__/MarkdownRenderer.test.tsx +113 -1
  24. package/src/__tests__/PathLayer.test.tsx +73 -0
  25. package/src/__tests__/fillStyle.test.tsx +112 -0
  26. package/src/__tests__/transitionStyles.test.ts +125 -0
  27. package/src/__tests__/useDocPlayback.transition.test.ts +70 -0
  28. package/src/__tests__/useJsonViewTokens.test.ts +41 -0
  29. package/src/__tests__/useSlideSwipe.test.ts +81 -0
  30. package/src/hooks/{AudioProvider.ts → AudioController.ts} +3 -3
  31. package/src/hooks/index.ts +7 -2
  32. package/src/hooks/useAudioSync.ts +19 -5
  33. package/src/hooks/useDocPlayback.ts +81 -100
  34. package/src/hooks/useMediaSchedule.ts +39 -0
  35. package/src/hooks/useSlideSwipe.ts +265 -0
  36. package/src/index.ts +8 -1
  37. package/src/jsonView/useJsonViewTokens.ts +6 -31
  38. package/src/layers/ImageLayer.tsx +11 -1
  39. package/src/layers/PathLayer.tsx +146 -0
  40. package/src/layers/ShapeLayer.tsx +27 -5
  41. package/src/layers/TextLayer.tsx +395 -22
  42. package/src/layers/VideoLayer.tsx +16 -9
  43. package/src/layers/index.ts +1 -0
  44. package/src/standalone-entry.tsx +1 -1
  45. package/src/styles/doc-animations.css +1936 -35
  46. package/src/utils/fillStyle.tsx +148 -0
package/dist/index.js CHANGED
@@ -1,8 +1,158 @@
1
1
  // src/DocPlayer.tsx
2
- import { Fragment as Fragment2, useRef as useRef5, useState as useState7, useEffect as useEffect7, useCallback as useCallback5, useMemo as useMemo7 } from "react";
3
- import { isTemplateBlock as isTemplateBlock2, getCaptionAtTime as getCaptionAtTime2 } from "@bendyline/squisq/schemas";
2
+ import { Fragment as Fragment3, useRef as useRef7, useState as useState7, useEffect as useEffect8, useCallback as useCallback6, useMemo as useMemo9 } from "react";
3
+ import {
4
+ isTemplateBlock as isTemplateBlock2,
5
+ getCaptionAtTime as getCaptionAtTime2,
6
+ resolveMediaSchedule,
7
+ getDocPlaybackDuration
8
+ } from "@bendyline/squisq/schemas";
9
+
10
+ // src/MediaClipLayer.tsx
11
+ import { useEffect as useEffect2, useRef } from "react";
12
+
13
+ // src/hooks/MediaContext.tsx
14
+ import { createContext, useContext, useState, useEffect, useMemo } from "react";
15
+ var MediaContext = createContext(null);
16
+ function useMediaProvider() {
17
+ return useContext(MediaContext);
18
+ }
19
+ function useMediaUrl(relativePath, basePath) {
20
+ const provider = useMediaProvider();
21
+ const safePath = typeof relativePath === "string" ? relativePath : "";
22
+ const isAbsolute = !safePath || safePath.startsWith("http") || safePath.startsWith("/") || safePath.startsWith("data:") || safePath.startsWith("blob:");
23
+ const fallback = useMemo(
24
+ () => isAbsolute ? safePath : `${basePath}/${safePath}`,
25
+ [isAbsolute, safePath, basePath]
26
+ );
27
+ const needsProvider = !isAbsolute && !!provider;
28
+ const [url, setUrl] = useState(fallback);
29
+ useEffect(() => {
30
+ if (!needsProvider) {
31
+ setUrl(fallback);
32
+ return;
33
+ }
34
+ let cancelled = false;
35
+ provider.resolveUrl(safePath).then((resolved) => {
36
+ if (!cancelled) setUrl(resolved);
37
+ });
38
+ return () => {
39
+ cancelled = true;
40
+ };
41
+ }, [needsProvider, provider, safePath, fallback]);
42
+ return needsProvider ? url : fallback;
43
+ }
44
+
45
+ // src/hooks/useMediaSchedule.ts
46
+ import { useMemo as useMemo2 } from "react";
47
+ function useMediaSchedule(schedule, currentTime) {
48
+ const activeIds = useMemo2(() => {
49
+ const ids = /* @__PURE__ */ new Set();
50
+ for (const c of schedule) {
51
+ if (currentTime >= c.absoluteStart && currentTime < c.absoluteEnd) ids.add(c.id);
52
+ }
53
+ return ids;
54
+ }, [schedule, currentTime]);
55
+ return { renderClips: schedule, activeIds };
56
+ }
57
+
58
+ // src/MediaClipLayer.tsx
59
+ import { jsx } from "react/jsx-runtime";
60
+ var DRIFT = 0.25;
61
+ function MediaClipLayer({
62
+ schedule,
63
+ currentTime,
64
+ isPlaying,
65
+ basePath,
66
+ renderMode = false
67
+ }) {
68
+ const { renderClips, activeIds } = useMediaSchedule(schedule, currentTime);
69
+ if (renderClips.length === 0) return null;
70
+ return /* @__PURE__ */ jsx("div", { className: "doc-player__media-clips", "aria-hidden": true, children: renderClips.map((clip) => /* @__PURE__ */ jsx(
71
+ MediaClipElement,
72
+ {
73
+ clip,
74
+ active: activeIds.has(clip.id),
75
+ currentTime,
76
+ isPlaying,
77
+ basePath,
78
+ renderMode
79
+ },
80
+ clip.id
81
+ )) });
82
+ }
83
+ function MediaClipElement({
84
+ clip,
85
+ active,
86
+ currentTime,
87
+ isPlaying,
88
+ basePath,
89
+ renderMode
90
+ }) {
91
+ const ref = useRef(null);
92
+ const src = useMediaUrl(clip.src, basePath);
93
+ useEffect2(() => {
94
+ const el = ref.current;
95
+ if (!el) return;
96
+ if (!active) {
97
+ if (!el.paused) el.pause();
98
+ return;
99
+ }
100
+ const target = Math.max(0, clip.sourceIn + (currentTime - clip.absoluteStart));
101
+ if (renderMode || Math.abs(el.currentTime - target) > DRIFT) {
102
+ try {
103
+ el.currentTime = target;
104
+ } catch {
105
+ }
106
+ }
107
+ if (isPlaying && !renderMode) {
108
+ const p = el.play();
109
+ if (p) p.catch(() => {
110
+ });
111
+ } else {
112
+ el.pause();
113
+ }
114
+ }, [active, currentTime, isPlaying, renderMode, clip.sourceIn, clip.absoluteStart]);
115
+ const isVideo = clip.kind === "video";
116
+ const common = {
117
+ ref,
118
+ src,
119
+ preload: "auto",
120
+ "data-clip-id": clip.id,
121
+ "data-abs-start": clip.absoluteStart,
122
+ "data-abs-end": clip.absoluteEnd,
123
+ "data-source-in": clip.sourceIn
124
+ };
125
+ if (isVideo) {
126
+ return /* @__PURE__ */ jsx(
127
+ "video",
128
+ {
129
+ ...common,
130
+ muted: true,
131
+ playsInline: true,
132
+ style: {
133
+ position: "absolute",
134
+ inset: 0,
135
+ width: "100%",
136
+ height: "100%",
137
+ objectFit: "cover",
138
+ zIndex: 0,
139
+ pointerEvents: "none"
140
+ }
141
+ }
142
+ );
143
+ }
144
+ return /* @__PURE__ */ jsx("audio", { ...common, muted: renderMode, style: { position: "absolute", width: 0, height: 0 } });
145
+ }
146
+
147
+ // src/DocPlayer.tsx
4
148
  import { applySurface as applySurface2 } from "@bendyline/squisq/schemas";
5
149
 
150
+ // src/BlockRenderer.tsx
151
+ import { resolveTransitionDuration } from "@bendyline/squisq/schemas";
152
+
153
+ // src/layers/ImageLayer.tsx
154
+ import { cssFilterForTreatment } from "@bendyline/squisq/doc";
155
+
6
156
  // src/utils/animationUtils.ts
7
157
  import {
8
158
  getAnimationStyle,
@@ -38,40 +188,8 @@ function getAnchorOffset(anchor, width, height) {
38
188
  }
39
189
  }
40
190
 
41
- // src/hooks/MediaContext.tsx
42
- import { createContext, useContext, useState, useEffect, useMemo } from "react";
43
- var MediaContext = createContext(null);
44
- function useMediaProvider() {
45
- return useContext(MediaContext);
46
- }
47
- function useMediaUrl(relativePath, basePath) {
48
- const provider = useMediaProvider();
49
- const safePath = typeof relativePath === "string" ? relativePath : "";
50
- const isAbsolute = !safePath || safePath.startsWith("http") || safePath.startsWith("/") || safePath.startsWith("data:") || safePath.startsWith("blob:");
51
- const fallback = useMemo(
52
- () => isAbsolute ? safePath : `${basePath}/${safePath}`,
53
- [isAbsolute, safePath, basePath]
54
- );
55
- const needsProvider = !isAbsolute && !!provider;
56
- const [url, setUrl] = useState(fallback);
57
- useEffect(() => {
58
- if (!needsProvider) {
59
- setUrl(fallback);
60
- return;
61
- }
62
- let cancelled = false;
63
- provider.resolveUrl(safePath).then((resolved) => {
64
- if (!cancelled) setUrl(resolved);
65
- });
66
- return () => {
67
- cancelled = true;
68
- };
69
- }, [needsProvider, provider, safePath, fallback]);
70
- return needsProvider ? url : fallback;
71
- }
72
-
73
191
  // src/layers/ImageLayer.tsx
74
- import { jsx } from "react/jsx-runtime";
192
+ import { jsx as jsx2 } from "react/jsx-runtime";
75
193
  function ImageLayer({ layer, basePath, viewport, blockTime }) {
76
194
  const { content, position, animation } = layer;
77
195
  const x = resolveValue(position.x, viewport.width);
@@ -83,13 +201,14 @@ function ImageLayer({ layer, basePath, viewport, blockTime }) {
83
201
  const finalY = y + offset.y;
84
202
  const src = useMediaUrl(content.src, basePath);
85
203
  const animStyle = getAnimationStyle(animation, blockTime);
204
+ const filter = cssFilterForTreatment(content.treatment, content.blur);
86
205
  const preserveAspectRatio = getPreserveAspectRatio(content.fit);
87
206
  const isCover = content.fit === "cover";
88
207
  const isSpatialAnim = animation && SPATIAL_ANIMATION_TYPES.has(animation.type);
89
208
  if (isCover && isSpatialAnim && animation) {
90
209
  const kbAnim = remapToKenBurns(animation);
91
210
  const kbStyle = getAnimationStyle(kbAnim, blockTime);
92
- return /* @__PURE__ */ jsx("g", { className: "block-layer block-layer--image", "data-layer-id": layer.id, children: /* @__PURE__ */ jsx("foreignObject", { x: finalX, y: finalY, width, height, children: /* @__PURE__ */ jsx(
211
+ return /* @__PURE__ */ jsx2("g", { className: "block-layer block-layer--image", "data-layer-id": layer.id, children: /* @__PURE__ */ jsx2("foreignObject", { x: finalX, y: finalY, width, height, children: /* @__PURE__ */ jsx2(
93
212
  "div",
94
213
  {
95
214
  style: {
@@ -97,7 +216,7 @@ function ImageLayer({ layer, basePath, viewport, blockTime }) {
97
216
  height: `${height}px`,
98
217
  overflow: "hidden"
99
218
  },
100
- children: /* @__PURE__ */ jsx(
219
+ children: /* @__PURE__ */ jsx2(
101
220
  "img",
102
221
  {
103
222
  src,
@@ -111,6 +230,7 @@ function ImageLayer({ layer, basePath, viewport, blockTime }) {
111
230
  display: "block",
112
231
  pointerEvents: "none",
113
232
  transformOrigin: "center center",
233
+ ...filter ? { filter } : {},
114
234
  ...kbStyle.style
115
235
  }
116
236
  }
@@ -119,13 +239,13 @@ function ImageLayer({ layer, basePath, viewport, blockTime }) {
119
239
  ) }) });
120
240
  }
121
241
  if (isCover) {
122
- return /* @__PURE__ */ jsx(
242
+ return /* @__PURE__ */ jsx2(
123
243
  "g",
124
244
  {
125
245
  className: `block-layer block-layer--image ${animStyle.className}`,
126
246
  style: animStyle.style,
127
247
  "data-layer-id": layer.id,
128
- children: /* @__PURE__ */ jsx("foreignObject", { x: finalX, y: finalY, width, height, children: /* @__PURE__ */ jsx(
248
+ children: /* @__PURE__ */ jsx2("foreignObject", { x: finalX, y: finalY, width, height, children: /* @__PURE__ */ jsx2(
129
249
  "img",
130
250
  {
131
251
  src,
@@ -136,20 +256,24 @@ function ImageLayer({ layer, basePath, viewport, blockTime }) {
136
256
  objectFit: "cover",
137
257
  objectPosition: "center",
138
258
  display: "block",
139
- pointerEvents: "none"
259
+ pointerEvents: "none",
260
+ ...filter ? { filter } : {},
261
+ // Over-scan blurred imagery so the soft edges never reveal
262
+ // the frame behind the layer.
263
+ ...content.blur && content.blur > 0 ? { transform: "scale(1.06)" } : {}
140
264
  }
141
265
  }
142
266
  ) })
143
267
  }
144
268
  );
145
269
  }
146
- return /* @__PURE__ */ jsx(
270
+ return /* @__PURE__ */ jsx2(
147
271
  "g",
148
272
  {
149
273
  className: `block-layer block-layer--image ${animStyle.className}`,
150
274
  style: animStyle.style,
151
275
  "data-layer-id": layer.id,
152
- children: /* @__PURE__ */ jsx(
276
+ children: /* @__PURE__ */ jsx2(
153
277
  "image",
154
278
  {
155
279
  href: src,
@@ -158,7 +282,7 @@ function ImageLayer({ layer, basePath, viewport, blockTime }) {
158
282
  width,
159
283
  height,
160
284
  preserveAspectRatio,
161
- style: { pointerEvents: "none" }
285
+ style: { pointerEvents: "none", ...filter ? { filter } : {} }
162
286
  }
163
287
  )
164
288
  }
@@ -192,16 +316,207 @@ function remapToKenBurns(anim) {
192
316
  }
193
317
 
194
318
  // src/layers/TextLayer.tsx
319
+ import { useMemo as useMemo3 } from "react";
195
320
  import { DEFAULT_DOC_FONT } from "@bendyline/squisq/schemas";
196
- import { jsx as jsx2, jsxs } from "react/jsx-runtime";
197
- function TextLayer({ layer, viewport, blockTime }) {
321
+ import {
322
+ parseHtmlToNodes,
323
+ sanitizeHtmlNodes,
324
+ stringifyHtmlNodes
325
+ } from "@bendyline/squisq/markdown";
326
+ import {
327
+ hasIconMarker,
328
+ splitIconMarkers,
329
+ stripIconMarkers,
330
+ iconClass
331
+ } from "@bendyline/squisq/icon-marker";
332
+
333
+ // src/utils/fillStyle.tsx
334
+ import { jsx as jsx3, jsxs } from "react/jsx-runtime";
335
+ function borderDashArray(style, strokeWidth) {
336
+ if (!style || style === "solid") return void 0;
337
+ const w = Math.max(1, strokeWidth ?? 1);
338
+ if (style === "dotted") return `${w} ${w * 2}`;
339
+ return `${w * 3} ${w * 2}`;
340
+ }
341
+ function gradientVector(angle = 0) {
342
+ const a = angle * Math.PI / 180;
343
+ const dx = Math.sin(a);
344
+ const dy = Math.cos(a);
345
+ return { x1: 0.5 - dx / 2, y1: 0.5 - dy / 2, x2: 0.5 + dx / 2, y2: 0.5 + dy / 2 };
346
+ }
347
+ function gradientDefId(layerId) {
348
+ return `squisq-grad-${layerId}`;
349
+ }
350
+ function resolveFill(layerId, color, gradient, pattern) {
351
+ if (pattern) {
352
+ const id = `squisq-pattern-${layerId}`;
353
+ return { fill: `url(#${id})`, def: patternDef(id, pattern) };
354
+ }
355
+ if (gradient) {
356
+ const id = gradientDefId(layerId);
357
+ const v = gradientVector(gradient.angle);
358
+ return {
359
+ fill: `url(#${id})`,
360
+ def: /* @__PURE__ */ jsxs("linearGradient", { id, x1: v.x1, y1: v.y1, x2: v.x2, y2: v.y2, children: [
361
+ /* @__PURE__ */ jsx3("stop", { offset: "0%", stopColor: gradient.from }),
362
+ /* @__PURE__ */ jsx3("stop", { offset: "100%", stopColor: gradient.to })
363
+ ] })
364
+ };
365
+ }
366
+ return { fill: color, def: null };
367
+ }
368
+ function patternDef(id, pattern) {
369
+ const size = pattern.size ?? 24;
370
+ const opacity = pattern.opacity ?? 1;
371
+ const color = pattern.color;
372
+ return /* @__PURE__ */ jsxs(
373
+ "pattern",
374
+ {
375
+ id,
376
+ width: size,
377
+ height: size,
378
+ patternUnits: "userSpaceOnUse",
379
+ patternTransform: pattern.kind === "diagonal" ? "rotate(45)" : void 0,
380
+ children: [
381
+ pattern.kind === "dots" && /* @__PURE__ */ jsx3(
382
+ "circle",
383
+ {
384
+ cx: size / 2,
385
+ cy: size / 2,
386
+ r: Math.max(1, size / 12),
387
+ fill: color,
388
+ opacity
389
+ }
390
+ ),
391
+ pattern.kind === "grid" && /* @__PURE__ */ jsx3(
392
+ "path",
393
+ {
394
+ d: `M ${size} 0 L 0 0 0 ${size}`,
395
+ fill: "none",
396
+ stroke: color,
397
+ strokeWidth: 1,
398
+ opacity
399
+ }
400
+ ),
401
+ pattern.kind === "diagonal" && /* @__PURE__ */ jsx3("line", { x1: 0, y1: 0, x2: 0, y2: size, stroke: color, strokeWidth: 1, opacity })
402
+ ]
403
+ }
404
+ );
405
+ }
406
+ function resolveShapeFilter(layerId, filter) {
407
+ if (!filter || filter.type !== "noise") return { filterAttr: void 0, def: null };
408
+ const id = `squisq-noise-${layerId}`;
409
+ const opacity = filter.opacity ?? 0.05;
410
+ return {
411
+ filterAttr: `url(#${id})`,
412
+ def: /* @__PURE__ */ jsxs("filter", { id, x: "0%", y: "0%", width: "100%", height: "100%", children: [
413
+ /* @__PURE__ */ jsx3(
414
+ "feTurbulence",
415
+ {
416
+ type: "fractalNoise",
417
+ baseFrequency: filter.baseFrequency ?? 0.8,
418
+ numOctaves: 2,
419
+ stitchTiles: "stitch",
420
+ result: "noise"
421
+ }
422
+ ),
423
+ /* @__PURE__ */ jsx3("feColorMatrix", { in: "noise", type: "saturate", values: "0", result: "mono" }),
424
+ /* @__PURE__ */ jsx3("feComponentTransfer", { in: "mono", result: "faded", children: /* @__PURE__ */ jsx3("feFuncA", { type: "linear", slope: opacity, intercept: 0 }) }),
425
+ /* @__PURE__ */ jsx3("feComposite", { in: "faded", in2: "SourceGraphic", operator: "in" })
426
+ ] })
427
+ };
428
+ }
429
+
430
+ // src/layers/TextLayer.tsx
431
+ import { Fragment, jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
432
+ function TextLayer(props) {
433
+ if (props.layer.content.html?.trim()) return /* @__PURE__ */ jsx4(RichTextLayer, { ...props });
434
+ if (hasIconMarker(props.layer.content.text ?? "")) return /* @__PURE__ */ jsx4(IconTextLayer, { ...props });
435
+ return /* @__PURE__ */ jsx4(PlainTextLayer, { ...props });
436
+ }
437
+ function escapeHtml(value) {
438
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
439
+ }
440
+ function iconRunsToHtml(text) {
441
+ return splitIconMarkers(text).map(
442
+ (run) => run.type === "icon" ? `<i class="${iconClass(run.family, run.name)}" aria-hidden="true"></i>` : escapeHtml(run.text).replace(/\n/g, "<br>")
443
+ ).join("");
444
+ }
445
+ function IconTextLayer({ layer, viewport, blockTime }) {
198
446
  const { content, position, animation } = layer;
199
447
  const { text, style } = content;
200
- const x = resolveValue(position.x, viewport.width);
201
- const y = resolveValue(position.y, viewport.height);
202
- const maxWidth = position.width ? resolveValue(position.width, viewport.width) : void 0;
448
+ const rawX = resolveValue(position.x, viewport.width);
449
+ const rawY = resolveValue(position.y, viewport.height);
450
+ const boxWidth = position.width != null ? resolveValue(position.width, viewport.width) : viewport.width;
451
+ const anchor = position.anchor ?? "top-left";
452
+ const lineHeight = style.lineHeight || 1.4;
453
+ const lineHeightPx = style.fontSize * lineHeight;
454
+ const padding = style.padding ?? 0;
455
+ const plain = stripIconMarkers(text ?? "");
456
+ const lines = plain.split("\n").flatMap((line) => wrapText(line, style.fontSize, boxWidth));
457
+ const boxHeight = position.height != null ? resolveValue(position.height, viewport.height) : Math.max(lineHeightPx, lines.length * lineHeightPx) + padding * 2;
458
+ const boxX = rawX - anchorAxis(anchor, boxWidth, "x");
459
+ const boxY = rawY - anchorAxis(anchor, boxHeight, "y");
460
+ const animStyle = getAnimationStyle(animation, blockTime);
461
+ const html = useMemo3(() => iconRunsToHtml(text ?? ""), [text]);
462
+ const verticalJustify = style.verticalAlign === "top" ? "flex-start" : style.verticalAlign === "bottom" ? "flex-end" : "center";
463
+ const boxStyle = {
464
+ boxSizing: "border-box",
465
+ width: "100%",
466
+ height: "100%",
467
+ display: "flex",
468
+ flexDirection: "column",
469
+ justifyContent: verticalJustify,
470
+ padding,
471
+ color: style.color,
472
+ fontSize: `${style.fontSize}px`,
473
+ fontFamily: style.fontFamily || DEFAULT_DOC_FONT,
474
+ fontWeight: style.fontWeight || "normal",
475
+ fontStyle: style.fontStyle || "normal",
476
+ lineHeight,
477
+ textAlign: style.textAlign ?? "left",
478
+ ...style.shadow ? { textShadow: "0 2px 3px rgba(0,0,0,0.7)" } : {},
479
+ ...animStyle.style
480
+ };
481
+ return /* @__PURE__ */ jsx4("g", { className: `block-layer block-layer--text ${animStyle.className}`, "data-layer-id": layer.id, children: /* @__PURE__ */ jsx4(
482
+ "foreignObject",
483
+ {
484
+ x: boxX,
485
+ y: boxY,
486
+ width: boxWidth,
487
+ height: boxHeight,
488
+ style: { overflow: "visible" },
489
+ children: /* @__PURE__ */ jsx4(
490
+ "div",
491
+ {
492
+ ...{ xmlns: "http://www.w3.org/1999/xhtml" },
493
+ style: boxStyle,
494
+ children: /* @__PURE__ */ jsx4(
495
+ "div",
496
+ {
497
+ style: { width: "100%", whiteSpace: "pre-wrap", wordBreak: "break-word" },
498
+ "aria-label": plain,
499
+ dangerouslySetInnerHTML: { __html: html }
500
+ }
501
+ )
502
+ }
503
+ )
504
+ }
505
+ ) });
506
+ }
507
+ function PlainTextLayer({ layer, viewport, blockTime }) {
508
+ const { content, position, animation } = layer;
509
+ const { text, style } = content;
510
+ const rawX = resolveValue(position.x, viewport.width);
511
+ const rawY = resolveValue(position.y, viewport.height);
512
+ const boxWidth = position.width != null ? resolveValue(position.width, viewport.width) : void 0;
513
+ const boxHeight = position.height != null ? resolveValue(position.height, viewport.height) : void 0;
514
+ const maxWidth = boxWidth;
203
515
  const textAnchor = getTextAnchor(style.textAlign, position.anchor);
204
- const dominantBaseline = getDominantBaseline(position.anchor);
516
+ const dominantBaseline = getDominantBaseline(style.verticalAlign, position.anchor);
517
+ const anchor = position.anchor ?? "top-left";
518
+ const x = pivotX(rawX, boxWidth, anchor, textAnchor);
519
+ const y = pivotY(rawY, boxHeight, anchor, dominantBaseline);
205
520
  const animStyle = getAnimationStyle(animation, blockTime);
206
521
  const rawLines = (text ?? "").split("\n");
207
522
  let lines = maxWidth ? rawLines.reduce(
@@ -219,25 +534,32 @@ function TextLayer({ layer, viewport, blockTime }) {
219
534
  fontSize: `${style.fontSize}px`,
220
535
  fontFamily: style.fontFamily || DEFAULT_DOC_FONT,
221
536
  fontWeight: style.fontWeight || "normal",
537
+ fontStyle: style.fontStyle || "normal",
222
538
  fill: style.color,
223
539
  ...animStyle.style
224
540
  };
225
541
  const filterId = style.shadow ? `shadow-${layer.id}` : void 0;
226
- return /* @__PURE__ */ jsxs("g", { className: `block-layer block-layer--text ${animStyle.className}`, "data-layer-id": layer.id, children: [
227
- style.shadow && /* @__PURE__ */ jsx2("defs", { children: /* @__PURE__ */ jsx2("filter", { id: filterId, x: "-20%", y: "-20%", width: "140%", height: "140%", children: /* @__PURE__ */ jsx2("feDropShadow", { dx: "0", dy: "2", stdDeviation: "3", floodColor: "rgba(0,0,0,0.7)" }) }) }),
228
- style.background && /* @__PURE__ */ jsx2(
229
- "rect",
542
+ return /* @__PURE__ */ jsxs2("g", { className: `block-layer block-layer--text ${animStyle.className}`, "data-layer-id": layer.id, children: [
543
+ 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
+ /* @__PURE__ */ jsx4(
545
+ TextBox,
230
546
  {
231
- x: x - (style.padding || 16),
232
- y: y - style.fontSize - (style.padding || 16),
233
- width: getTextBoxWidth(lines, style) + (style.padding || 16) * 2,
234
- height: lines.length * lineHeightPx + (style.padding || 16) * 2,
235
- fill: style.background,
236
- rx: 4,
237
- ry: 4
547
+ layerId: layer.id,
548
+ style,
549
+ box: boxWidth != null && boxHeight != null ? {
550
+ x: rawX - anchorAxis(anchor, boxWidth, "x"),
551
+ y: rawY - anchorAxis(anchor, boxHeight, "y"),
552
+ width: boxWidth,
553
+ height: boxHeight
554
+ } : {
555
+ x: x - (style.padding || 16),
556
+ y: y - style.fontSize - (style.padding || 16),
557
+ width: getTextBoxWidth(lines, style) + (style.padding || 16) * 2,
558
+ height: lines.length * lineHeightPx + (style.padding || 16) * 2
559
+ }
238
560
  }
239
561
  ),
240
- /* @__PURE__ */ jsx2(
562
+ /* @__PURE__ */ jsx4(
241
563
  "text",
242
564
  {
243
565
  x,
@@ -246,7 +568,7 @@ function TextLayer({ layer, viewport, blockTime }) {
246
568
  dominantBaseline,
247
569
  style: textStyles,
248
570
  filter: filterId ? `url(#${filterId})` : void 0,
249
- children: lines.map((line, i) => /* @__PURE__ */ jsxs("tspan", { x, dy: i === 0 ? 0 : lineHeightPx, children: [
571
+ children: lines.map((line, i) => /* @__PURE__ */ jsxs2("tspan", { x, dy: i === 0 ? 0 : lineHeightPx, children: [
250
572
  line || "\xA0",
251
573
  " "
252
574
  ] }, i))
@@ -254,6 +576,102 @@ function TextLayer({ layer, viewport, blockTime }) {
254
576
  )
255
577
  ] });
256
578
  }
579
+ function RichTextLayer({ layer, viewport, blockTime }) {
580
+ const { content, position, animation } = layer;
581
+ const { html, style } = content;
582
+ const rawX = resolveValue(position.x, viewport.width);
583
+ const rawY = resolveValue(position.y, viewport.height);
584
+ const boxWidth = position.width != null ? resolveValue(position.width, viewport.width) : viewport.width;
585
+ const boxHeight = position.height != null ? resolveValue(position.height, viewport.height) : style.fontSize * (style.lineHeight || 1.4) * 2;
586
+ const anchor = position.anchor ?? "top-left";
587
+ const boxX = rawX - anchorAxis(anchor, boxWidth, "x");
588
+ const boxY = rawY - anchorAxis(anchor, boxHeight, "y");
589
+ const safeHtml = useMemo3(
590
+ () => stringifyHtmlNodes(sanitizeHtmlNodes(parseHtmlToNodes(html ?? ""))),
591
+ [html]
592
+ );
593
+ const animStyle = getAnimationStyle(animation, blockTime);
594
+ const verticalJustify = style.verticalAlign === "middle" ? "center" : style.verticalAlign === "bottom" ? "flex-end" : "flex-start";
595
+ const hasBorder = !!(style.borderColor && (style.borderWidth ?? 0) > 0);
596
+ const boxStyle = {
597
+ boxSizing: "border-box",
598
+ width: "100%",
599
+ height: "100%",
600
+ display: "flex",
601
+ flexDirection: "column",
602
+ justifyContent: verticalJustify,
603
+ padding: style.padding ?? 0,
604
+ color: style.color,
605
+ fontSize: `${style.fontSize}px`,
606
+ fontFamily: style.fontFamily || DEFAULT_DOC_FONT,
607
+ fontWeight: style.fontWeight || "normal",
608
+ fontStyle: style.fontStyle || "normal",
609
+ lineHeight: style.lineHeight || 1.4,
610
+ textAlign: style.textAlign ?? "left",
611
+ overflow: "hidden",
612
+ ...style.background ? { background: style.background } : {},
613
+ ...hasBorder ? {
614
+ border: `${style.borderWidth}px ${style.borderStyle ?? "solid"} ${style.borderColor}`,
615
+ borderRadius: 4
616
+ } : {},
617
+ ...style.shadow ? { textShadow: "0 2px 3px rgba(0,0,0,0.7)" } : {},
618
+ ...animStyle.style
619
+ };
620
+ const cls = `squisq-rich-text-${cssId(layer.id)}`;
621
+ const scopedCss = `.${cls}{margin:0}.${cls} p{margin:0 0 .4em}.${cls} h1,.${cls} h2,.${cls} h3,.${cls} h4,.${cls} h5,.${cls} h6{margin:0 0 .3em;line-height:1.2}.${cls} ul,.${cls} ol{margin:0 0 .4em;padding-left:1.2em;list-style-position:inside}.${cls} li{margin:0}.${cls} li>p{display:inline;margin:0}.${cls} *:first-child{margin-top:0}.${cls} *:last-child{margin-bottom:0}.${cls} a{color:inherit;text-decoration:underline}`;
622
+ return /* @__PURE__ */ jsx4("g", { className: `block-layer block-layer--text ${animStyle.className}`, "data-layer-id": layer.id, children: /* @__PURE__ */ jsx4("foreignObject", { x: boxX, y: boxY, width: boxWidth, height: boxHeight, children: /* @__PURE__ */ jsxs2(
623
+ "div",
624
+ {
625
+ ...{ xmlns: "http://www.w3.org/1999/xhtml" },
626
+ style: boxStyle,
627
+ children: [
628
+ /* @__PURE__ */ jsx4("style", { children: scopedCss }),
629
+ /* @__PURE__ */ jsx4(
630
+ "div",
631
+ {
632
+ className: cls,
633
+ "aria-label": content.text,
634
+ style: { width: "100%", whiteSpace: "pre-wrap", wordBreak: "break-word" },
635
+ dangerouslySetInnerHTML: { __html: safeHtml }
636
+ }
637
+ )
638
+ ]
639
+ }
640
+ ) }) });
641
+ }
642
+ function cssId(id) {
643
+ return id.replace(/[^a-zA-Z0-9_-]/g, "-");
644
+ }
645
+ function TextBox({
646
+ layerId,
647
+ style,
648
+ box
649
+ }) {
650
+ const hasFill = !!(style.background || style.backgroundGradient);
651
+ const hasBorder = !!(style.borderColor && (style.borderWidth ?? 0) > 0);
652
+ if (!hasFill && !hasBorder) return null;
653
+ const { fill, def } = resolveFill(layerId, style.background, style.backgroundGradient);
654
+ const dash = borderDashArray(style.borderStyle, style.borderWidth);
655
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
656
+ def && /* @__PURE__ */ jsx4("defs", { children: def }),
657
+ /* @__PURE__ */ jsx4(
658
+ "rect",
659
+ {
660
+ x: box.x,
661
+ y: box.y,
662
+ width: box.width,
663
+ height: box.height,
664
+ fill: hasFill ? fill : "none",
665
+ fillOpacity: hasFill ? style.backgroundOpacity : void 0,
666
+ stroke: hasBorder ? style.borderColor : void 0,
667
+ strokeWidth: hasBorder ? style.borderWidth : void 0,
668
+ strokeDasharray: hasBorder ? dash : void 0,
669
+ rx: 4,
670
+ ry: 4
671
+ }
672
+ )
673
+ ] });
674
+ }
257
675
  function getTextAnchor(align, anchor) {
258
676
  if (align === "center") return "middle";
259
677
  if (align === "right") return "end";
@@ -262,11 +680,33 @@ function getTextAnchor(align, anchor) {
262
680
  if (anchor === "center") return "middle";
263
681
  return "start";
264
682
  }
265
- function getDominantBaseline(anchor) {
683
+ function getDominantBaseline(verticalAlign, anchor) {
684
+ if (verticalAlign === "top") return "text-before-edge";
685
+ if (verticalAlign === "middle") return "middle";
686
+ if (verticalAlign === "bottom") return "text-after-edge";
266
687
  if (anchor?.includes("bottom")) return "text-after-edge";
267
688
  if (anchor === "center") return "middle";
268
689
  return "text-before-edge";
269
690
  }
691
+ function pivotX(rawX, width, anchor, textAnchor) {
692
+ if (width == null) return rawX;
693
+ const boxLeft = rawX - anchorAxis(anchor, width, "x");
694
+ if (textAnchor === "middle") return boxLeft + width / 2;
695
+ if (textAnchor === "end") return boxLeft + width;
696
+ return boxLeft;
697
+ }
698
+ function pivotY(rawY, height, anchor, dominantBaseline) {
699
+ if (height == null) return rawY;
700
+ const boxTop = rawY - anchorAxis(anchor, height, "y");
701
+ if (dominantBaseline === "middle") return boxTop + height / 2;
702
+ if (dominantBaseline === "text-after-edge") return boxTop + height;
703
+ return boxTop;
704
+ }
705
+ function anchorAxis(anchor, size, axis) {
706
+ if (anchor === "center") return size / 2;
707
+ if (axis === "x") return anchor.includes("right") ? size : 0;
708
+ return anchor.includes("bottom") ? size : 0;
709
+ }
270
710
  function getTextBoxWidth(lines, style) {
271
711
  const maxLineLength = Math.max(...lines.map((l) => l.length));
272
712
  return maxLineLength * style.fontSize * 0.55;
@@ -306,7 +746,7 @@ function wrapText(text, fontSize, maxWidth) {
306
746
  }
307
747
 
308
748
  // src/layers/ShapeLayer.tsx
309
- import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
749
+ import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
310
750
  function ShapeLayer({ layer, viewport, blockTime }) {
311
751
  const { content, position, animation } = layer;
312
752
  const rawX = resolveValue(position.x, viewport.width);
@@ -319,14 +759,14 @@ function ShapeLayer({ layer, viewport, blockTime }) {
319
759
  const animStyle = getAnimationStyle(animation, blockTime);
320
760
  const fill = content.fill || "none";
321
761
  const isCSSGradient = typeof fill === "string" && fill.includes("gradient(");
322
- if (content.shape === "rect" && isCSSGradient) {
323
- return /* @__PURE__ */ jsx3(
762
+ if (content.shape === "rect" && isCSSGradient && !content.gradient) {
763
+ return /* @__PURE__ */ jsx5(
324
764
  "g",
325
765
  {
326
766
  className: `block-layer block-layer--shape ${animStyle.className}`,
327
767
  style: animStyle.style,
328
768
  "data-layer-id": layer.id,
329
- children: /* @__PURE__ */ jsx3("foreignObject", { x, y, width, height, children: /* @__PURE__ */ jsx3(
769
+ children: /* @__PURE__ */ jsx5("foreignObject", { x, y, width, height, children: /* @__PURE__ */ jsx5(
330
770
  "div",
331
771
  {
332
772
  style: {
@@ -341,19 +781,34 @@ function ShapeLayer({ layer, viewport, blockTime }) {
341
781
  }
342
782
  );
343
783
  }
344
- const shapeProps = {
784
+ const { fill: fillValue, def: fillDef } = resolveFill(
785
+ layer.id,
345
786
  fill,
787
+ content.gradient,
788
+ content.pattern
789
+ );
790
+ const { filterAttr, def: filterDef } = resolveShapeFilter(layer.id, content.filter);
791
+ const dash = borderDashArray(content.borderStyle, content.strokeWidth);
792
+ const shapeProps = {
793
+ fill: fillValue,
794
+ fillOpacity: content.fillOpacity,
346
795
  stroke: content.stroke,
347
- strokeWidth: content.strokeWidth
796
+ strokeWidth: content.strokeWidth,
797
+ strokeDasharray: dash,
798
+ ...filterAttr ? { filter: filterAttr } : {}
348
799
  };
349
- return /* @__PURE__ */ jsxs2(
800
+ return /* @__PURE__ */ jsxs3(
350
801
  "g",
351
802
  {
352
803
  className: `block-layer block-layer--shape ${animStyle.className}`,
353
804
  style: animStyle.style,
354
805
  "data-layer-id": layer.id,
355
806
  children: [
356
- content.shape === "rect" && /* @__PURE__ */ jsx3(
807
+ (fillDef || filterDef) && /* @__PURE__ */ jsxs3("defs", { children: [
808
+ fillDef,
809
+ filterDef
810
+ ] }),
811
+ content.shape === "rect" && /* @__PURE__ */ jsx5(
357
812
  "rect",
358
813
  {
359
814
  x,
@@ -365,7 +820,7 @@ function ShapeLayer({ layer, viewport, blockTime }) {
365
820
  ...shapeProps
366
821
  }
367
822
  ),
368
- content.shape === "circle" && /* @__PURE__ */ jsx3(
823
+ content.shape === "circle" && /* @__PURE__ */ jsx5(
369
824
  "circle",
370
825
  {
371
826
  cx: x + width / 2,
@@ -374,7 +829,7 @@ function ShapeLayer({ layer, viewport, blockTime }) {
374
829
  ...shapeProps
375
830
  }
376
831
  ),
377
- content.shape === "line" && /* @__PURE__ */ jsx3(
832
+ content.shape === "line" && /* @__PURE__ */ jsx5(
378
833
  "line",
379
834
  {
380
835
  x1: x,
@@ -382,16 +837,108 @@ function ShapeLayer({ layer, viewport, blockTime }) {
382
837
  x2: x + width,
383
838
  y2: y + height,
384
839
  stroke: content.stroke || "#ffffff",
385
- strokeWidth: content.strokeWidth || 2
840
+ strokeWidth: content.strokeWidth || 2,
841
+ strokeDasharray: dash
842
+ }
843
+ )
844
+ ]
845
+ }
846
+ );
847
+ }
848
+
849
+ // src/layers/PathLayer.tsx
850
+ import { markerPath, shapePath } from "@bendyline/squisq/doc";
851
+ import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
852
+ function effectivePath(layer, viewport) {
853
+ const { content, position } = layer;
854
+ if (!content.shapeKind) return content.d;
855
+ const w = position.width ? resolveValue(position.width, viewport.width) : 0;
856
+ const h = position.height ? resolveValue(position.height, viewport.height) : 0;
857
+ const rawX = resolveValue(position.x, viewport.width);
858
+ const rawY = resolveValue(position.y, viewport.height);
859
+ const anchor = getAnchorOffset(position.anchor, w, h);
860
+ const derived = shapePath(content.shapeKind, rawX + anchor.x, rawY + anchor.y, w, h);
861
+ return derived ?? content.d;
862
+ }
863
+ function effectiveMarker(explicit, arrow, end) {
864
+ if (explicit) return explicit;
865
+ const wants = arrow === "both" || arrow === end;
866
+ return wants ? "arrow" : "none";
867
+ }
868
+ function PathLayer({ layer, viewport, blockTime }) {
869
+ const { content, animation, id } = layer;
870
+ const d = effectivePath(layer, viewport);
871
+ const stroke = content.stroke ?? "#1e293b";
872
+ const strokeWidth = content.strokeWidth ?? 2;
873
+ const { fill, def: fillDef } = resolveFill(id, content.fill ?? "none", content.gradient);
874
+ const dash = content.borderStyle ? borderDashArray(content.borderStyle, strokeWidth) : content.dasharray;
875
+ 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");
880
+ return /* @__PURE__ */ jsxs4(
881
+ "g",
882
+ {
883
+ className: `block-layer block-layer--path ${animStyle.className}`,
884
+ style: animStyle.style,
885
+ "data-layer-id": id,
886
+ children: [
887
+ /* @__PURE__ */ jsxs4("defs", { children: [
888
+ fillDef,
889
+ end && /* @__PURE__ */ jsx6(MarkerDef, { id: endId, dir: "end", d: end.d, filled: end.filled, stroke }),
890
+ start && /* @__PURE__ */ jsx6(MarkerDef, { id: startId, dir: "start", d: start.d, filled: start.filled, stroke })
891
+ ] }),
892
+ /* @__PURE__ */ jsx6(
893
+ "path",
894
+ {
895
+ d,
896
+ stroke,
897
+ strokeWidth,
898
+ fill,
899
+ fillOpacity: content.fillOpacity,
900
+ strokeDasharray: dash,
901
+ markerStart: start ? `url(#${startId})` : void 0,
902
+ markerEnd: end ? `url(#${endId})` : void 0
386
903
  }
387
904
  )
388
905
  ]
389
906
  }
390
907
  );
391
908
  }
909
+ function MarkerDef({
910
+ id,
911
+ dir,
912
+ d,
913
+ filled,
914
+ stroke
915
+ }) {
916
+ return /* @__PURE__ */ jsx6(
917
+ "marker",
918
+ {
919
+ id,
920
+ viewBox: "0 0 10 10",
921
+ refX: dir === "end" ? 9 : 1,
922
+ refY: 5,
923
+ markerWidth: 4,
924
+ markerHeight: 4,
925
+ orient: "auto-start-reverse",
926
+ markerUnits: "strokeWidth",
927
+ children: /* @__PURE__ */ jsx6(
928
+ "path",
929
+ {
930
+ d,
931
+ fill: filled ? stroke : "none",
932
+ stroke: filled ? "none" : stroke,
933
+ strokeWidth: filled ? void 0 : 1.5
934
+ }
935
+ )
936
+ }
937
+ );
938
+ }
392
939
 
393
940
  // src/layers/MapLayer.tsx
394
- import { useState as useState2, useEffect as useEffect2 } from "react";
941
+ import { useState as useState2, useEffect as useEffect3 } from "react";
395
942
 
396
943
  // src/utils/mapTileUtils.ts
397
944
  var TILE_PROVIDERS = {
@@ -574,7 +1121,7 @@ function drawAttribution(ctx, text, width, height) {
574
1121
  }
575
1122
 
576
1123
  // src/layers/MapLayer.tsx
577
- import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
1124
+ import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
578
1125
  function MapLayer({ layer, basePath, viewport, blockTime }) {
579
1126
  const { content, position, animation } = layer;
580
1127
  const [mapImageUrl, setMapImageUrl] = useState2(null);
@@ -587,7 +1134,7 @@ function MapLayer({ layer, basePath, viewport, blockTime }) {
587
1134
  const offset = getAnchorOffset(position.anchor, width, height);
588
1135
  const finalX = x + offset.x;
589
1136
  const finalY = y + offset.y;
590
- useEffect2(() => {
1137
+ useEffect3(() => {
591
1138
  let cancelled = false;
592
1139
  if (content.staticSrc) {
593
1140
  const src = content.staticSrc.startsWith("http") ? content.staticSrc : `${basePath}/${content.staticSrc}`;
@@ -632,15 +1179,15 @@ function MapLayer({ layer, basePath, viewport, blockTime }) {
632
1179
  ]);
633
1180
  const animStyle = getAnimationStyle(animation, blockTime);
634
1181
  if (isLoading) {
635
- return /* @__PURE__ */ jsxs3(
1182
+ return /* @__PURE__ */ jsxs5(
636
1183
  "g",
637
1184
  {
638
1185
  className: `block-layer block-layer--map ${animStyle.className}`,
639
1186
  style: animStyle.style,
640
1187
  "data-layer-id": layer.id,
641
1188
  children: [
642
- /* @__PURE__ */ jsx4("rect", { x: finalX, y: finalY, width, height, fill: "#e5e7eb" }),
643
- /* @__PURE__ */ jsx4(
1189
+ /* @__PURE__ */ jsx7("rect", { x: finalX, y: finalY, width, height, fill: "#e5e7eb" }),
1190
+ /* @__PURE__ */ jsx7(
644
1191
  "text",
645
1192
  {
646
1193
  x: finalX + width / 2,
@@ -658,15 +1205,15 @@ function MapLayer({ layer, basePath, viewport, blockTime }) {
658
1205
  );
659
1206
  }
660
1207
  if (error || !mapImageUrl) {
661
- return /* @__PURE__ */ jsxs3(
1208
+ return /* @__PURE__ */ jsxs5(
662
1209
  "g",
663
1210
  {
664
1211
  className: `block-layer block-layer--map ${animStyle.className}`,
665
1212
  style: animStyle.style,
666
1213
  "data-layer-id": layer.id,
667
1214
  children: [
668
- /* @__PURE__ */ jsx4("rect", { x: finalX, y: finalY, width, height, fill: "#fef2f2" }),
669
- /* @__PURE__ */ jsx4(
1215
+ /* @__PURE__ */ jsx7("rect", { x: finalX, y: finalY, width, height, fill: "#fef2f2" }),
1216
+ /* @__PURE__ */ jsx7(
670
1217
  "text",
671
1218
  {
672
1219
  x: finalX + width / 2,
@@ -683,15 +1230,15 @@ function MapLayer({ layer, basePath, viewport, blockTime }) {
683
1230
  }
684
1231
  );
685
1232
  }
686
- return /* @__PURE__ */ jsxs3(
1233
+ return /* @__PURE__ */ jsxs5(
687
1234
  "g",
688
1235
  {
689
1236
  className: `block-layer block-layer--map ${animStyle.className}`,
690
1237
  style: animStyle.style,
691
1238
  "data-layer-id": layer.id,
692
1239
  children: [
693
- /* @__PURE__ */ jsx4("defs", { children: /* @__PURE__ */ jsx4("clipPath", { id: `clip-${layer.id}`, children: /* @__PURE__ */ jsx4("rect", { x: finalX, y: finalY, width, height }) }) }),
694
- /* @__PURE__ */ jsx4("g", { clipPath: `url(#clip-${layer.id})`, children: /* @__PURE__ */ jsx4(
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(
695
1242
  "image",
696
1243
  {
697
1244
  href: mapImageUrl,
@@ -709,18 +1256,14 @@ function MapLayer({ layer, basePath, viewport, blockTime }) {
709
1256
  }
710
1257
 
711
1258
  // src/layers/VideoLayer.tsx
712
- import { useRef, useEffect as useEffect3 } from "react";
713
- import { jsx as jsx5 } from "react/jsx-runtime";
714
- function VideoLayer({
715
- layer,
716
- basePath,
717
- viewport,
718
- blockTime: _blockTime,
719
- isPlaying
720
- }) {
1259
+ import { useRef as useRef2, useEffect as useEffect4 } from "react";
1260
+ import { jsx as jsx8 } from "react/jsx-runtime";
1261
+ function VideoLayer({ layer, basePath, viewport, blockTime, isPlaying }) {
721
1262
  const { content, position } = layer;
722
- const videoRef = useRef(null);
723
- const hasStartedRef = useRef(false);
1263
+ const videoRef = useRef2(null);
1264
+ const hasStartedRef = useRef2(false);
1265
+ const startAt = content.startAt ?? 0;
1266
+ const gated = blockTime < startAt;
724
1267
  const x = resolveValue(position.x, viewport.width);
725
1268
  const y = resolveValue(position.y, viewport.height);
726
1269
  const width = position.width ? resolveValue(position.width, viewport.width) : viewport.width;
@@ -731,7 +1274,7 @@ function VideoLayer({
731
1274
  const src = useMediaUrl(content.src, basePath);
732
1275
  const resolvedPoster = useMediaUrl(content.posterSrc || "", basePath);
733
1276
  const posterSrc = content.posterSrc ? resolvedPoster : void 0;
734
- useEffect3(() => {
1277
+ useEffect4(() => {
735
1278
  const video = videoRef.current;
736
1279
  if (!video) return;
737
1280
  video.currentTime = content.clipStart;
@@ -755,9 +1298,14 @@ function VideoLayer({
755
1298
  video.pause();
756
1299
  };
757
1300
  }, [content.src, content.clipStart, content.clipEnd]);
758
- useEffect3(() => {
1301
+ useEffect4(() => {
759
1302
  const video = videoRef.current;
760
1303
  if (!video || !hasStartedRef.current) return;
1304
+ if (gated) {
1305
+ video.pause();
1306
+ video.currentTime = content.clipStart;
1307
+ return;
1308
+ }
761
1309
  if (video.currentTime >= content.clipEnd) return;
762
1310
  if (isPlaying) {
763
1311
  const playPromise = video.play();
@@ -768,8 +1316,8 @@ function VideoLayer({
768
1316
  } else {
769
1317
  video.pause();
770
1318
  }
771
- }, [isPlaying, content.clipEnd]);
772
- return /* @__PURE__ */ jsx5("g", { className: "block-layer block-layer--video", "data-layer-id": layer.id, children: /* @__PURE__ */ jsx5("foreignObject", { x: finalX, y: finalY, width, height, children: /* @__PURE__ */ jsx5(
1319
+ }, [isPlaying, gated, content.clipStart, content.clipEnd]);
1320
+ 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(
773
1321
  "video",
774
1322
  {
775
1323
  ref: videoRef,
@@ -780,6 +1328,7 @@ function VideoLayer({
780
1328
  preload: "auto",
781
1329
  "data-clip-start": content.clipStart,
782
1330
  "data-clip-end": content.clipEnd,
1331
+ "data-start-at": startAt,
783
1332
  style: {
784
1333
  width: `${width}px`,
785
1334
  height: `${height}px`,
@@ -793,7 +1342,7 @@ function VideoLayer({
793
1342
  }
794
1343
 
795
1344
  // src/layers/TableLayer.tsx
796
- import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
1345
+ import { jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
797
1346
  function TableLayer({ layer, viewport, blockTime }) {
798
1347
  const { content, position, animation } = layer;
799
1348
  const { headers, rows, align, style } = content;
@@ -810,7 +1359,7 @@ function TableLayer({ layer, viewport, blockTime }) {
810
1359
  return a ? { textAlign: a } : void 0;
811
1360
  };
812
1361
  const borderRadius = style.borderRadius ?? 8;
813
- return /* @__PURE__ */ jsx6("foreignObject", { x: finalX, y: finalY, width, height, style: animStyle, children: /* @__PURE__ */ jsx6(
1362
+ return /* @__PURE__ */ jsx9("foreignObject", { x: finalX, y: finalY, width, height, style: animStyle, children: /* @__PURE__ */ jsx9(
814
1363
  "div",
815
1364
  {
816
1365
  ...{ xmlns: "http://www.w3.org/1999/xhtml" },
@@ -823,7 +1372,7 @@ function TableLayer({ layer, viewport, blockTime }) {
823
1372
  padding: "16px",
824
1373
  boxSizing: "border-box"
825
1374
  },
826
- children: /* @__PURE__ */ jsxs4(
1375
+ children: /* @__PURE__ */ jsxs6(
827
1376
  "table",
828
1377
  {
829
1378
  style: {
@@ -837,7 +1386,7 @@ function TableLayer({ layer, viewport, blockTime }) {
837
1386
  border: `1px solid ${style.borderColor}`
838
1387
  },
839
1388
  children: [
840
- headers.length > 0 && /* @__PURE__ */ jsx6("thead", { children: /* @__PURE__ */ jsx6("tr", { children: headers.map((header, ci) => /* @__PURE__ */ jsx6(
1389
+ headers.length > 0 && /* @__PURE__ */ jsx9("thead", { children: /* @__PURE__ */ jsx9("tr", { children: headers.map((header, ci) => /* @__PURE__ */ jsx9(
841
1390
  "th",
842
1391
  {
843
1392
  style: {
@@ -854,7 +1403,7 @@ function TableLayer({ layer, viewport, blockTime }) {
854
1403
  },
855
1404
  ci
856
1405
  )) }) }),
857
- rows.length > 0 && /* @__PURE__ */ jsx6("tbody", { children: rows.map((row, ri) => /* @__PURE__ */ jsx6("tr", { children: row.map((cell, ci) => /* @__PURE__ */ jsx6(
1406
+ rows.length > 0 && /* @__PURE__ */ jsx9("tbody", { children: rows.map((row, ri) => /* @__PURE__ */ jsx9("tr", { children: row.map((cell, ci) => /* @__PURE__ */ jsx9(
858
1407
  "td",
859
1408
  {
860
1409
  style: {
@@ -877,7 +1426,7 @@ function TableLayer({ layer, viewport, blockTime }) {
877
1426
  }
878
1427
 
879
1428
  // src/BlockRenderer.tsx
880
- import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
1429
+ import { jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
881
1430
  var VIEWPORT = {
882
1431
  width: 1920,
883
1432
  height: 1080
@@ -888,20 +1437,22 @@ function BlockRenderer({
888
1437
  basePath,
889
1438
  isEntering = false,
890
1439
  isExiting = false,
1440
+ transition,
891
1441
  viewport = VIEWPORT,
892
1442
  isPlaying
893
1443
  }) {
894
1444
  let transitionClass = "";
895
1445
  const transitionStyle = {};
896
- if (block.transition && isEntering) {
897
- transitionClass = getTransitionClass(block.transition.type, true);
898
- transitionStyle["--transition-duration"] = `${block.transition.duration}s`;
899
- } else if (block.transition && isExiting) {
900
- transitionClass = getTransitionClass(block.transition.type, false);
901
- transitionStyle["--transition-duration"] = `${block.transition.duration}s`;
1446
+ const activeTransition = transition ?? block.transition;
1447
+ if (activeTransition && isEntering) {
1448
+ transitionClass = getTransitionClass(activeTransition.type, true, activeTransition.direction);
1449
+ transitionStyle["--transition-duration"] = `${resolveTransitionDuration(activeTransition)}s`;
1450
+ } else if (activeTransition && isExiting) {
1451
+ transitionClass = getTransitionClass(activeTransition.type, false, activeTransition.direction);
1452
+ transitionStyle["--transition-duration"] = `${resolveTransitionDuration(activeTransition)}s`;
902
1453
  }
903
1454
  const clipId = `vb-clip-${block.id}`;
904
- return /* @__PURE__ */ jsxs5(
1455
+ return /* @__PURE__ */ jsxs7(
905
1456
  "svg",
906
1457
  {
907
1458
  className: `block-svg ${transitionClass}`,
@@ -911,8 +1462,8 @@ function BlockRenderer({
911
1462
  overflow: "hidden",
912
1463
  "data-block-id": block.id,
913
1464
  children: [
914
- /* @__PURE__ */ jsx7("defs", { children: /* @__PURE__ */ jsx7("clipPath", { id: clipId, children: /* @__PURE__ */ jsx7("rect", { x: "0", y: "0", width: viewport.width, height: viewport.height }) }) }),
915
- /* @__PURE__ */ jsx7("g", { clipPath: `url(#${clipId})`, children: (block.layers ?? []).map((layer) => /* @__PURE__ */ jsx7(
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(
916
1467
  LayerRenderer,
917
1468
  {
918
1469
  layer,
@@ -930,15 +1481,17 @@ function BlockRenderer({
930
1481
  function LayerRenderer({ layer, basePath, viewport, blockTime, isPlaying }) {
931
1482
  switch (layer.type) {
932
1483
  case "image":
933
- return /* @__PURE__ */ jsx7(ImageLayer, { layer, basePath, viewport, blockTime });
1484
+ return /* @__PURE__ */ jsx10(ImageLayer, { layer, basePath, viewport, blockTime });
934
1485
  case "text":
935
- return /* @__PURE__ */ jsx7(TextLayer, { layer, viewport, blockTime });
1486
+ return /* @__PURE__ */ jsx10(TextLayer, { layer, viewport, blockTime });
936
1487
  case "shape":
937
- return /* @__PURE__ */ jsx7(ShapeLayer, { layer, viewport, blockTime });
1488
+ return /* @__PURE__ */ jsx10(ShapeLayer, { layer, viewport, blockTime });
1489
+ case "path":
1490
+ return /* @__PURE__ */ jsx10(PathLayer, { layer, viewport, blockTime });
938
1491
  case "map":
939
- return /* @__PURE__ */ jsx7(MapLayer, { layer, basePath, viewport, blockTime });
1492
+ return /* @__PURE__ */ jsx10(MapLayer, { layer, basePath, viewport, blockTime });
940
1493
  case "video":
941
- return /* @__PURE__ */ jsx7(
1494
+ return /* @__PURE__ */ jsx10(
942
1495
  VideoLayer,
943
1496
  {
944
1497
  layer,
@@ -949,7 +1502,7 @@ function LayerRenderer({ layer, basePath, viewport, blockTime, isPlaying }) {
949
1502
  }
950
1503
  );
951
1504
  case "table":
952
- return /* @__PURE__ */ jsx7(TableLayer, { layer, viewport, blockTime });
1505
+ return /* @__PURE__ */ jsx10(TableLayer, { layer, viewport, blockTime });
953
1506
  default:
954
1507
  console.warn(`Unknown layer type: ${layer.type}`);
955
1508
  return null;
@@ -960,9 +1513,9 @@ function LayerRenderer({ layer, basePath, viewport, blockTime, isPlaying }) {
960
1513
  import { getCaptionAtTime } from "@bendyline/squisq/schemas";
961
1514
 
962
1515
  // src/SocialCaptionOverlay.tsx
963
- import { useMemo as useMemo2 } from "react";
1516
+ import { useMemo as useMemo4 } from "react";
964
1517
  import { resolveFontFamily } from "@bendyline/squisq/schemas";
965
- import { jsx as jsx8 } from "react/jsx-runtime";
1518
+ import { jsx as jsx11 } from "react/jsx-runtime";
966
1519
  var TARGET_CHUNK_SIZE = 4;
967
1520
  var MIN_CHUNK_SIZE = 2;
968
1521
  var MAX_CHUNK_SIZE = 6;
@@ -1016,12 +1569,12 @@ function SocialCaptionOverlay({
1016
1569
  theme,
1017
1570
  viewport
1018
1571
  }) {
1019
- const { chunks } = useMemo2(
1572
+ const { chunks } = useMemo4(
1020
1573
  () => captions ? buildWordStream(captions) : { words: [], chunks: [] },
1021
1574
  [captions]
1022
1575
  );
1023
1576
  if (!enabled || chunks.length === 0) {
1024
- return /* @__PURE__ */ jsx8(
1577
+ return /* @__PURE__ */ jsx11(
1025
1578
  "div",
1026
1579
  {
1027
1580
  className: "social-caption-overlay",
@@ -1083,7 +1636,7 @@ function SocialCaptionOverlay({
1083
1636
  const viewportHeight = viewport?.height ?? 720;
1084
1637
  const baseFontSize = Math.round(viewportHeight * 0.055);
1085
1638
  const fontSize = Math.max(24, Math.min(72, baseFontSize));
1086
- return /* @__PURE__ */ jsx8(
1639
+ return /* @__PURE__ */ jsx11(
1087
1640
  "div",
1088
1641
  {
1089
1642
  className: "social-caption-overlay",
@@ -1100,7 +1653,7 @@ function SocialCaptionOverlay({
1100
1653
  opacity: 1,
1101
1654
  transition: "opacity 0.15s ease-in-out"
1102
1655
  },
1103
- children: /* @__PURE__ */ jsx8(
1656
+ children: /* @__PURE__ */ jsx11(
1104
1657
  "div",
1105
1658
  {
1106
1659
  style: {
@@ -1109,7 +1662,7 @@ function SocialCaptionOverlay({
1109
1662
  },
1110
1663
  children: activeChunk.words.map((word, i) => {
1111
1664
  const isActive = i === activeWordIndex;
1112
- return /* @__PURE__ */ jsx8(
1665
+ return /* @__PURE__ */ jsx11(
1113
1666
  "span",
1114
1667
  {
1115
1668
  style: {
@@ -1134,7 +1687,7 @@ function SocialCaptionOverlay({
1134
1687
  }
1135
1688
 
1136
1689
  // src/CaptionOverlay.tsx
1137
- import { jsx as jsx9 } from "react/jsx-runtime";
1690
+ import { jsx as jsx12 } from "react/jsx-runtime";
1138
1691
  function CaptionOverlay({
1139
1692
  captions,
1140
1693
  currentTime,
@@ -1145,7 +1698,7 @@ function CaptionOverlay({
1145
1698
  viewport
1146
1699
  }) {
1147
1700
  if (captionStyle === "social") {
1148
- return /* @__PURE__ */ jsx9(
1701
+ return /* @__PURE__ */ jsx12(
1149
1702
  SocialCaptionOverlay,
1150
1703
  {
1151
1704
  captions,
@@ -1158,7 +1711,7 @@ function CaptionOverlay({
1158
1711
  }
1159
1712
  const phrase = enabled && captions ? getCaptionAtTime(captions, currentTime) : null;
1160
1713
  const captionText = phrase?.text ?? null;
1161
- return /* @__PURE__ */ jsx9(
1714
+ return /* @__PURE__ */ jsx12(
1162
1715
  "div",
1163
1716
  {
1164
1717
  className: "caption-overlay",
@@ -1177,7 +1730,7 @@ function CaptionOverlay({
1177
1730
  padding: "0 4px",
1178
1731
  boxSizing: "border-box"
1179
1732
  },
1180
- children: captionText && /* @__PURE__ */ jsx9(
1733
+ children: captionText && /* @__PURE__ */ jsx12(
1181
1734
  "div",
1182
1735
  {
1183
1736
  style: {
@@ -1187,7 +1740,7 @@ function CaptionOverlay({
1187
1740
  borderRadius: "4px",
1188
1741
  backdropFilter: "blur(4px)"
1189
1742
  },
1190
- children: /* @__PURE__ */ jsx9(
1743
+ children: /* @__PURE__ */ jsx12(
1191
1744
  "span",
1192
1745
  {
1193
1746
  style: {
@@ -1210,12 +1763,12 @@ function CaptionOverlay({
1210
1763
  }
1211
1764
 
1212
1765
  // src/hooks/useAutoSurface.ts
1213
- import { useCallback, useMemo as useMemo3, useSyncExternalStore } from "react";
1766
+ import { useCallback, useMemo as useMemo5, useSyncExternalStore } from "react";
1214
1767
  import { DARK_SURFACE, LIGHT_SURFACE } from "@bendyline/squisq/schemas";
1215
1768
  var DARK_QUERY = "(prefers-color-scheme: dark)";
1216
1769
  var getServerSnapshot = () => LIGHT_SURFACE;
1217
1770
  function useAutoSurface(enabled) {
1218
- const mql = useMemo3(
1771
+ const mql = useMemo5(
1219
1772
  () => enabled && typeof window !== "undefined" ? window.matchMedia(DARK_QUERY) : null,
1220
1773
  [enabled]
1221
1774
  );
@@ -1233,7 +1786,7 @@ function useAutoSurface(enabled) {
1233
1786
  }
1234
1787
 
1235
1788
  // src/hooks/useAudioSync.ts
1236
- import { useState as useState3, useEffect as useEffect4, useRef as useRef2, useCallback as useCallback2 } from "react";
1789
+ import { useState as useState3, useEffect as useEffect5, useRef as useRef3, useCallback as useCallback2 } from "react";
1237
1790
  function useAudioSync(audioRef, audioTrack, basePath = "") {
1238
1791
  const [currentTime, setCurrentTime] = useState3(0);
1239
1792
  const [isPlaying, setIsPlaying] = useState3(false);
@@ -1241,13 +1794,13 @@ function useAudioSync(audioRef, audioTrack, basePath = "") {
1241
1794
  const [isEnded, setIsEnded] = useState3(false);
1242
1795
  const [isAudioReady, setIsAudioReady] = useState3(false);
1243
1796
  const [totalDuration, setTotalDuration] = useState3(0);
1244
- const segmentStarts = useRef2([]);
1245
- const pendingSeekTime = useRef2(null);
1246
- const shouldPlayAfterLoad = useRef2(false);
1247
- const blobUrls = useRef2(/* @__PURE__ */ new Map());
1248
- const loadingPromises = useRef2(/* @__PURE__ */ new Map());
1249
- const fallbackMode = useRef2(false);
1250
- useEffect4(() => {
1797
+ const segmentStarts = useRef3([]);
1798
+ const pendingSeekTime = useRef3(null);
1799
+ const shouldPlayAfterLoad = useRef3(false);
1800
+ const blobUrls = useRef3(/* @__PURE__ */ new Map());
1801
+ const loadingPromises = useRef3(/* @__PURE__ */ new Map());
1802
+ const fallbackMode = useRef3(false);
1803
+ useEffect5(() => {
1251
1804
  if (!audioTrack?.segments) {
1252
1805
  return;
1253
1806
  }
@@ -1287,7 +1840,7 @@ function useAudioSync(audioRef, audioTrack, basePath = "") {
1287
1840
  },
1288
1841
  [basePath]
1289
1842
  );
1290
- useEffect4(() => {
1843
+ useEffect5(() => {
1291
1844
  if (!audioTrack?.segments) return;
1292
1845
  audioTrack.segments.forEach((segment) => {
1293
1846
  preloadAudio(segment.src);
@@ -1300,16 +1853,16 @@ function useAudioSync(audioRef, audioTrack, basePath = "") {
1300
1853
  currentBlobUrls.clear();
1301
1854
  };
1302
1855
  }, [audioTrack, preloadAudio]);
1303
- useEffect4(() => {
1856
+ useEffect5(() => {
1304
1857
  const audio = audioRef.current;
1305
1858
  if (!audio) return;
1306
1859
  const handleTimeUpdate = () => {
1860
+ if (fallbackMode.current) return;
1307
1861
  const segmentStart = segmentStarts.current[currentSegment] || 0;
1308
1862
  const overallTime = segmentStart + audio.currentTime;
1309
1863
  setCurrentTime(overallTime);
1310
1864
  };
1311
1865
  const handlePlay = () => {
1312
- fallbackMode.current = false;
1313
1866
  setIsPlaying(true);
1314
1867
  };
1315
1868
  const handlePause = () => setIsPlaying(false);
@@ -1338,7 +1891,7 @@ function useAudioSync(audioRef, audioTrack, basePath = "") {
1338
1891
  audio.removeEventListener("error", handleError);
1339
1892
  };
1340
1893
  }, [audioRef, currentSegment, audioTrack]);
1341
- useEffect4(() => {
1894
+ useEffect5(() => {
1342
1895
  const audio = audioRef.current;
1343
1896
  if (!audio || !audioTrack?.segments) return;
1344
1897
  const segment = audioTrack.segments[currentSegment];
@@ -1461,7 +2014,7 @@ function useAudioSync(audioRef, audioTrack, basePath = "") {
1461
2014
  await new Promise((resolve) => setTimeout(resolve, 50));
1462
2015
  play();
1463
2016
  }, [seekTo, play]);
1464
- useEffect4(() => {
2017
+ useEffect5(() => {
1465
2018
  if (!isPlaying || !fallbackMode.current || !totalDuration) return;
1466
2019
  let lastTime = performance.now();
1467
2020
  let raf;
@@ -1505,27 +2058,34 @@ function useAudioSync(audioRef, audioTrack, basePath = "") {
1505
2058
  }
1506
2059
 
1507
2060
  // src/hooks/useDocPlayback.ts
1508
- import { useState as useState4, useEffect as useEffect5, useMemo as useMemo4, useCallback as useCallback3, useRef as useRef3 } from "react";
1509
- import { getBlockAtTime } from "@bendyline/squisq/schemas";
2061
+ import { useMemo as useMemo6, useCallback as useCallback3, useRef as useRef4 } from "react";
2062
+ import {
2063
+ DEFAULT_THEME,
2064
+ getBlockAtTime,
2065
+ resolveBlockTransition,
2066
+ resolveTransitionDuration as resolveTransitionDuration2
2067
+ } from "@bendyline/squisq/schemas";
1510
2068
  import {
1511
2069
  expandDocBlocks,
1512
- flattenBlocks,
2070
+ flattenRenderableBlocks,
1513
2071
  isTemplateBlock,
2072
+ resolvePersistentLayers,
1514
2073
  VIEWPORT_PRESETS
1515
2074
  } from "@bendyline/squisq/doc";
1516
2075
  function useDocPlayback(script, currentTime, viewport = VIEWPORT_PRESETS.landscape, renderMode = false, theme) {
1517
- const [transitionState, setTransitionState] = useState4({
1518
- entering: false,
1519
- exiting: false,
1520
- previousBlock: null
1521
- });
1522
- const blocks = useMemo4(() => {
2076
+ void renderMode;
2077
+ const blocks = useMemo6(() => {
1523
2078
  if (!script?.blocks) {
1524
2079
  return [];
1525
2080
  }
1526
2081
  const hasChildren = script.blocks.some((b) => b.children && b.children.length > 0);
1527
- const flatBlocks = hasChildren ? flattenBlocks(script.blocks) : script.blocks;
2082
+ const flatBlocks = hasChildren ? flattenRenderableBlocks(script.blocks) : script.blocks;
1528
2083
  const hasTemplates = flatBlocks.some(isTemplateBlock);
2084
+ const resolvedTheme = theme ?? DEFAULT_THEME;
2085
+ const persistentLayers = resolvePersistentLayers(
2086
+ { persistentLayers: script.persistentLayers },
2087
+ resolvedTheme
2088
+ );
1529
2089
  if (hasTemplates) {
1530
2090
  const audioSegments = script.audio?.segments?.map((seg) => ({
1531
2091
  startTime: seg.startTime,
@@ -1534,78 +2094,58 @@ function useDocPlayback(script, currentTime, viewport = VIEWPORT_PRESETS.landsca
1534
2094
  const expanded = expandDocBlocks(flatBlocks, {
1535
2095
  audioSegments,
1536
2096
  viewport,
1537
- persistentLayers: script.persistentLayers,
1538
- theme
2097
+ persistentLayers,
2098
+ theme,
2099
+ // Custom (user-defined) templates inlined into the doc's
2100
+ // frontmatter — see CustomTemplates.ts. Merged onto the
2101
+ // built-in registry so blocks annotated with `{[myhero]}`
2102
+ // resolve through the user's design.
2103
+ customTemplates: script.customTemplates
1539
2104
  });
1540
2105
  return expanded;
1541
2106
  }
1542
- return flatBlocks;
1543
- }, [script?.blocks, script?.audio?.segments, script?.persistentLayers, viewport, theme]);
1544
- const currentBlock = useMemo4(() => getBlockAtTime(blocks, currentTime), [blocks, currentTime]);
1545
- const currentBlockIndex = useMemo4(
2107
+ return flatBlocks.map((block, index) => {
2108
+ const transition = resolveBlockTransition(block, resolvedTheme, index);
2109
+ return transition !== block.transition ? { ...block, transition } : block;
2110
+ });
2111
+ }, [
2112
+ script?.blocks,
2113
+ script?.audio?.segments,
2114
+ script?.persistentLayers,
2115
+ script?.customTemplates,
2116
+ viewport,
2117
+ theme
2118
+ ]);
2119
+ const currentBlock = useMemo6(() => getBlockAtTime(blocks, currentTime), [blocks, currentTime]);
2120
+ const currentBlockIndex = useMemo6(
1546
2121
  () => currentBlock ? blocks.indexOf(currentBlock) : -1,
1547
2122
  [blocks, currentBlock]
1548
2123
  );
1549
- const blockTime = useMemo4(() => {
2124
+ const blockTime = useMemo6(() => {
1550
2125
  if (!currentBlock) return 0;
1551
2126
  return Math.max(0, currentTime - currentBlock.startTime);
1552
2127
  }, [currentBlock, currentTime]);
1553
- const blockProgress = useMemo4(() => {
2128
+ const blockProgress = useMemo6(() => {
1554
2129
  if (!currentBlock || currentBlock.duration === 0) return 0;
1555
2130
  return Math.min(1, blockTime / currentBlock.duration);
1556
2131
  }, [currentBlock, blockTime]);
1557
- const docProgress = useMemo4(() => {
2132
+ const docProgress = useMemo6(() => {
1558
2133
  if (!script || script.duration === 0) return 0;
1559
2134
  return Math.min(1, currentTime / script.duration);
1560
2135
  }, [script, currentTime]);
1561
- const _prevBlockRef = useMemo4(
1562
- () => currentBlock,
1563
- // eslint-disable-next-line react-hooks/exhaustive-deps -- intentionally keyed on index, not block reference
1564
- [currentBlockIndex]
1565
- );
1566
- useEffect5(() => {
1567
- if (!currentBlock || renderMode) return;
1568
- if (transitionState.previousBlock?.id !== currentBlock.id) {
1569
- const transition = currentBlock.transition;
1570
- const transitionDuration = transition?.duration || 0;
1571
- if (transitionDuration > 0) {
1572
- setTransitionState({
1573
- entering: true,
1574
- exiting: true,
1575
- previousBlock: transitionState.previousBlock
1576
- });
1577
- const timer = setTimeout(() => {
1578
- setTransitionState({
1579
- entering: false,
1580
- exiting: false,
1581
- previousBlock: currentBlock
1582
- });
1583
- }, transitionDuration * 1e3);
1584
- return () => clearTimeout(timer);
1585
- } else {
1586
- setTransitionState({
1587
- entering: false,
1588
- exiting: false,
1589
- previousBlock: currentBlock
1590
- });
1591
- }
1592
- }
1593
- }, [currentBlock?.id, renderMode]);
1594
- const renderPrevBlockRef = useRef3(null);
1595
- useEffect5(() => {
1596
- if (!renderMode || !currentBlock) return;
1597
- if (transitionState.previousBlock?.id !== currentBlock.id) {
1598
- const oldPrev = transitionState.previousBlock;
1599
- renderPrevBlockRef.current = oldPrev;
1600
- setTransitionState((prev) => ({
1601
- ...prev,
1602
- previousBlock: currentBlock
1603
- }));
1604
- }
1605
- }, [currentBlock?.id, renderMode]);
1606
- const renderTransitionDuration = currentBlock?.transition?.duration || 0;
1607
- const renderIsEntering = renderMode && renderTransitionDuration > 0 && blockTime < renderTransitionDuration;
1608
- const renderIsExiting = renderIsEntering && renderPrevBlockRef.current !== null;
2136
+ const outgoingBlockRef = useRef4(null);
2137
+ const activeBlockIdRef = useRef4(null);
2138
+ const lastRenderedBlockRef = useRef4(null);
2139
+ if (currentBlock && currentBlock.id !== activeBlockIdRef.current) {
2140
+ outgoingBlockRef.current = lastRenderedBlockRef.current;
2141
+ activeBlockIdRef.current = currentBlock.id;
2142
+ }
2143
+ lastRenderedBlockRef.current = currentBlock;
2144
+ const transitionDuration = currentBlock?.transition ? resolveTransitionDuration2(currentBlock.transition) : 0;
2145
+ const isEntering = !!currentBlock && transitionDuration > 0 && blockTime < transitionDuration;
2146
+ const outgoingBlock = outgoingBlockRef.current;
2147
+ const isExiting = isEntering && outgoingBlock != null && outgoingBlock.id !== currentBlock?.id;
2148
+ const previousBlock = isExiting ? outgoingBlock : null;
1609
2149
  const goToBlock = useCallback3(
1610
2150
  (index) => {
1611
2151
  if (!script || index < 0 || index >= blocks.length) return;
@@ -1629,9 +2169,9 @@ function useDocPlayback(script, currentTime, viewport = VIEWPORT_PRESETS.landsca
1629
2169
  return {
1630
2170
  currentBlock,
1631
2171
  currentBlockIndex,
1632
- previousBlock: renderMode ? renderIsExiting ? renderPrevBlockRef.current : null : transitionState.exiting ? transitionState.previousBlock : null,
1633
- isEntering: renderMode ? renderIsEntering : transitionState.entering,
1634
- isExiting: renderMode ? renderIsExiting : transitionState.exiting,
2172
+ previousBlock,
2173
+ isEntering,
2174
+ isExiting,
1635
2175
  blockTime,
1636
2176
  blockProgress,
1637
2177
  docProgress,
@@ -1644,7 +2184,7 @@ function useDocPlayback(script, currentTime, viewport = VIEWPORT_PRESETS.landsca
1644
2184
  }
1645
2185
 
1646
2186
  // src/hooks/useViewportOrientation.ts
1647
- import { useState as useState5, useEffect as useEffect6, useMemo as useMemo5 } from "react";
2187
+ import { useState as useState4, useEffect as useEffect6, useMemo as useMemo7 } from "react";
1648
2188
  import {
1649
2189
  VIEWPORT_PRESETS as VIEWPORT_PRESETS2
1650
2190
  } from "@bendyline/squisq/doc";
@@ -1670,7 +2210,7 @@ function getViewportForOrientation(orientation) {
1670
2210
  }
1671
2211
  }
1672
2212
  function useViewportOrientation() {
1673
- const [windowSize, setWindowSize] = useState5(() => ({
2213
+ const [windowSize, setWindowSize] = useState4(() => ({
1674
2214
  width: typeof window !== "undefined" ? window.innerWidth : 1920,
1675
2215
  height: typeof window !== "undefined" ? window.innerHeight : 1080
1676
2216
  }));
@@ -1693,11 +2233,11 @@ function useViewportOrientation() {
1693
2233
  clearTimeout(timeoutId);
1694
2234
  };
1695
2235
  }, []);
1696
- const orientation = useMemo5(
2236
+ const orientation = useMemo7(
1697
2237
  () => getOrientationFromWindow(windowSize.width, windowSize.height),
1698
2238
  [windowSize.width, windowSize.height]
1699
2239
  );
1700
- const viewport = useMemo5(() => getViewportForOrientation(orientation), [orientation]);
2240
+ const viewport = useMemo7(() => getViewportForOrientation(orientation), [orientation]);
1701
2241
  return {
1702
2242
  viewport,
1703
2243
  orientation,
@@ -1705,16 +2245,164 @@ function useViewportOrientation() {
1705
2245
  };
1706
2246
  }
1707
2247
 
2248
+ // src/hooks/useSlideSwipe.ts
2249
+ import { useCallback as useCallback4, useEffect as useEffect7, useRef as useRef5, useState as useState5 } from "react";
2250
+ var DISTANCE_RATIO = 0.3;
2251
+ var FLICK_VELOCITY = 0.5;
2252
+ var MIN_FLICK_DISTANCE = 12;
2253
+ var RUBBER_BAND = 0.35;
2254
+ var DEFAULT_SETTLE_MS = 260;
2255
+ function decideSwipe({
2256
+ dx,
2257
+ width,
2258
+ elapsedMs,
2259
+ canNext,
2260
+ canPrev
2261
+ }) {
2262
+ const distanceThreshold = width > 0 ? width * DISTANCE_RATIO : Infinity;
2263
+ const velocity = elapsedMs > 0 ? Math.abs(dx) / elapsedMs : 0;
2264
+ const passesDistance = Math.abs(dx) >= distanceThreshold;
2265
+ const passesFlick = velocity >= FLICK_VELOCITY && Math.abs(dx) >= MIN_FLICK_DISTANCE;
2266
+ if (!passesDistance && !passesFlick) return "snap";
2267
+ if (dx < 0) return canNext ? "next" : "snap";
2268
+ if (dx > 0) return canPrev ? "prev" : "snap";
2269
+ return "snap";
2270
+ }
2271
+ function useSlideSwipe(opts) {
2272
+ const settleMs = opts.settleMs ?? DEFAULT_SETTLE_MS;
2273
+ const [offsetPx, setOffsetPx] = useState5(0);
2274
+ const [phase, setPhase] = useState5("idle");
2275
+ const optsRef = useRef5(opts);
2276
+ optsRef.current = opts;
2277
+ const dragRef = useRef5(null);
2278
+ const phaseRef = useRef5("idle");
2279
+ phaseRef.current = phase;
2280
+ const settleTimer = useRef5(null);
2281
+ const settleRaf = useRef5(null);
2282
+ const clearPending = useCallback4(() => {
2283
+ if (settleTimer.current != null) {
2284
+ clearTimeout(settleTimer.current);
2285
+ settleTimer.current = null;
2286
+ }
2287
+ if (settleRaf.current != null) {
2288
+ cancelAnimationFrame(settleRaf.current);
2289
+ settleRaf.current = null;
2290
+ }
2291
+ }, []);
2292
+ const onPointerDown = useCallback4((e) => {
2293
+ const o = optsRef.current;
2294
+ if (!o.enabled) return;
2295
+ if (phaseRef.current === "settling") return;
2296
+ if (e.pointerType === "mouse" && e.button !== 0) return;
2297
+ const target = e.target;
2298
+ if (target.closest?.("button, a, input, textarea, select, [data-no-swipe]")) return;
2299
+ dragRef.current = {
2300
+ pointerId: e.pointerId,
2301
+ startX: e.clientX,
2302
+ startTime: performance.now(),
2303
+ target
2304
+ };
2305
+ try {
2306
+ target.setPointerCapture?.(e.pointerId);
2307
+ } catch {
2308
+ }
2309
+ setPhase("dragging");
2310
+ setOffsetPx(0);
2311
+ }, []);
2312
+ useEffect7(() => {
2313
+ function currentWidth() {
2314
+ return optsRef.current.containerRef.current?.getBoundingClientRect().width ?? 0;
2315
+ }
2316
+ function endDrag(drag) {
2317
+ dragRef.current = null;
2318
+ try {
2319
+ drag.target.releasePointerCapture?.(drag.pointerId);
2320
+ } catch {
2321
+ }
2322
+ }
2323
+ function settleTo(target, onArrive) {
2324
+ setPhase("settling");
2325
+ settleRaf.current = requestAnimationFrame(() => {
2326
+ settleRaf.current = null;
2327
+ setOffsetPx(target);
2328
+ settleTimer.current = setTimeout(() => {
2329
+ settleTimer.current = null;
2330
+ onArrive?.();
2331
+ setOffsetPx(0);
2332
+ setPhase("idle");
2333
+ }, settleMs);
2334
+ });
2335
+ }
2336
+ function onMove(e) {
2337
+ const drag = dragRef.current;
2338
+ if (!drag || e.pointerId !== drag.pointerId) return;
2339
+ const o = optsRef.current;
2340
+ const raw = e.clientX - drag.startX;
2341
+ const blocked = raw > 0 && !o.canGoPrev || raw < 0 && !o.canGoNext;
2342
+ setOffsetPx(blocked ? raw * RUBBER_BAND : raw);
2343
+ }
2344
+ function onUp(e) {
2345
+ const drag = dragRef.current;
2346
+ if (!drag || e.pointerId !== drag.pointerId) return;
2347
+ endDrag(drag);
2348
+ const o = optsRef.current;
2349
+ const rawDx = e.clientX - drag.startX;
2350
+ const width = currentWidth();
2351
+ const elapsedMs = performance.now() - drag.startTime;
2352
+ const decision = decideSwipe({
2353
+ dx: rawDx,
2354
+ width,
2355
+ elapsedMs,
2356
+ canNext: o.canGoNext,
2357
+ canPrev: o.canGoPrev
2358
+ });
2359
+ if (decision === "snap") {
2360
+ settleTo(0);
2361
+ return;
2362
+ }
2363
+ const distance = Math.max(width, Math.abs(rawDx));
2364
+ const target = decision === "next" ? -distance : distance;
2365
+ settleTo(target, decision === "next" ? o.onNext : o.onPrev);
2366
+ }
2367
+ function onCancel(e) {
2368
+ const drag = dragRef.current;
2369
+ if (!drag || e.pointerId !== drag.pointerId) return;
2370
+ endDrag(drag);
2371
+ settleTo(0);
2372
+ }
2373
+ window.addEventListener("pointermove", onMove);
2374
+ window.addEventListener("pointerup", onUp);
2375
+ window.addEventListener("pointercancel", onCancel);
2376
+ return () => {
2377
+ window.removeEventListener("pointermove", onMove);
2378
+ window.removeEventListener("pointerup", onUp);
2379
+ window.removeEventListener("pointercancel", onCancel);
2380
+ };
2381
+ }, [settleMs]);
2382
+ useEffect7(() => {
2383
+ if (!opts.enabled) {
2384
+ dragRef.current = null;
2385
+ clearPending();
2386
+ setPhase("idle");
2387
+ setOffsetPx(0);
2388
+ }
2389
+ }, [opts.enabled, clearPending]);
2390
+ useEffect7(() => clearPending, [clearPending]);
2391
+ return { offsetPx, phase, onPointerDown };
2392
+ }
2393
+
1708
2394
  // src/DocPlayer.tsx
1709
2395
  import {
1710
2396
  expandCoverBlock,
1711
2397
  createTemplateContext,
1712
- DEFAULT_THEME as DEFAULT_THEME2,
2398
+ markdownToDoc as markdownToDoc2,
2399
+ DEFAULT_THEME as DEFAULT_THEME3,
1713
2400
  VIEWPORT_PRESETS as VIEWPORT_PRESETS4
1714
2401
  } from "@bendyline/squisq/doc";
2402
+ import { parseMarkdown as parseMarkdown2 } from "@bendyline/squisq/markdown";
1715
2403
 
1716
2404
  // src/DocProgressBar.tsx
1717
- import { useRef as useRef4, useState as useState6, useCallback as useCallback4 } from "react";
2405
+ import { useRef as useRef6, useState as useState6, useCallback as useCallback5 } from "react";
1718
2406
 
1719
2407
  // src/types.ts
1720
2408
  function formatTime(seconds) {
@@ -1724,7 +2412,7 @@ function formatTime(seconds) {
1724
2412
  }
1725
2413
 
1726
2414
  // src/DocProgressBar.tsx
1727
- import { jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
2415
+ import { jsx as jsx13, jsxs as jsxs8 } from "react/jsx-runtime";
1728
2416
  function DocProgressBar({
1729
2417
  state,
1730
2418
  actions,
@@ -1732,9 +2420,10 @@ function DocProgressBar({
1732
2420
  expandedBlocks,
1733
2421
  getBlockTitle
1734
2422
  }) {
1735
- const progressBarRef = useRef4(null);
2423
+ const progressBarRef = useRef6(null);
1736
2424
  const [hoverPosition, setHoverPosition] = useState6(null);
1737
- const handleProgressHover = useCallback4((e) => {
2425
+ const playProgress = state.totalDuration > 0 ? Math.max(0, Math.min(1, state.currentTime / state.totalDuration)) : 0;
2426
+ const handleProgressHover = useCallback5((e) => {
1738
2427
  const bar = progressBarRef.current;
1739
2428
  if (!bar) return;
1740
2429
  const rect = bar.getBoundingClientRect();
@@ -1742,10 +2431,10 @@ function DocProgressBar({
1742
2431
  const progress = Math.max(0, Math.min(1, x / rect.width));
1743
2432
  setHoverPosition(progress);
1744
2433
  }, []);
1745
- const handleProgressLeave = useCallback4(() => {
2434
+ const handleProgressLeave = useCallback5(() => {
1746
2435
  setHoverPosition(null);
1747
2436
  }, []);
1748
- const getBlockAtTimeLocal = useCallback4(
2437
+ const getBlockAtTimeLocal = useCallback5(
1749
2438
  (time) => {
1750
2439
  for (let i = expandedBlocks.length - 1; i >= 0; i--) {
1751
2440
  const blk = expandedBlocks[i];
@@ -1757,7 +2446,7 @@ function DocProgressBar({
1757
2446
  },
1758
2447
  [expandedBlocks]
1759
2448
  );
1760
- return /* @__PURE__ */ jsxs6(
2449
+ return /* @__PURE__ */ jsxs8(
1761
2450
  "div",
1762
2451
  {
1763
2452
  ref: progressBarRef,
@@ -1778,7 +2467,7 @@ function DocProgressBar({
1778
2467
  onMouseMove: handleProgressHover,
1779
2468
  onMouseLeave: handleProgressLeave,
1780
2469
  children: [
1781
- /* @__PURE__ */ jsx10(
2470
+ /* @__PURE__ */ jsx13(
1782
2471
  "div",
1783
2472
  {
1784
2473
  style: {
@@ -1791,21 +2480,21 @@ function DocProgressBar({
1791
2480
  }
1792
2481
  }
1793
2482
  ),
1794
- /* @__PURE__ */ jsx10(
2483
+ /* @__PURE__ */ jsx13(
1795
2484
  "div",
1796
2485
  {
2486
+ "data-testid": "doc-progress-fill",
1797
2487
  style: {
1798
2488
  position: "absolute",
1799
2489
  left: 0,
1800
- width: `${state.docProgress * 100}%`,
2490
+ width: `${playProgress * 100}%`,
1801
2491
  height: "6px",
1802
2492
  background: "#5b9bd5",
1803
- borderRadius: "3px",
1804
- transition: "width 0.1s"
2493
+ borderRadius: "3px"
1805
2494
  }
1806
2495
  }
1807
2496
  ),
1808
- blockMarkers.map((marker, i) => /* @__PURE__ */ jsx10(
2497
+ blockMarkers.map((marker, i) => /* @__PURE__ */ jsx13(
1809
2498
  "div",
1810
2499
  {
1811
2500
  style: {
@@ -1835,7 +2524,7 @@ function DocProgressBar({
1835
2524
  },
1836
2525
  `${marker.block.id}-${i}`
1837
2526
  )),
1838
- hoverPosition !== null && /* @__PURE__ */ jsxs6(
2527
+ hoverPosition !== null && /* @__PURE__ */ jsxs8(
1839
2528
  "div",
1840
2529
  {
1841
2530
  style: {
@@ -1852,19 +2541,19 @@ function DocProgressBar({
1852
2541
  zIndex: 10
1853
2542
  },
1854
2543
  children: [
1855
- /* @__PURE__ */ jsx10("div", { style: { color: "white", fontSize: "12px", fontFamily: "monospace" }, children: formatTime(hoverPosition * state.totalDuration) }),
2544
+ /* @__PURE__ */ jsx13("div", { style: { color: "white", fontSize: "12px", fontFamily: "monospace" }, children: formatTime(hoverPosition * state.totalDuration) }),
1856
2545
  (() => {
1857
2546
  const hoverTime = hoverPosition * state.totalDuration;
1858
2547
  const slideInfo = getBlockAtTimeLocal(hoverTime);
1859
2548
  if (slideInfo && getBlockTitle) {
1860
- return /* @__PURE__ */ jsx10("div", { style: { color: "rgba(255,255,255,0.7)", fontSize: "11px", marginTop: "2px" }, children: getBlockTitle(slideInfo.block) });
2549
+ return /* @__PURE__ */ jsx13("div", { style: { color: "rgba(255,255,255,0.7)", fontSize: "11px", marginTop: "2px" }, children: getBlockTitle(slideInfo.block) });
1861
2550
  }
1862
2551
  return null;
1863
2552
  })()
1864
2553
  ]
1865
2554
  }
1866
2555
  ),
1867
- hoverPosition !== null && /* @__PURE__ */ jsx10(
2556
+ hoverPosition !== null && /* @__PURE__ */ jsx13(
1868
2557
  "div",
1869
2558
  {
1870
2559
  style: {
@@ -1886,7 +2575,7 @@ function DocProgressBar({
1886
2575
  }
1887
2576
 
1888
2577
  // src/DocControlsOverlay.tsx
1889
- import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
2578
+ import { jsx as jsx14, jsxs as jsxs9 } from "react/jsx-runtime";
1890
2579
  function DocControlsOverlay({
1891
2580
  state,
1892
2581
  actions,
@@ -1894,7 +2583,7 @@ function DocControlsOverlay({
1894
2583
  expandedBlocks,
1895
2584
  getBlockTitle
1896
2585
  }) {
1897
- return /* @__PURE__ */ jsxs7(
2586
+ return /* @__PURE__ */ jsxs9(
1898
2587
  "div",
1899
2588
  {
1900
2589
  className: "doc-player__controls",
@@ -1911,7 +2600,7 @@ function DocControlsOverlay({
1911
2600
  zIndex: 100
1912
2601
  },
1913
2602
  children: [
1914
- /* @__PURE__ */ jsx11(
2603
+ /* @__PURE__ */ jsx14(
1915
2604
  "button",
1916
2605
  {
1917
2606
  onClick: actions.restart,
@@ -1927,10 +2616,10 @@ function DocControlsOverlay({
1927
2616
  },
1928
2617
  title: "Restart",
1929
2618
  "aria-label": "Restart from beginning",
1930
- children: /* @__PURE__ */ jsx11("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx11("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" }) })
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" }) })
1931
2620
  }
1932
2621
  ),
1933
- /* @__PURE__ */ jsx11(
2622
+ /* @__PURE__ */ jsx14(
1934
2623
  "button",
1935
2624
  {
1936
2625
  onClick: actions.toggle,
@@ -1949,15 +2638,15 @@ function DocControlsOverlay({
1949
2638
  height: "40px"
1950
2639
  },
1951
2640
  "aria-label": state.isPlaying ? "Pause" : "Play",
1952
- children: state.isPlaying ? /* @__PURE__ */ jsx11("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx11("path", { d: "M6 19h4V5H6v14zm8-14v14h4V5h-4z" }) }) : /* @__PURE__ */ jsx11("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx11("path", { d: "M8 5v14l11-7z" }) })
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" }) })
1953
2642
  }
1954
2643
  ),
1955
- /* @__PURE__ */ jsxs7("span", { style: { color: "white", fontSize: "12px", minWidth: "80px", fontFamily: "monospace" }, children: [
2644
+ /* @__PURE__ */ jsxs9("span", { style: { color: "white", fontSize: "12px", minWidth: "80px", fontFamily: "monospace" }, children: [
1956
2645
  formatTime(state.currentTime),
1957
2646
  " / ",
1958
2647
  formatTime(state.totalDuration)
1959
2648
  ] }),
1960
- /* @__PURE__ */ jsx11(
2649
+ /* @__PURE__ */ jsx14(
1961
2650
  DocProgressBar,
1962
2651
  {
1963
2652
  state,
@@ -1967,12 +2656,12 @@ function DocControlsOverlay({
1967
2656
  getBlockTitle
1968
2657
  }
1969
2658
  ),
1970
- /* @__PURE__ */ jsxs7("span", { style: { color: "rgba(255,255,255,0.5)", fontSize: "11px", whiteSpace: "nowrap" }, children: [
2659
+ /* @__PURE__ */ jsxs9("span", { style: { color: "rgba(255,255,255,0.5)", fontSize: "11px", whiteSpace: "nowrap" }, children: [
1971
2660
  state.currentBlockIndex + 1,
1972
2661
  "/",
1973
2662
  state.totalBlocks
1974
2663
  ] }),
1975
- state.hasCaptions && /* @__PURE__ */ jsxs7(
2664
+ state.hasCaptions && /* @__PURE__ */ jsxs9(
1976
2665
  "button",
1977
2666
  {
1978
2667
  onClick: () => actions.cycleCaptionMode(),
@@ -1991,12 +2680,12 @@ function DocControlsOverlay({
1991
2680
  title: state.captionMode === "off" ? "Captions: Off (click for Standard)" : state.captionMode === "standard" ? "Captions: Standard (click for Social)" : "Captions: Social (click to turn off)",
1992
2681
  "aria-label": "Cycle caption style",
1993
2682
  children: [
1994
- /* @__PURE__ */ jsx11("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx11("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" }) }),
1995
- state.captionMode !== "off" && /* @__PURE__ */ jsx11("span", { style: { fontSize: "10px", textTransform: "uppercase", letterSpacing: "0.5px" }, children: state.captionMode === "standard" ? "CC" : "SM" })
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" })
1996
2685
  ]
1997
2686
  }
1998
2687
  ),
1999
- actions.toggleFullscreen && /* @__PURE__ */ jsx11(
2688
+ actions.toggleFullscreen && /* @__PURE__ */ jsx14(
2000
2689
  "button",
2001
2690
  {
2002
2691
  onClick: actions.toggleFullscreen,
@@ -2012,7 +2701,7 @@ function DocControlsOverlay({
2012
2701
  },
2013
2702
  title: state.isFullscreen ? "Exit fullscreen (F)" : "Fullscreen (F)",
2014
2703
  "aria-label": state.isFullscreen ? "Exit fullscreen" : "Enter fullscreen",
2015
- children: state.isFullscreen ? /* @__PURE__ */ jsx11("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx11("path", { d: "M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z" }) }) : /* @__PURE__ */ jsx11("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx11("path", { d: "M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" }) })
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" }) })
2016
2705
  }
2017
2706
  )
2018
2707
  ]
@@ -2021,12 +2710,12 @@ function DocControlsOverlay({
2021
2710
  }
2022
2711
 
2023
2712
  // src/DocControlsSlideshow.tsx
2024
- import { jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
2713
+ import { jsx as jsx15, jsxs as jsxs10 } from "react/jsx-runtime";
2025
2714
  function DocControlsSlideshow({ state, slideNav }) {
2026
2715
  const { currentBlockIndex, totalBlocks } = state;
2027
2716
  const isFirst = currentBlockIndex <= 0;
2028
2717
  const isLast = currentBlockIndex >= totalBlocks - 1;
2029
- return /* @__PURE__ */ jsxs8(
2718
+ return /* @__PURE__ */ jsxs10(
2030
2719
  "div",
2031
2720
  {
2032
2721
  className: "doc-controls-slideshow",
@@ -2047,7 +2736,7 @@ function DocControlsSlideshow({ state, slideNav }) {
2047
2736
  WebkitBackdropFilter: "blur(8px)"
2048
2737
  },
2049
2738
  children: [
2050
- /* @__PURE__ */ jsx12(
2739
+ /* @__PURE__ */ jsx15(
2051
2740
  "button",
2052
2741
  {
2053
2742
  onClick: (e) => {
@@ -2076,10 +2765,10 @@ function DocControlsSlideshow({ state, slideNav }) {
2076
2765
  onMouseLeave: (e) => {
2077
2766
  e.currentTarget.style.background = "none";
2078
2767
  },
2079
- children: /* @__PURE__ */ jsx12("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx12("path", { d: "M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z" }) })
2768
+ children: /* @__PURE__ */ jsx15("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx15("path", { d: "M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z" }) })
2080
2769
  }
2081
2770
  ),
2082
- /* @__PURE__ */ jsx12(
2771
+ /* @__PURE__ */ jsx15(
2083
2772
  "span",
2084
2773
  {
2085
2774
  "data-testid": "slide-counter",
@@ -2096,7 +2785,7 @@ function DocControlsSlideshow({ state, slideNav }) {
2096
2785
  children: totalBlocks > 0 ? `${currentBlockIndex + 1} / ${totalBlocks}` : "\u2014"
2097
2786
  }
2098
2787
  ),
2099
- /* @__PURE__ */ jsx12(
2788
+ /* @__PURE__ */ jsx15(
2100
2789
  "button",
2101
2790
  {
2102
2791
  onClick: (e) => {
@@ -2125,7 +2814,7 @@ function DocControlsSlideshow({ state, slideNav }) {
2125
2814
  onMouseLeave: (e) => {
2126
2815
  e.currentTarget.style.background = "none";
2127
2816
  },
2128
- children: /* @__PURE__ */ jsx12("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx12("path", { d: "M8.59 16.59L10 18l6-6-6-6-1.41 1.41L13.17 12z" }) })
2817
+ children: /* @__PURE__ */ jsx15("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx15("path", { d: "M8.59 16.59L10 18l6-6-6-6-1.41 1.41L13.17 12z" }) })
2129
2818
  }
2130
2819
  )
2131
2820
  ]
@@ -2134,20 +2823,30 @@ function DocControlsSlideshow({ state, slideNav }) {
2134
2823
  }
2135
2824
 
2136
2825
  // src/LinearDocView.tsx
2137
- import { useMemo as useMemo6 } from "react";
2826
+ import { useMemo as useMemo8 } from "react";
2138
2827
  import {
2139
2828
  applySurface,
2140
2829
  resolveFontFamily as resolveFontFamily2
2141
2830
  } from "@bendyline/squisq/schemas";
2142
2831
  import { VIEWPORT_PRESETS as VIEWPORT_PRESETS3 } from "@bendyline/squisq/schemas";
2143
- import { getLayers, hasTemplate, DEFAULT_THEME } from "@bendyline/squisq/doc";
2144
- import { extractPlainText } from "@bendyline/squisq/markdown";
2832
+ import {
2833
+ getLayers,
2834
+ hasTemplate,
2835
+ markdownToDoc,
2836
+ DEFAULT_THEME as DEFAULT_THEME2,
2837
+ deriveTemplateInputs
2838
+ } from "@bendyline/squisq/doc";
2839
+ import { extractPlainText, parseMarkdown } from "@bendyline/squisq/markdown";
2145
2840
 
2146
2841
  // src/MarkdownRenderer.tsx
2147
- import { Fragment } from "react";
2842
+ import { Fragment as Fragment2 } from "react";
2843
+ import {
2844
+ sanitizeHtmlNodes as sanitizeHtmlNodes2,
2845
+ sanitizeUrl
2846
+ } from "@bendyline/squisq/markdown";
2148
2847
 
2149
2848
  // src/InlineVideoPlayer.tsx
2150
- import { jsx as jsx13 } from "react/jsx-runtime";
2849
+ import { jsx as jsx16 } from "react/jsx-runtime";
2151
2850
  function InlineVideoPlayer({
2152
2851
  src,
2153
2852
  basePath = "",
@@ -2162,7 +2861,7 @@ function InlineVideoPlayer({
2162
2861
  const resolvedPoster = useMediaUrl(poster ?? "", basePath);
2163
2862
  const posterUrl = poster ? resolvedPoster : void 0;
2164
2863
  if (!resolvedSrc) return null;
2165
- return /* @__PURE__ */ jsx13("span", { className: `squisq-inline-video-player ${className ?? ""}`.trim(), children: /* @__PURE__ */ jsx13(
2864
+ return /* @__PURE__ */ jsx16("span", { className: `squisq-inline-video-player ${className ?? ""}`.trim(), children: /* @__PURE__ */ jsx16(
2166
2865
  "video",
2167
2866
  {
2168
2867
  src: resolvedSrc,
@@ -2177,7 +2876,7 @@ function InlineVideoPlayer({
2177
2876
  }
2178
2877
 
2179
2878
  // src/InlineAudioPlayer.tsx
2180
- import { jsx as jsx14 } from "react/jsx-runtime";
2879
+ import { jsx as jsx17 } from "react/jsx-runtime";
2181
2880
  function InlineAudioPlayer({
2182
2881
  src,
2183
2882
  basePath = "",
@@ -2187,55 +2886,62 @@ function InlineAudioPlayer({
2187
2886
  }) {
2188
2887
  const resolvedSrc = useMediaUrl(src, basePath);
2189
2888
  if (!resolvedSrc) return null;
2190
- return /* @__PURE__ */ jsx14("span", { className: `squisq-inline-audio-player ${className ?? ""}`.trim(), children: /* @__PURE__ */ jsx14("audio", { src: resolvedSrc, controls, preload }) });
2889
+ return /* @__PURE__ */ jsx17("span", { className: `squisq-inline-audio-player ${className ?? ""}`.trim(), children: /* @__PURE__ */ jsx17("audio", { src: resolvedSrc, controls, preload }) });
2191
2890
  }
2192
2891
 
2193
2892
  // src/MarkdownRenderer.tsx
2194
- import { jsx as jsx15, jsxs as jsxs9 } from "react/jsx-runtime";
2195
- function renderInline(nodes, keyPrefix = "") {
2893
+ import { jsx as jsx18, jsxs as jsxs11 } from "react/jsx-runtime";
2894
+ var DEFAULT_CTX = { htmlPolicy: "sanitize" };
2895
+ function renderInline(nodes, keyPrefix = "", ctx = DEFAULT_CTX) {
2196
2896
  return nodes.map((node, i) => {
2197
2897
  const key = `${keyPrefix}i${i}`;
2198
2898
  switch (node.type) {
2199
2899
  case "text": {
2200
2900
  if (!node.value.includes("\n")) {
2201
- return /* @__PURE__ */ jsx15(Fragment, { children: node.value }, key);
2901
+ return /* @__PURE__ */ jsx18(Fragment2, { children: node.value }, key);
2202
2902
  }
2203
2903
  const parts = node.value.split("\n");
2204
- return /* @__PURE__ */ jsx15(Fragment, { children: parts.map((part, j) => /* @__PURE__ */ jsxs9(Fragment, { children: [
2205
- j > 0 && /* @__PURE__ */ jsx15("br", {}),
2904
+ return /* @__PURE__ */ jsx18(Fragment2, { children: parts.map((part, j) => /* @__PURE__ */ jsxs11(Fragment2, { children: [
2905
+ j > 0 && /* @__PURE__ */ jsx18("br", {}),
2206
2906
  part
2207
2907
  ] }, j)) }, key);
2208
2908
  }
2209
2909
  case "emphasis":
2210
- return /* @__PURE__ */ jsx15("em", { className: "squisq-md-em", children: renderInline(node.children, key) }, key);
2910
+ return /* @__PURE__ */ jsx18("em", { className: "squisq-md-em", children: renderInline(node.children, key, ctx) }, key);
2211
2911
  case "strong":
2212
- return /* @__PURE__ */ jsx15("strong", { className: "squisq-md-strong", children: renderInline(node.children, key) }, key);
2912
+ return /* @__PURE__ */ jsx18("strong", { className: "squisq-md-strong", children: renderInline(node.children, key, ctx) }, key);
2213
2913
  case "delete":
2214
- return /* @__PURE__ */ jsx15("del", { className: "squisq-md-del", children: renderInline(node.children, key) }, key);
2914
+ return /* @__PURE__ */ jsx18("del", { className: "squisq-md-del", children: renderInline(node.children, key, ctx) }, key);
2215
2915
  case "inlineCode":
2216
- return /* @__PURE__ */ jsx15("code", { className: "squisq-md-inline-code", children: node.value }, key);
2217
- case "link":
2218
- return /* @__PURE__ */ jsx15(
2916
+ return /* @__PURE__ */ jsx18("code", { className: "squisq-md-inline-code", children: node.value }, key);
2917
+ case "link": {
2918
+ const href = sanitizeUrl(node.url, "link", { extraLinkSchemes: ctx.linkSchemes });
2919
+ if (!href) {
2920
+ return /* @__PURE__ */ jsx18("span", { className: "squisq-md-link squisq-md-link--blocked", children: renderInline(node.children, key, ctx) }, key);
2921
+ }
2922
+ return /* @__PURE__ */ jsx18(
2219
2923
  "a",
2220
2924
  {
2221
2925
  className: "squisq-md-link",
2222
- href: node.url,
2926
+ href,
2223
2927
  title: node.title ?? void 0,
2224
2928
  target: "_blank",
2225
2929
  rel: "noopener noreferrer",
2226
- children: renderInline(node.children, key)
2930
+ children: renderInline(node.children, key, ctx)
2227
2931
  },
2228
2932
  key
2229
2933
  );
2934
+ }
2230
2935
  case "image":
2231
- return /* @__PURE__ */ jsx15(MdImage, { src: node.url, alt: node.alt ?? "", title: node.title ?? void 0 }, key);
2936
+ return /* @__PURE__ */ jsx18(MdImage, { src: node.url, alt: node.alt ?? "", title: node.title ?? void 0 }, key);
2232
2937
  case "break":
2233
- return /* @__PURE__ */ jsx15("br", {}, key);
2938
+ return /* @__PURE__ */ jsx18("br", {}, key);
2234
2939
  case "inlineMath":
2235
- return /* @__PURE__ */ jsx15("code", { className: "squisq-md-inline-math", children: node.value }, key);
2940
+ return /* @__PURE__ */ jsx18("code", { className: "squisq-md-inline-math", children: node.value }, key);
2236
2941
  case "htmlInline":
2237
- if (!containsMediaTag(node.htmlChildren)) {
2238
- return /* @__PURE__ */ jsx15(
2942
+ if (ctx.htmlPolicy === "strip") return null;
2943
+ if (ctx.htmlPolicy === "trusted" && !containsMediaTag(node.htmlChildren) && !containsDangerousTag(node.htmlChildren) && !hasDangerousRawHtml(node.rawHtml)) {
2944
+ return /* @__PURE__ */ jsx18(
2239
2945
  "span",
2240
2946
  {
2241
2947
  className: "squisq-md-html-inline",
@@ -2244,25 +2950,25 @@ function renderInline(nodes, keyPrefix = "") {
2244
2950
  key
2245
2951
  );
2246
2952
  }
2247
- return /* @__PURE__ */ jsx15("span", { className: "squisq-md-html-inline", children: renderHtmlNodes(node.htmlChildren, `${key}h`) }, key);
2953
+ return /* @__PURE__ */ jsx18("span", { className: "squisq-md-html-inline", children: renderHtmlNodes(resolveHtmlNodes(node.htmlChildren, ctx.htmlPolicy), `${key}h`) }, key);
2248
2954
  case "footnoteReference":
2249
- return /* @__PURE__ */ jsx15("sup", { className: "squisq-md-footnote-ref", children: /* @__PURE__ */ jsxs9("a", { href: `#fn-${node.identifier}`, children: [
2955
+ return /* @__PURE__ */ jsx18("sup", { className: "squisq-md-footnote-ref", children: /* @__PURE__ */ jsxs11("a", { href: `#fn-${node.identifier}`, children: [
2250
2956
  "[",
2251
2957
  node.label ?? node.identifier,
2252
2958
  "]"
2253
2959
  ] }) }, key);
2254
2960
  case "linkReference":
2255
- return /* @__PURE__ */ jsx15("span", { className: "squisq-md-link-ref", children: renderInline(node.children, key) }, key);
2961
+ return /* @__PURE__ */ jsx18("span", { className: "squisq-md-link-ref", children: renderInline(node.children, key, ctx) }, key);
2256
2962
  case "imageReference":
2257
- return /* @__PURE__ */ jsxs9("span", { className: "squisq-md-image-ref", children: [
2963
+ return /* @__PURE__ */ jsxs11("span", { className: "squisq-md-image-ref", children: [
2258
2964
  "[",
2259
2965
  node.alt ?? node.identifier,
2260
2966
  "]"
2261
2967
  ] }, key);
2262
2968
  case "textDirective":
2263
- return /* @__PURE__ */ jsx15("span", { className: "squisq-md-text-directive", "data-directive": node.name, children: renderInline(node.children, key) }, key);
2969
+ return /* @__PURE__ */ jsx18("span", { className: "squisq-md-text-directive", "data-directive": node.name, children: renderInline(node.children, key, ctx) }, key);
2264
2970
  case "mention":
2265
- return /* @__PURE__ */ jsxs9(
2971
+ return /* @__PURE__ */ jsxs11(
2266
2972
  "span",
2267
2973
  {
2268
2974
  className: "squisq-md-mention mention",
@@ -2282,30 +2988,31 @@ function renderInline(nodes, keyPrefix = "") {
2282
2988
  }
2283
2989
  });
2284
2990
  }
2285
- function renderBlock(node, key) {
2991
+ function renderBlock(node, key, ctx = DEFAULT_CTX) {
2286
2992
  switch (node.type) {
2287
2993
  case "paragraph":
2288
- return /* @__PURE__ */ jsx15("p", { className: "squisq-md-p", children: renderInline(node.children, key) }, key);
2994
+ return /* @__PURE__ */ jsx18("p", { className: "squisq-md-p", children: renderInline(node.children, key, ctx) }, key);
2289
2995
  case "heading": {
2290
2996
  const Tag = `h${node.depth}`;
2291
- return /* @__PURE__ */ jsx15(Tag, { className: `squisq-md-heading squisq-md-h${node.depth}`, children: renderInline(node.children, key) }, key);
2997
+ return /* @__PURE__ */ jsx18(Tag, { className: `squisq-md-heading squisq-md-h${node.depth}`, children: renderInline(node.children, key, ctx) }, key);
2292
2998
  }
2293
2999
  case "blockquote":
2294
- return /* @__PURE__ */ jsx15("blockquote", { className: "squisq-md-blockquote", children: renderBlocks(node.children, key) }, key);
3000
+ return /* @__PURE__ */ jsx18("blockquote", { className: "squisq-md-blockquote", children: renderBlocks(node.children, key, ctx) }, key);
2295
3001
  case "list":
2296
3002
  if (node.ordered) {
2297
- return /* @__PURE__ */ jsx15("ol", { className: "squisq-md-list squisq-md-ol", start: node.start ?? void 0, children: node.children.map((item, i) => renderListItem(item, `${key}li${i}`)) }, key);
3003
+ return /* @__PURE__ */ jsx18("ol", { className: "squisq-md-list squisq-md-ol", start: node.start ?? void 0, children: node.children.map((item, i) => renderListItem(item, `${key}li${i}`, ctx)) }, key);
2298
3004
  }
2299
- return /* @__PURE__ */ jsx15("ul", { className: "squisq-md-list squisq-md-ul", children: node.children.map((item, i) => renderListItem(item, `${key}li${i}`)) }, key);
3005
+ return /* @__PURE__ */ jsx18("ul", { className: "squisq-md-list squisq-md-ul", children: node.children.map((item, i) => renderListItem(item, `${key}li${i}`, ctx)) }, key);
2300
3006
  case "code":
2301
- return /* @__PURE__ */ jsx15("pre", { className: "squisq-md-code-block", children: /* @__PURE__ */ jsx15("code", { className: node.lang ? `language-${node.lang}` : void 0, children: node.value }) }, key);
3007
+ return /* @__PURE__ */ jsx18("pre", { className: "squisq-md-code-block", children: /* @__PURE__ */ jsx18("code", { className: node.lang ? `language-${node.lang}` : void 0, children: node.value }) }, key);
2302
3008
  case "thematicBreak":
2303
- return /* @__PURE__ */ jsx15("hr", { className: "squisq-md-hr" }, key);
3009
+ return /* @__PURE__ */ jsx18("hr", { className: "squisq-md-hr" }, key);
2304
3010
  case "table":
2305
- return renderTable(node.children, node.align, key);
3011
+ return renderTable(node.children, node.align, key, ctx);
2306
3012
  case "htmlBlock":
2307
- if (!containsMediaTag(node.htmlChildren)) {
2308
- return /* @__PURE__ */ jsx15(
3013
+ if (ctx.htmlPolicy === "strip") return null;
3014
+ if (ctx.htmlPolicy === "trusted" && !containsMediaTag(node.htmlChildren) && !containsDangerousTag(node.htmlChildren) && !hasDangerousRawHtml(node.rawHtml)) {
3015
+ return /* @__PURE__ */ jsx18(
2309
3016
  "div",
2310
3017
  {
2311
3018
  className: "squisq-md-html-block",
@@ -2314,95 +3021,126 @@ function renderBlock(node, key) {
2314
3021
  key
2315
3022
  );
2316
3023
  }
2317
- return /* @__PURE__ */ jsx15("div", { className: "squisq-md-html-block", children: renderHtmlNodes(node.htmlChildren, `${key}h`) }, key);
3024
+ return /* @__PURE__ */ jsx18("div", { className: "squisq-md-html-block", children: renderHtmlNodes(resolveHtmlNodes(node.htmlChildren, ctx.htmlPolicy), `${key}h`) }, key);
2318
3025
  case "math":
2319
- return /* @__PURE__ */ jsx15("pre", { className: "squisq-md-math-block", children: /* @__PURE__ */ jsx15("code", { children: node.value }) }, key);
3026
+ return /* @__PURE__ */ jsx18("pre", { className: "squisq-md-math-block", children: /* @__PURE__ */ jsx18("code", { children: node.value }) }, key);
2320
3027
  case "definition":
2321
3028
  return null;
2322
3029
  case "footnoteDefinition":
2323
- return /* @__PURE__ */ jsxs9("div", { className: "squisq-md-footnote-def", id: `fn-${node.identifier}`, children: [
2324
- /* @__PURE__ */ jsx15("sup", { children: node.label ?? node.identifier }),
2325
- renderBlocks(node.children, key)
3030
+ return /* @__PURE__ */ jsxs11("div", { className: "squisq-md-footnote-def", id: `fn-${node.identifier}`, children: [
3031
+ /* @__PURE__ */ jsx18("sup", { children: node.label ?? node.identifier }),
3032
+ renderBlocks(node.children, key, ctx)
2326
3033
  ] }, key);
2327
3034
  case "containerDirective":
2328
- return /* @__PURE__ */ jsxs9(
3035
+ return /* @__PURE__ */ jsxs11(
2329
3036
  "div",
2330
3037
  {
2331
3038
  className: `squisq-md-directive squisq-md-directive-${node.name}`,
2332
3039
  "data-directive": node.name,
2333
3040
  children: [
2334
- node.label && /* @__PURE__ */ jsx15("div", { className: "squisq-md-directive-label", children: node.label }),
2335
- renderBlocks(node.children, key)
3041
+ node.label && /* @__PURE__ */ jsx18("div", { className: "squisq-md-directive-label", children: node.label }),
3042
+ renderBlocks(node.children, key, ctx)
2336
3043
  ]
2337
3044
  },
2338
3045
  key
2339
3046
  );
2340
3047
  case "leafDirective":
2341
- return /* @__PURE__ */ jsx15(
3048
+ return /* @__PURE__ */ jsx18(
2342
3049
  "div",
2343
3050
  {
2344
3051
  className: `squisq-md-directive squisq-md-directive-${node.name}`,
2345
3052
  "data-directive": node.name,
2346
- children: renderInline(node.children, key)
3053
+ children: renderInline(node.children, key, ctx)
2347
3054
  },
2348
3055
  key
2349
3056
  );
2350
3057
  case "definitionList":
2351
- return /* @__PURE__ */ jsx15("dl", { className: "squisq-md-dl", children: node.children.map((child, i) => {
3058
+ return /* @__PURE__ */ jsx18("dl", { className: "squisq-md-dl", children: node.children.map((child, i) => {
2352
3059
  if (child.type === "definitionTerm") {
2353
- return /* @__PURE__ */ jsx15("dt", { className: "squisq-md-dt", children: renderInline(child.children, `${key}dt${i}`) }, `${key}dt${i}`);
3060
+ return /* @__PURE__ */ jsx18("dt", { className: "squisq-md-dt", children: renderInline(child.children, `${key}dt${i}`, ctx) }, `${key}dt${i}`);
2354
3061
  }
2355
- return /* @__PURE__ */ jsx15("dd", { className: "squisq-md-dd", children: renderBlocks(child.children, `${key}dd${i}`) }, `${key}dd${i}`);
3062
+ return /* @__PURE__ */ jsx18("dd", { className: "squisq-md-dd", children: renderBlocks(child.children, `${key}dd${i}`, ctx) }, `${key}dd${i}`);
2356
3063
  }) }, key);
2357
3064
  default:
2358
3065
  return null;
2359
3066
  }
2360
3067
  }
2361
- function renderListItem(item, key) {
3068
+ function renderListItem(item, key, ctx = DEFAULT_CTX) {
2362
3069
  const isTask = item.checked !== null && item.checked !== void 0;
2363
- return /* @__PURE__ */ jsxs9("li", { className: `squisq-md-li${isTask ? " squisq-md-task" : ""}`, children: [
2364
- isTask && /* @__PURE__ */ jsx15("input", { type: "checkbox", checked: !!item.checked, readOnly: true, className: "squisq-md-checkbox" }),
2365
- renderBlocks(item.children, key)
3070
+ return /* @__PURE__ */ jsxs11("li", { className: `squisq-md-li${isTask ? " squisq-md-task" : ""}`, children: [
3071
+ isTask && /* @__PURE__ */ jsx18("input", { type: "checkbox", checked: !!item.checked, readOnly: true, className: "squisq-md-checkbox" }),
3072
+ renderBlocks(item.children, key, ctx)
2366
3073
  ] }, key);
2367
3074
  }
2368
- function renderTable(rows, align, key) {
3075
+ function renderTable(rows, align, key, ctx = DEFAULT_CTX) {
2369
3076
  const [headerRow, ...bodyRows] = rows;
2370
- return /* @__PURE__ */ jsxs9("table", { className: "squisq-md-table", children: [
2371
- headerRow && /* @__PURE__ */ jsx15("thead", { children: /* @__PURE__ */ jsx15("tr", { children: headerRow.children.map((cell, ci) => /* @__PURE__ */ jsx15(
3077
+ return /* @__PURE__ */ jsxs11("table", { className: "squisq-md-table", children: [
3078
+ headerRow && /* @__PURE__ */ jsx18("thead", { children: /* @__PURE__ */ jsx18("tr", { children: headerRow.children.map((cell, ci) => /* @__PURE__ */ jsx18(
2372
3079
  "th",
2373
3080
  {
2374
3081
  className: "squisq-md-th",
2375
3082
  style: align?.[ci] ? { textAlign: align[ci] } : void 0,
2376
- children: renderInline(cell.children, `${key}th${ci}`)
3083
+ children: renderInline(cell.children, `${key}th${ci}`, ctx)
2377
3084
  },
2378
3085
  `${key}th${ci}`
2379
3086
  )) }) }),
2380
- bodyRows.length > 0 && /* @__PURE__ */ jsx15("tbody", { children: bodyRows.map((row, ri) => /* @__PURE__ */ jsx15("tr", { children: row.children.map((cell, ci) => /* @__PURE__ */ jsx15(
3087
+ bodyRows.length > 0 && /* @__PURE__ */ jsx18("tbody", { children: bodyRows.map((row, ri) => /* @__PURE__ */ jsx18("tr", { children: row.children.map((cell, ci) => /* @__PURE__ */ jsx18(
2381
3088
  "td",
2382
3089
  {
2383
3090
  className: "squisq-md-td",
2384
3091
  style: align?.[ci] ? { textAlign: align[ci] } : void 0,
2385
- children: renderInline(cell.children, `${key}td${ri}-${ci}`)
3092
+ children: renderInline(cell.children, `${key}td${ri}-${ci}`, ctx)
2386
3093
  },
2387
3094
  `${key}td${ri}-${ci}`
2388
3095
  )) }, `${key}tr${ri}`)) })
2389
3096
  ] }, key);
2390
3097
  }
2391
- function renderBlocks(nodes, keyPrefix = "") {
2392
- return nodes.map((node, i) => renderBlock(node, `${keyPrefix}b${i}`));
3098
+ function renderBlocks(nodes, keyPrefix = "", ctx = DEFAULT_CTX) {
3099
+ return nodes.map((node, i) => renderBlock(node, `${keyPrefix}b${i}`, ctx));
2393
3100
  }
2394
3101
  function MdImage({ src, alt, title }) {
2395
- const resolved = useMediaUrl(src, ".");
2396
- return /* @__PURE__ */ jsx15("img", { className: "squisq-md-image", src: resolved, alt, title });
3102
+ const safeSrc = sanitizeUrl(src, "media");
3103
+ const resolved = useMediaUrl(safeSrc ?? "", ".");
3104
+ if (!safeSrc) return null;
3105
+ return /* @__PURE__ */ jsx18("img", { className: "squisq-md-image", src: resolved, alt, title });
3106
+ }
3107
+ function resolveHtmlNodes(nodes, htmlPolicy) {
3108
+ if (htmlPolicy === "strip") return [];
3109
+ if (htmlPolicy === "trusted") return nodes;
3110
+ return sanitizeHtmlNodes2(nodes);
2397
3111
  }
2398
3112
  function containsMediaTag(nodes) {
2399
3113
  for (const node of nodes) {
2400
3114
  if (node.type !== "htmlElement") continue;
2401
- if (node.tagName === "video" || node.tagName === "audio") return true;
3115
+ const tagName = node.tagName.toLowerCase();
3116
+ if (tagName === "video" || tagName === "audio") return true;
2402
3117
  if (containsMediaTag(node.children)) return true;
2403
3118
  }
2404
3119
  return false;
2405
3120
  }
3121
+ var DANGEROUS_HTML_TAGS = /* @__PURE__ */ new Set([
3122
+ "base",
3123
+ "embed",
3124
+ "iframe",
3125
+ "link",
3126
+ "meta",
3127
+ "object",
3128
+ "script",
3129
+ "style",
3130
+ "title"
3131
+ ]);
3132
+ function containsDangerousTag(nodes) {
3133
+ for (const node of nodes) {
3134
+ if (node.type !== "htmlElement") continue;
3135
+ if (DANGEROUS_HTML_TAGS.has(node.tagName.toLowerCase())) return true;
3136
+ if (containsDangerousTag(node.children)) return true;
3137
+ }
3138
+ return false;
3139
+ }
3140
+ var DANGEROUS_RAW_HTML_RE = /<\s*\/?\s*(?:base|embed|iframe|link|meta|object|script|style|title)\b/i;
3141
+ function hasDangerousRawHtml(rawHtml) {
3142
+ return DANGEROUS_RAW_HTML_RE.test(rawHtml);
3143
+ }
2406
3144
  var PASSTHROUGH_ATTRS = {
2407
3145
  // common
2408
3146
  class: "className",
@@ -2412,6 +3150,10 @@ var PASSTHROUGH_ATTRS = {
2412
3150
  // media-adjacent (used when video/audio appear inside other wrappers)
2413
3151
  width: "width",
2414
3152
  height: "height",
3153
+ src: "src",
3154
+ alt: "alt",
3155
+ loading: "loading",
3156
+ decoding: "decoding",
2415
3157
  // anchor
2416
3158
  href: "href",
2417
3159
  target: "target",
@@ -2431,8 +3173,10 @@ function reactPropsFromAttrs(attrs) {
2431
3173
  return out;
2432
3174
  }
2433
3175
  function renderHtmlElement(el, key) {
2434
- if (el.tagName === "video") {
2435
- return /* @__PURE__ */ jsx15(
3176
+ const tagName = el.tagName.toLowerCase();
3177
+ if (DANGEROUS_HTML_TAGS.has(tagName)) return null;
3178
+ if (tagName === "video") {
3179
+ return /* @__PURE__ */ jsx18(
2436
3180
  InlineVideoPlayer,
2437
3181
  {
2438
3182
  src: el.attributes.src ?? "",
@@ -2445,8 +3189,8 @@ function renderHtmlElement(el, key) {
2445
3189
  key
2446
3190
  );
2447
3191
  }
2448
- if (el.tagName === "audio") {
2449
- return /* @__PURE__ */ jsx15(
3192
+ if (tagName === "audio") {
3193
+ return /* @__PURE__ */ jsx18(
2450
3194
  InlineAudioPlayer,
2451
3195
  {
2452
3196
  src: el.attributes.src ?? "",
@@ -2456,12 +3200,12 @@ function renderHtmlElement(el, key) {
2456
3200
  key
2457
3201
  );
2458
3202
  }
2459
- const Tag = el.tagName;
3203
+ const Tag = tagName;
2460
3204
  const props = reactPropsFromAttrs(el.attributes);
2461
3205
  if (el.selfClosing) {
2462
- return /* @__PURE__ */ jsx15(Tag, { ...props }, key);
3206
+ return /* @__PURE__ */ jsx18(Tag, { ...props }, key);
2463
3207
  }
2464
- return /* @__PURE__ */ jsx15(Tag, { ...props, children: renderHtmlNodes(el.children, `${key}c`) }, key);
3208
+ return /* @__PURE__ */ jsx18(Tag, { ...props, children: renderHtmlNodes(el.children, `${key}c`) }, key);
2465
3209
  }
2466
3210
  function renderHtmlNodes(nodes, keyPrefix) {
2467
3211
  return nodes.map((node, i) => {
@@ -2470,7 +3214,7 @@ function renderHtmlNodes(nodes, keyPrefix) {
2470
3214
  case "htmlElement":
2471
3215
  return renderHtmlElement(node, key);
2472
3216
  case "htmlText":
2473
- return /* @__PURE__ */ jsx15(Fragment, { children: node.value }, key);
3217
+ return /* @__PURE__ */ jsx18(Fragment2, { children: node.value }, key);
2474
3218
  case "htmlComment":
2475
3219
  return null;
2476
3220
  default:
@@ -2478,17 +3222,32 @@ function renderHtmlNodes(nodes, keyPrefix) {
2478
3222
  }
2479
3223
  });
2480
3224
  }
2481
- function MarkdownRenderer({ nodes, className }) {
3225
+ function MarkdownRenderer({
3226
+ nodes,
3227
+ className,
3228
+ htmlPolicy = "sanitize",
3229
+ linkSchemes
3230
+ }) {
2482
3231
  if (!nodes || nodes.length === 0) return null;
2483
- return /* @__PURE__ */ jsx15("div", { className: `squisq-md ${className || ""}`, children: renderBlocks(nodes) });
3232
+ return /* @__PURE__ */ jsx18("div", { className: `squisq-md ${className || ""}`, children: renderBlocks(nodes, "", { htmlPolicy, linkSchemes }) });
2484
3233
  }
2485
3234
 
2486
3235
  // src/LinearDocView.tsx
2487
- import { jsx as jsx16, jsxs as jsxs10 } from "react/jsx-runtime";
3236
+ import { jsx as jsx19, jsxs as jsxs12 } from "react/jsx-runtime";
3237
+ var warnedUnknownTemplates = /* @__PURE__ */ new Set();
2488
3238
  function isAnnotatedBlock(block) {
2489
3239
  const annotation = block.sourceHeading?.templateAnnotation;
2490
- if (!annotation) return false;
2491
- return !!annotation.template && hasTemplate(annotation.template);
3240
+ if (!annotation?.template) return false;
3241
+ if (!hasTemplate(annotation.template)) {
3242
+ if (!warnedUnknownTemplates.has(annotation.template)) {
3243
+ warnedUnknownTemplates.add(annotation.template);
3244
+ console.warn(
3245
+ `[squisq] Unknown template "${annotation.template}" \u2014 rendering the block as plain markdown.`
3246
+ );
3247
+ }
3248
+ return false;
3249
+ }
3250
+ return true;
2492
3251
  }
2493
3252
  function countAll(blocks) {
2494
3253
  let count = 0;
@@ -2500,11 +3259,10 @@ function countAll(blocks) {
2500
3259
  }
2501
3260
  function BlockSection({ block, basePath, viewport, renderContext, blockIndex }) {
2502
3261
  const isAnnotated = isAnnotatedBlock(block);
2503
- const visualBlock = useMemo6(() => {
3262
+ const visualBlock = useMemo8(() => {
2504
3263
  if (!isAnnotated) return null;
2505
3264
  const annotation = block.sourceHeading.templateAnnotation;
2506
3265
  const headingText = extractPlainText(block.sourceHeading);
2507
- const bodyText = extractBodyPlainText(block.contents);
2508
3266
  const templateBlock = {
2509
3267
  id: block.id,
2510
3268
  template: annotation.template,
@@ -2512,12 +3270,14 @@ function BlockSection({ block, basePath, viewport, renderContext, blockIndex })
2512
3270
  duration: 1,
2513
3271
  audioSegment: 0,
2514
3272
  title: headingText,
2515
- ...getTemplateDefaults(
3273
+ ...deriveTemplateInputs(
2516
3274
  annotation.template ?? "sectionHeader",
2517
3275
  headingText,
2518
- bodyText,
2519
- block.contents
2520
- ),
3276
+ block.contents,
3277
+ {
3278
+ placeholders: true
3279
+ }
3280
+ ) ?? {},
2521
3281
  ...annotation.params,
2522
3282
  ...block.templateOverrides
2523
3283
  };
@@ -2532,15 +3292,15 @@ function BlockSection({ block, basePath, viewport, renderContext, blockIndex })
2532
3292
  template: annotation.template
2533
3293
  };
2534
3294
  }, [block, isAnnotated, renderContext, blockIndex]);
2535
- return /* @__PURE__ */ jsxs10(
3295
+ return /* @__PURE__ */ jsxs12(
2536
3296
  "div",
2537
3297
  {
2538
3298
  className: "squisq-linear-section",
2539
3299
  "data-block-id": block.id,
2540
3300
  "data-template": isAnnotated ? block.sourceHeading?.templateAnnotation?.template : void 0,
2541
3301
  children: [
2542
- block.sourceHeading && !isAnnotated && /* @__PURE__ */ jsx16(MarkdownRenderer, { nodes: [block.sourceHeading] }),
2543
- isAnnotated && visualBlock && /* @__PURE__ */ jsx16("div", { className: "squisq-linear-card", children: /* @__PURE__ */ jsx16(
3302
+ block.sourceHeading && !isAnnotated && /* @__PURE__ */ jsx19(MarkdownRenderer, { nodes: [block.sourceHeading] }),
3303
+ isAnnotated && visualBlock && /* @__PURE__ */ jsx19("div", { className: "squisq-linear-card", children: /* @__PURE__ */ jsx19(
2544
3304
  "div",
2545
3305
  {
2546
3306
  className: "squisq-linear-card-svg",
@@ -2550,7 +3310,7 @@ function BlockSection({ block, basePath, viewport, renderContext, blockIndex })
2550
3310
  overflow: "hidden",
2551
3311
  marginBottom: "1em"
2552
3312
  },
2553
- children: /* @__PURE__ */ jsx16(
3313
+ children: /* @__PURE__ */ jsx19(
2554
3314
  BlockRenderer,
2555
3315
  {
2556
3316
  block: visualBlock,
@@ -2561,8 +3321,8 @@ function BlockSection({ block, basePath, viewport, renderContext, blockIndex })
2561
3321
  )
2562
3322
  }
2563
3323
  ) }),
2564
- !isAnnotated && block.contents && block.contents.length > 0 && /* @__PURE__ */ jsx16(MarkdownRenderer, { nodes: block.contents }),
2565
- block.children && block.children.length > 0 && /* @__PURE__ */ jsx16("div", { className: "squisq-linear-children", children: block.children.map((child, i) => /* @__PURE__ */ jsx16(
3324
+ !isAnnotated && block.contents && block.contents.length > 0 && /* @__PURE__ */ jsx19(MarkdownRenderer, { nodes: block.contents }),
3325
+ block.children && block.children.length > 0 && /* @__PURE__ */ jsx19("div", { className: "squisq-linear-children", children: block.children.map((child, i) => /* @__PURE__ */ jsx19(
2566
3326
  BlockSection,
2567
3327
  {
2568
3328
  block: child,
@@ -2577,141 +3337,9 @@ function BlockSection({ block, basePath, viewport, renderContext, blockIndex })
2577
3337
  }
2578
3338
  );
2579
3339
  }
2580
- function extractBodyPlainText(contents) {
2581
- if (!contents || contents.length === 0) return "";
2582
- return contents.map((n) => extractPlainText(n)).join("\n").trim();
2583
- }
2584
- function extractListItems(contents) {
2585
- if (!contents) return [];
2586
- const items = [];
2587
- for (const node of contents) {
2588
- if (node.type === "list") {
2589
- for (const item of node.children) {
2590
- const text = extractPlainText(item).trim();
2591
- if (text) items.push(text);
2592
- }
2593
- }
2594
- }
2595
- return items;
2596
- }
2597
- function extractFirstImage(contents) {
2598
- if (!contents || contents.length === 0) return null;
2599
- function fromHtml(nodes) {
2600
- for (const node of nodes) {
2601
- if (!node || typeof node !== "object") continue;
2602
- const n = node;
2603
- if (n.type === "htmlElement" && n.tagName === "img") {
2604
- const attrs = n.attributes;
2605
- if (attrs && typeof attrs.src === "string" && attrs.src) {
2606
- return {
2607
- src: attrs.src,
2608
- alt: typeof attrs.alt === "string" ? attrs.alt : "",
2609
- width: parseDim(attrs.width),
2610
- height: parseDim(attrs.height)
2611
- };
2612
- }
2613
- }
2614
- if (Array.isArray(n.children)) {
2615
- const found = fromHtml(n.children);
2616
- if (found) return found;
2617
- }
2618
- }
2619
- return null;
2620
- }
2621
- function walk(node) {
2622
- if (!node || typeof node !== "object") return null;
2623
- const n = node;
2624
- if (n.type === "image" && typeof n.url === "string" && n.url) {
2625
- return { src: n.url, alt: typeof n.alt === "string" ? n.alt : "" };
2626
- }
2627
- if ((n.type === "htmlBlock" || n.type === "htmlInline") && Array.isArray(n.htmlChildren)) {
2628
- const found = fromHtml(n.htmlChildren);
2629
- if (found) return found;
2630
- }
2631
- if (Array.isArray(n.children)) {
2632
- for (const child of n.children) {
2633
- const found = walk(child);
2634
- if (found) return found;
2635
- }
2636
- }
2637
- return null;
2638
- }
2639
- for (const node of contents) {
2640
- const found = walk(node);
2641
- if (found) return found;
2642
- }
2643
- return null;
2644
- }
2645
- function parseDim(raw) {
2646
- if (raw === void 0) return void 0;
2647
- const n = parseFloat(raw);
2648
- return Number.isFinite(n) && n > 0 ? n : void 0;
2649
- }
2650
- function extractTableData(contents) {
2651
- if (!contents) return null;
2652
- for (const node of contents) {
2653
- if (node.type === "table") {
2654
- const table = node;
2655
- const [headerRow, ...bodyRows] = table.children;
2656
- if (!headerRow) return null;
2657
- const headers = headerRow.children.map((cell) => extractPlainText(cell).trim());
2658
- const rows = bodyRows.map((row) => row.children.map((cell) => extractPlainText(cell).trim()));
2659
- return { headers, rows, align: table.align };
2660
- }
2661
- }
2662
- return null;
2663
- }
2664
- function getTemplateDefaults(templateName, headingText, bodyText, contents) {
2665
- switch (templateName) {
2666
- case "statHighlight":
2667
- return { stat: headingText, description: bodyText || headingText };
2668
- case "quote":
2669
- case "fullBleedQuote":
2670
- case "pullQuote":
2671
- return { quote: bodyText || headingText };
2672
- case "factCard":
2673
- return { fact: headingText, explanation: bodyText || headingText };
2674
- case "comparisonBar":
2675
- return { leftLabel: "A", leftValue: 60, rightLabel: "B", rightValue: 40 };
2676
- case "list": {
2677
- const items = extractListItems(contents);
2678
- return { items: items.length > 0 ? items : ["Item 1", "Item 2", "Item 3"] };
2679
- }
2680
- case "definitionCard":
2681
- return { term: headingText, definition: bodyText || headingText };
2682
- case "dateEvent":
2683
- return { date: headingText, description: bodyText || headingText };
2684
- case "dataTable": {
2685
- const tableData = extractTableData(contents);
2686
- return tableData ?? { headers: ["Column"], rows: [["Data"]] };
2687
- }
2688
- case "imageWithCaption": {
2689
- const img = extractFirstImage(contents);
2690
- if (!img) return { caption: headingText };
2691
- return {
2692
- imageSrc: img.src,
2693
- imageAlt: img.alt || headingText,
2694
- caption: headingText
2695
- };
2696
- }
2697
- case "leftFeature":
2698
- case "rightFeature": {
2699
- const img = extractFirstImage(contents);
2700
- return {
2701
- imageSrc: img?.src ?? "",
2702
- imageAlt: img?.alt || headingText,
2703
- imageWidth: img?.width,
2704
- imageHeight: img?.height,
2705
- title: headingText,
2706
- body: bodyText
2707
- };
2708
- }
2709
- default:
2710
- return {};
2711
- }
2712
- }
2713
3340
  function LinearDocView({
2714
3341
  doc,
3342
+ markdown,
2715
3343
  basePath = "/",
2716
3344
  viewport,
2717
3345
  className,
@@ -2721,18 +3349,33 @@ function LinearDocView({
2721
3349
  imageDisplayMode = "inline"
2722
3350
  }) {
2723
3351
  const activeViewport = viewport ?? VIEWPORT_PRESETS3.landscape;
2724
- const totalBlocks = useMemo6(() => countAll(doc.blocks), [doc.blocks]);
3352
+ const markdownDoc = useMemo8(
3353
+ () => !doc && markdown !== void 0 ? markdownToDoc(parseMarkdown(markdown)) : void 0,
3354
+ [doc, markdown]
3355
+ );
3356
+ const resolvedDoc = doc ?? markdownDoc;
3357
+ const totalBlocks = useMemo8(
3358
+ () => resolvedDoc ? countAll(resolvedDoc.blocks) : 0,
3359
+ [resolvedDoc]
3360
+ );
2725
3361
  const autoSurface = useAutoSurface(surface === "auto");
2726
3362
  const resolvedSurface = surface === "auto" ? autoSurface : surface;
2727
- const renderContext = useMemo6(() => {
2728
- const baseTheme = theme ?? DEFAULT_THEME;
3363
+ const renderContext = useMemo8(() => {
3364
+ const baseTheme = theme ?? DEFAULT_THEME2;
3365
+ const effectiveTheme = resolvedSurface ? applySurface(baseTheme, resolvedSurface) : baseTheme;
2729
3366
  return {
2730
- theme: resolvedSurface ? applySurface(baseTheme, resolvedSurface) : baseTheme,
3367
+ theme: effectiveTheme,
2731
3368
  viewport: activeViewport,
2732
- totalBlocks
3369
+ totalBlocks,
3370
+ // Theme atmosphere (vignette/grain/gradient persistent layers) shows
3371
+ // on the inline template cards so they match the player's look.
3372
+ persistentLayers: effectiveTheme.persistentLayers
2733
3373
  };
2734
3374
  }, [activeViewport, totalBlocks, theme, resolvedSurface]);
2735
3375
  const activeTheme = renderContext.theme;
3376
+ if (!resolvedDoc) {
3377
+ return /* @__PURE__ */ jsx19("div", { className: `squisq-linear squisq-linear--empty ${className || ""}` });
3378
+ }
2736
3379
  const bgColor = activeTheme.colors.background;
2737
3380
  const textColor = activeTheme.colors.text;
2738
3381
  const mutedColor = activeTheme.colors.textMuted;
@@ -2740,7 +3383,7 @@ function LinearDocView({
2740
3383
  const bodyFont = resolveFontFamily2(activeTheme.typography.bodyFont, "system-ui, sans-serif");
2741
3384
  const titleFont = resolveFontFamily2(activeTheme.typography.titleFont, "Georgia, serif");
2742
3385
  const lineHt = activeTheme.typography.lineHeight ?? 1.7;
2743
- return /* @__PURE__ */ jsx16(
3386
+ return /* @__PURE__ */ jsx19(
2744
3387
  "div",
2745
3388
  {
2746
3389
  className: `squisq-linear ${className || ""}`,
@@ -2756,7 +3399,7 @@ function LinearDocView({
2756
3399
  overflowX: "hidden",
2757
3400
  background: bgColor
2758
3401
  },
2759
- children: /* @__PURE__ */ jsxs10(
3402
+ children: /* @__PURE__ */ jsxs12(
2760
3403
  "div",
2761
3404
  {
2762
3405
  className: `squisq-linear-content squisq-md${thinMargins ? " squisq-linear-content--thin" : ""}${imageDisplayMode === "thumbnail" ? " squisq-linear-content--thumbnail-images" : ""}`,
@@ -2781,7 +3424,7 @@ function LinearDocView({
2781
3424
  "--squisq-linear-bg": bgColor
2782
3425
  },
2783
3426
  children: [
2784
- /* @__PURE__ */ jsx16("style", { children: `
3427
+ /* @__PURE__ */ jsx19("style", { children: `
2785
3428
  .squisq-linear-content h1,
2786
3429
  .squisq-linear-content h2,
2787
3430
  .squisq-linear-content h3,
@@ -2893,7 +3536,7 @@ function LinearDocView({
2893
3536
  background: color-mix(in srgb, var(--squisq-linear-primary) 8%, transparent);
2894
3537
  }
2895
3538
  ` }),
2896
- doc.blocks.map((block, i) => /* @__PURE__ */ jsx16(
3539
+ resolvedDoc.blocks.map((block, i) => /* @__PURE__ */ jsx19(
2897
3540
  BlockSection,
2898
3541
  {
2899
3542
  block,
@@ -2912,7 +3555,7 @@ function LinearDocView({
2912
3555
  }
2913
3556
 
2914
3557
  // src/DocPlayer.tsx
2915
- import { jsx as jsx17, jsxs as jsxs11 } from "react/jsx-runtime";
3558
+ import { jsx as jsx20, jsxs as jsxs13 } from "react/jsx-runtime";
2916
3559
  var SMALL_WORDS = /* @__PURE__ */ new Set([
2917
3560
  "a",
2918
3561
  "an",
@@ -2930,9 +3573,9 @@ var SMALL_WORDS = /* @__PURE__ */ new Set([
2930
3573
  "by",
2931
3574
  "is"
2932
3575
  ]);
2933
- function buildSegmentTitleMap(script) {
3576
+ function buildSegmentTitleMap(doc) {
2934
3577
  const map = /* @__PURE__ */ new Map();
2935
- for (const block of script.blocks) {
3578
+ for (const block of doc.blocks) {
2936
3579
  if (isTemplateBlock2(block) && block.template === "sectionHeader" && "title" in block) {
2937
3580
  const segIdx = block.audioSegment;
2938
3581
  if (!map.has(segIdx)) {
@@ -2940,9 +3583,9 @@ function buildSegmentTitleMap(script) {
2940
3583
  }
2941
3584
  }
2942
3585
  }
2943
- for (let i = 0; i < script.audio.segments.length; i++) {
3586
+ for (let i = 0; i < doc.audio.segments.length; i++) {
2944
3587
  if (!map.has(i)) {
2945
- const name = script.audio.segments[i].name;
3588
+ const name = doc.audio.segments[i].name;
2946
3589
  if (name === "intro" || name.includes("intro")) {
2947
3590
  map.set(i, "Introduction");
2948
3591
  } else if (name === "flight-context" || name.includes("flight-context")) {
@@ -2958,14 +3601,34 @@ function buildSegmentTitleMap(script) {
2958
3601
  }
2959
3602
  return map;
2960
3603
  }
2961
- function DocPlayer({
2962
- script,
2963
- basePath,
3604
+ function isDevEnvironment() {
3605
+ try {
3606
+ return typeof process !== "undefined" && process.env.NODE_ENV !== "production";
3607
+ } catch {
3608
+ return false;
3609
+ }
3610
+ }
3611
+ var warnedMissingStyles = false;
3612
+ function DocPlayer(props) {
3613
+ const { doc, markdown } = props;
3614
+ const markdownDoc = useMemo9(
3615
+ () => !doc && markdown !== void 0 ? markdownToDoc2(parseMarkdown2(markdown)) : void 0,
3616
+ [doc, markdown]
3617
+ );
3618
+ const resolvedDoc = doc ?? markdownDoc;
3619
+ if (!resolvedDoc) {
3620
+ return /* @__PURE__ */ jsx20("div", { className: "doc-player doc-player--empty" });
3621
+ }
3622
+ return /* @__PURE__ */ jsx20(DocPlayerContent, { ...props, doc: resolvedDoc });
3623
+ }
3624
+ function DocPlayerContent({
3625
+ doc,
3626
+ basePath = ".",
2964
3627
  renderMode = false,
2965
3628
  autoPlay = false,
2966
3629
  onEnded,
2967
3630
  onTimeUpdate,
2968
- audioProvider: externalAudioProvider,
3631
+ audioController: externalAudioController,
2969
3632
  showControls = true,
2970
3633
  showScrubber = false,
2971
3634
  muted = false,
@@ -2980,23 +3643,36 @@ function DocPlayer({
2980
3643
  displayMode = "video",
2981
3644
  theme,
2982
3645
  surface,
2983
- captionStyle = "standard"
3646
+ captionStyle = "standard",
3647
+ enableSwipe = true
2984
3648
  }) {
2985
3649
  const isSlideshowMode = displayMode === "slideshow";
2986
3650
  const isLinearMode = displayMode === "linear";
2987
- const audioRef = useRef5(null);
2988
- const containerRef = useRef5(null);
3651
+ const audioRef = useRef7(null);
3652
+ const containerRef = useRef7(null);
2989
3653
  const [tapFeedback, setTapFeedback] = useState7(null);
2990
- const tapFeedbackTimer = useRef5();
3654
+ const tapFeedbackTimer = useRef7();
2991
3655
  const { viewport, orientation } = useViewportOrientation();
2992
3656
  const activeViewport = forceViewport || (renderMode ? VIEWPORT_PRESETS4.landscape : viewport);
2993
- const isDebugMode = useMemo7(() => {
3657
+ const isDebugMode = useMemo9(() => {
2994
3658
  if (typeof window === "undefined") return false;
2995
3659
  const params = new URLSearchParams(window.location.search);
2996
3660
  return params.get("debug") === "true";
2997
3661
  }, []);
2998
- const internalAudio = useAudioSync(audioRef, script.audio, basePath);
2999
- const audio = externalAudioProvider || internalAudio;
3662
+ const internalAudio = useAudioSync(audioRef, doc.audio, basePath);
3663
+ const audio = externalAudioController || internalAudio;
3664
+ useEffect8(() => {
3665
+ if (warnedMissingStyles || !isDevEnvironment()) return;
3666
+ const el = containerRef.current;
3667
+ if (!el || typeof getComputedStyle !== "function") return;
3668
+ const value = getComputedStyle(el).getPropertyValue("--squisq-styles-loaded");
3669
+ if (!value.trim()) {
3670
+ warnedMissingStyles = true;
3671
+ console.warn(
3672
+ '[squisq] @bendyline/squisq-react/styles is not loaded \u2014 import "@bendyline/squisq-react/styles"'
3673
+ );
3674
+ }
3675
+ }, []);
3000
3676
  const {
3001
3677
  currentTime,
3002
3678
  isPlaying,
@@ -3013,12 +3689,13 @@ function DocPlayer({
3013
3689
  skipToSegment: _skipToSegment,
3014
3690
  restart
3015
3691
  } = audio;
3016
- const currentTimeRef = useRef5(currentTime);
3692
+ const mediaSchedule = useMemo9(() => resolveMediaSchedule(doc), [doc]);
3693
+ const currentTimeRef = useRef7(currentTime);
3017
3694
  currentTimeRef.current = currentTime;
3018
- const totalDurationRef = useRef5(totalDuration);
3695
+ const totalDurationRef = useRef7(totalDuration);
3019
3696
  totalDurationRef.current = totalDuration;
3020
- const expandedBlocksLenRef = useRef5(0);
3021
- const handleContainerClick = useCallback5(
3697
+ const expandedBlocksLenRef = useRef7(0);
3698
+ const handleContainerClick = useCallback6(
3022
3699
  (e) => {
3023
3700
  if (renderMode || isSlideshowMode || isLinearMode) return;
3024
3701
  const target = e.target;
@@ -3036,8 +3713,8 @@ function DocPlayer({
3036
3713
  );
3037
3714
  const autoSurface = useAutoSurface(surface === "auto");
3038
3715
  const resolvedSurface = surface === "auto" ? autoSurface : surface;
3039
- const effectiveTheme = useMemo7(() => {
3040
- const base = theme ?? DEFAULT_THEME2;
3716
+ const effectiveTheme = useMemo9(() => {
3717
+ const base = theme ?? DEFAULT_THEME3;
3041
3718
  return resolvedSurface ? applySurface2(base, resolvedSurface) : base;
3042
3719
  }, [theme, resolvedSurface]);
3043
3720
  const {
@@ -3052,9 +3729,9 @@ function DocPlayer({
3052
3729
  nextBlock: _nextBlock,
3053
3730
  prevBlock: _prevBlock,
3054
3731
  blocks: expandedBlocks
3055
- } = useDocPlayback(script, currentTime, activeViewport, renderMode, effectiveTheme);
3056
- const coverBlock = useMemo7(() => {
3057
- const startBlockConfig = script.startBlock;
3732
+ } = useDocPlayback(doc, currentTime, activeViewport, renderMode, effectiveTheme);
3733
+ const coverBlock = useMemo9(() => {
3734
+ const startBlockConfig = doc.startBlock;
3058
3735
  if (!startBlockConfig) return null;
3059
3736
  const context = createTemplateContext(effectiveTheme, 0, 1, activeViewport);
3060
3737
  const layers = expandCoverBlock(startBlockConfig, context);
@@ -3067,15 +3744,15 @@ function DocPlayer({
3067
3744
  audioSegment: -1,
3068
3745
  layers
3069
3746
  };
3070
- }, [script.startBlock, activeViewport, effectiveTheme]);
3747
+ }, [doc.startBlock, activeViewport, effectiveTheme]);
3071
3748
  const [coverForced, setCoverForced] = useState7(false);
3072
3749
  const [coverGraceActive, setCoverGraceActive] = useState7(false);
3073
- const coverGraceTimer = useRef5();
3074
- const coverWasShowing = useRef5(false);
3075
- const hasPlayedOnce = useRef5(false);
3750
+ const coverGraceTimer = useRef7();
3751
+ const coverWasShowing = useRef7(false);
3752
+ const hasPlayedOnce = useRef7(false);
3076
3753
  const atRest = !!(coverBlock && !isPlaying && currentTime === 0 && !hasPlayedOnce.current && !renderMode && !autoPlay);
3077
3754
  if (atRest) coverWasShowing.current = true;
3078
- useEffect7(() => {
3755
+ useEffect8(() => {
3079
3756
  if (isPlaying && coverWasShowing.current && coverBlock && !renderMode) {
3080
3757
  coverWasShowing.current = false;
3081
3758
  hasPlayedOnce.current = true;
@@ -3083,24 +3760,24 @@ function DocPlayer({
3083
3760
  coverGraceTimer.current = setTimeout(() => setCoverGraceActive(false), 3e3);
3084
3761
  }
3085
3762
  }, [isPlaying, coverBlock, renderMode]);
3086
- useEffect7(() => () => clearTimeout(coverGraceTimer.current), []);
3763
+ useEffect8(() => () => clearTimeout(coverGraceTimer.current), []);
3087
3764
  const showCoverBlock = !isSlideshowMode && !isLinearMode && coverBlock && (coverForced || coverGraceActive || !isPlaying && currentTime === 0 && !hasPlayedOnce.current && !renderMode && !autoPlay);
3088
- const hasAutoPlayed = useRef5(false);
3089
- useEffect7(() => {
3765
+ const hasAutoPlayed = useRef7(false);
3766
+ useEffect8(() => {
3090
3767
  if (isAudioReady && autoPlay && !hasAutoPlayed.current) {
3091
3768
  hasAutoPlayed.current = true;
3092
3769
  play();
3093
3770
  }
3094
3771
  }, [isAudioReady, autoPlay, play]);
3095
- useEffect7(() => {
3772
+ useEffect8(() => {
3096
3773
  onTimeUpdate?.(currentTime);
3097
3774
  }, [currentTime, onTimeUpdate]);
3098
- useEffect7(() => {
3775
+ useEffect8(() => {
3099
3776
  if (isEnded) {
3100
3777
  onEnded?.();
3101
3778
  }
3102
3779
  }, [isEnded, onEnded]);
3103
- useEffect7(() => {
3780
+ useEffect8(() => {
3104
3781
  if ((renderMode || isDebugMode) && typeof window !== "undefined") {
3105
3782
  const w = window;
3106
3783
  w.seekTo = (time) => {
@@ -3133,7 +3810,11 @@ function DocPlayer({
3133
3810
  const video = el;
3134
3811
  const clipStart = parseFloat(video.dataset.clipStart || "0");
3135
3812
  const clipEnd = parseFloat(video.dataset.clipEnd || "0");
3136
- const targetTime = Math.min(clipStart + Math.max(0, blockElapsed), clipEnd);
3813
+ const startAt = parseFloat(video.dataset.startAt || "0");
3814
+ const targetTime = Math.min(
3815
+ clipStart + Math.max(0, blockElapsed - startAt),
3816
+ clipEnd
3817
+ );
3137
3818
  video.pause();
3138
3819
  video.currentTime = targetTime;
3139
3820
  videoSeekPromises.push(
@@ -3148,6 +3829,26 @@ function DocPlayer({
3148
3829
  );
3149
3830
  });
3150
3831
  }
3832
+ document.querySelectorAll("video[data-clip-id]").forEach((el) => {
3833
+ const video = el;
3834
+ const absStart = parseFloat(video.dataset.absStart || "0");
3835
+ const absEnd = parseFloat(video.dataset.absEnd || "0");
3836
+ const sourceIn = parseFloat(video.dataset.sourceIn || "0");
3837
+ video.pause();
3838
+ if (time < absStart || time >= absEnd) return;
3839
+ const targetTime = sourceIn + (time - absStart);
3840
+ video.currentTime = targetTime;
3841
+ videoSeekPromises.push(
3842
+ new Promise((r) => {
3843
+ if (Math.abs(video.currentTime - targetTime) < 0.1) {
3844
+ r();
3845
+ } else {
3846
+ video.addEventListener("seeked", () => r(), { once: true });
3847
+ setTimeout(r, 200);
3848
+ }
3849
+ })
3850
+ );
3851
+ });
3151
3852
  Promise.all(videoSeekPromises).then(() => {
3152
3853
  requestAnimationFrame(() => resolve());
3153
3854
  });
@@ -3155,12 +3856,9 @@ function DocPlayer({
3155
3856
  });
3156
3857
  };
3157
3858
  w.getDuration = () => {
3158
- if (totalDuration > 0) return totalDuration;
3159
- if (expandedBlocks.length > 0) {
3160
- const last = expandedBlocks[expandedBlocks.length - 1];
3161
- return last.startTime + last.duration;
3162
- }
3163
- return 0;
3859
+ const mediaDuration = getDocPlaybackDuration(doc);
3860
+ if (totalDuration > 0) return Math.max(totalDuration, mediaDuration);
3861
+ return mediaDuration;
3164
3862
  };
3165
3863
  w.getBlocks = () => expandedBlocks.map((s) => ({
3166
3864
  id: s.id,
@@ -3168,20 +3866,20 @@ function DocPlayer({
3168
3866
  startTime: s.startTime,
3169
3867
  duration: s.duration
3170
3868
  }));
3171
- w.getAudioSegments = () => script.audio.segments.map((seg) => ({
3869
+ w.getAudioSegments = () => doc.audio.segments.map((seg) => ({
3172
3870
  src: seg.src,
3173
3871
  name: seg.name,
3174
3872
  duration: seg.duration,
3175
3873
  startTime: seg.startTime
3176
3874
  }));
3177
- w.getCaptions = () => script.captions?.phrases?.map((p) => ({
3875
+ w.getCaptions = () => doc.captions?.phrases?.map((p) => ({
3178
3876
  text: p.text,
3179
3877
  startTime: p.startTime,
3180
3878
  endTime: p.endTime
3181
3879
  })) || [];
3182
3880
  w.getChapters = () => {
3183
- const titleMap = buildSegmentTitleMap(script);
3184
- return script.audio.segments.map((seg, i) => ({
3881
+ const titleMap = buildSegmentTitleMap(doc);
3882
+ return doc.audio.segments.map((seg, i) => ({
3185
3883
  title: titleMap.get(i) || seg.name,
3186
3884
  startTime: seg.startTime,
3187
3885
  duration: seg.duration
@@ -3214,25 +3912,28 @@ function DocPlayer({
3214
3912
  }, [renderMode, isDebugMode, seekTo, totalDuration, expandedBlocks, coverBlock]);
3215
3913
  const defaultMode = captionsEnabledProp === false ? "off" : captionStyle || "standard";
3216
3914
  const [captionMode, setCaptionMode] = useState7(defaultMode);
3915
+ useEffect8(() => {
3916
+ setCaptionMode(defaultMode);
3917
+ }, [defaultMode]);
3217
3918
  const captionsEnabled = captionMode !== "off";
3218
3919
  const activeCaptionStyle = captionMode === "social" ? "social" : "standard";
3219
- const setCaptionsEnabled = useCallback5(
3920
+ const setCaptionsEnabled = useCallback6(
3220
3921
  (enabled) => {
3221
3922
  setCaptionMode(enabled ? captionStyle || "standard" : "off");
3222
3923
  onCaptionsToggle?.(enabled);
3223
3924
  },
3224
3925
  [onCaptionsToggle, captionStyle]
3225
3926
  );
3226
- const cycleCaptionMode = useCallback5(() => {
3927
+ const cycleCaptionMode = useCallback6(() => {
3227
3928
  setCaptionMode((prev) => {
3228
3929
  const next = prev === "off" ? "standard" : prev === "standard" ? "social" : "off";
3229
3930
  onCaptionsToggle?.(next !== "off");
3230
3931
  return next;
3231
3932
  });
3232
3933
  }, [onCaptionsToggle]);
3233
- const hasCaptions = script.captions && script.captions.phrases.length > 0;
3234
- const segmentTitleMap = useMemo7(() => buildSegmentTitleMap(script), [script]);
3235
- const playbackState = useMemo7(
3934
+ const hasCaptions = doc.captions && doc.captions.phrases.length > 0;
3935
+ const segmentTitleMap = useMemo9(() => buildSegmentTitleMap(doc), [doc]);
3936
+ const playbackState = useMemo9(
3236
3937
  () => ({
3237
3938
  isPlaying,
3238
3939
  currentTime,
@@ -3245,10 +3946,10 @@ function DocPlayer({
3245
3946
  captionMode,
3246
3947
  isFullscreen,
3247
3948
  currentSegmentIndex: currentSegment,
3248
- currentSegmentName: segmentTitleMap.get(currentSegment) ?? script.audio.segments[currentSegment]?.name ?? null,
3949
+ currentSegmentName: segmentTitleMap.get(currentSegment) ?? doc.audio.segments[currentSegment]?.name ?? null,
3249
3950
  currentBlock: currentBlock ?? null
3250
3951
  }),
3251
- // eslint-disable-next-line react-hooks/exhaustive-deps -- script.audio.segments is stable within a given script
3952
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- doc.audio.segments is stable within a given doc
3252
3953
  [
3253
3954
  isPlaying,
3254
3955
  currentTime,
@@ -3265,7 +3966,7 @@ function DocPlayer({
3265
3966
  currentBlock
3266
3967
  ]
3267
3968
  );
3268
- const playbackActions = useMemo7(
3969
+ const playbackActions = useMemo9(
3269
3970
  () => ({
3270
3971
  toggle,
3271
3972
  restart,
@@ -3276,7 +3977,7 @@ function DocPlayer({
3276
3977
  }),
3277
3978
  [toggle, restart, seekTo, setCaptionsEnabled, cycleCaptionMode, onFullscreenToggle]
3278
3979
  );
3279
- const slideNavActions = useMemo7(
3980
+ const slideNavActions = useMemo9(
3280
3981
  () => ({
3281
3982
  nextSlide: () => {
3282
3983
  if (currentBlockIndex < expandedBlocks.length - 1) {
@@ -3308,13 +4009,22 @@ function DocPlayer({
3308
4009
  }),
3309
4010
  [currentBlockIndex, expandedBlocks, seekTo, pause]
3310
4011
  );
3311
- useEffect7(() => {
4012
+ const swipeEnabled = isSlideshowMode && !isLinearMode && !renderMode && enableSwipe;
4013
+ const swipe = useSlideSwipe({
4014
+ enabled: swipeEnabled,
4015
+ containerRef,
4016
+ canGoNext: currentBlockIndex < expandedBlocks.length - 1,
4017
+ canGoPrev: currentBlockIndex > 0,
4018
+ onNext: slideNavActions.nextSlide,
4019
+ onPrev: slideNavActions.prevSlide
4020
+ });
4021
+ useEffect8(() => {
3312
4022
  onPlaybackStateChange?.(playbackState);
3313
4023
  }, [playbackState, onPlaybackStateChange]);
3314
- useEffect7(() => {
4024
+ useEffect8(() => {
3315
4025
  onControlsReady?.({ play, pause, ...playbackActions });
3316
4026
  }, [play, pause, playbackActions, onControlsReady]);
3317
- const getBlockTitle = useCallback5((block) => {
4027
+ const getBlockTitle = useCallback6((block) => {
3318
4028
  const docBlock = block;
3319
4029
  if (isTemplateBlock2(docBlock)) {
3320
4030
  const props = docBlock;
@@ -3338,7 +4048,7 @@ function DocPlayer({
3338
4048
  }
3339
4049
  return block.id.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
3340
4050
  }, []);
3341
- const blockMarkers = useMemo7(() => {
4051
+ const blockMarkers = useMemo9(() => {
3342
4052
  if (!totalDuration || !expandedBlocks.length) return [];
3343
4053
  let prevSegment = -1;
3344
4054
  return expandedBlocks.map((block, index) => {
@@ -3353,13 +4063,13 @@ function DocPlayer({
3353
4063
  };
3354
4064
  });
3355
4065
  }, [expandedBlocks, totalDuration, getBlockTitle]);
3356
- useEffect7(() => {
4066
+ useEffect8(() => {
3357
4067
  if (blockMarkers.length > 0) {
3358
4068
  onBlockMarkers?.(blockMarkers);
3359
4069
  }
3360
4070
  }, [blockMarkers, onBlockMarkers]);
3361
4071
  expandedBlocksLenRef.current = expandedBlocks.length;
3362
- const handleKeyDown = useCallback5(
4072
+ const handleKeyDown = useCallback6(
3363
4073
  (e) => {
3364
4074
  const activeEl = document.activeElement;
3365
4075
  if (activeEl && (activeEl.tagName === "INPUT" || activeEl.tagName === "TEXTAREA" || activeEl.tagName === "SELECT")) {
@@ -3405,13 +4115,13 @@ function DocPlayer({
3405
4115
  },
3406
4116
  [isSlideshowMode, isLinearMode, toggle, seekTo, slideNavActions]
3407
4117
  );
3408
- useEffect7(() => {
4118
+ useEffect8(() => {
3409
4119
  if (renderMode) return;
3410
4120
  window.addEventListener("keydown", handleKeyDown);
3411
4121
  return () => window.removeEventListener("keydown", handleKeyDown);
3412
4122
  }, [handleKeyDown, renderMode]);
3413
4123
  if (isLinearMode) {
3414
- return /* @__PURE__ */ jsx17(
4124
+ return /* @__PURE__ */ jsx20(
3415
4125
  "div",
3416
4126
  {
3417
4127
  ref: containerRef,
@@ -3422,10 +4132,10 @@ function DocPlayer({
3422
4132
  height: "100%",
3423
4133
  overflow: "hidden"
3424
4134
  },
3425
- children: /* @__PURE__ */ jsx17(
4135
+ children: /* @__PURE__ */ jsx20(
3426
4136
  LinearDocView,
3427
4137
  {
3428
- doc: script,
4138
+ doc,
3429
4139
  basePath,
3430
4140
  viewport: activeViewport,
3431
4141
  theme,
@@ -3435,24 +4145,38 @@ function DocPlayer({
3435
4145
  }
3436
4146
  );
3437
4147
  }
3438
- return /* @__PURE__ */ jsxs11(
4148
+ return /* @__PURE__ */ jsxs13(
3439
4149
  "div",
3440
4150
  {
3441
4151
  ref: containerRef,
3442
- className: "doc-player",
4152
+ className: `doc-player${swipeEnabled ? " doc-player--swipe" : ""}${swipe.phase === "dragging" ? " doc-player--grabbing" : ""}`,
3443
4153
  onClick: handleContainerClick,
4154
+ onPointerDown: swipe.onPointerDown,
3444
4155
  style: {
3445
4156
  position: "relative",
3446
4157
  width: "100%",
3447
4158
  aspectRatio: `${activeViewport.width} / ${activeViewport.height}`,
3448
4159
  margin: "0 auto",
3449
4160
  overflow: "hidden",
3450
- cursor: renderMode ? void 0 : "pointer"
4161
+ // Swipe uses the grab/grabbing cursor via CSS classes; let vertical page
4162
+ // scroll through on touch while we own horizontal drags.
4163
+ cursor: renderMode || swipeEnabled ? void 0 : "pointer",
4164
+ touchAction: swipeEnabled ? "pan-y" : void 0
3451
4165
  },
3452
4166
  children: [
3453
- /* @__PURE__ */ jsx17("audio", { ref: audioRef, preload: "auto", muted }),
3454
- /* @__PURE__ */ jsxs11("div", { className: "doc-player__viewport", children: [
3455
- showCoverBlock && coverBlock && /* @__PURE__ */ jsx17("div", { className: "doc-player__block doc-player__block--cover", children: /* @__PURE__ */ jsx17(
4167
+ /* @__PURE__ */ jsx20("audio", { ref: audioRef, preload: "auto", muted }),
4168
+ /* @__PURE__ */ jsx20(
4169
+ MediaClipLayer,
4170
+ {
4171
+ schedule: mediaSchedule,
4172
+ currentTime,
4173
+ isPlaying,
4174
+ basePath,
4175
+ renderMode
4176
+ }
4177
+ ),
4178
+ /* @__PURE__ */ jsxs13("div", { className: "doc-player__viewport", children: [
4179
+ showCoverBlock && coverBlock && /* @__PURE__ */ jsx20("div", { className: "doc-player__block doc-player__block--cover", children: /* @__PURE__ */ jsx20(
3456
4180
  BlockRenderer,
3457
4181
  {
3458
4182
  block: coverBlock,
@@ -3462,31 +4186,44 @@ function DocPlayer({
3462
4186
  viewport: activeViewport
3463
4187
  }
3464
4188
  ) }),
3465
- !showCoverBlock && previousBlock && isExiting && /* @__PURE__ */ jsx17("div", { className: "doc-player__block doc-player__block--previous", children: /* @__PURE__ */ jsx17(
4189
+ !showCoverBlock && previousBlock && isExiting && // Keyed by block id so each block is its own DOM subtree: React never
4190
+ // reconciles one block's layers onto another's (templates reuse layer
4191
+ // ids like `title`/`background`), which would otherwise reuse stale
4192
+ // DOM / skip entrance animations mid-transition.
4193
+ /* @__PURE__ */ jsx20("div", { className: "doc-player__block doc-player__block--previous", children: /* @__PURE__ */ jsx20(
3466
4194
  BlockRenderer,
3467
4195
  {
3468
4196
  block: previousBlock,
3469
4197
  blockTime,
3470
4198
  basePath,
3471
4199
  isExiting: true,
4200
+ transition: currentBlock?.transition,
3472
4201
  viewport: activeViewport
3473
4202
  }
3474
- ) }),
3475
- !showCoverBlock && currentBlock && /* @__PURE__ */ jsx17("div", { className: "doc-player__block doc-player__block--active", children: /* @__PURE__ */ jsx17(
3476
- BlockRenderer,
4203
+ ) }, previousBlock.id),
4204
+ !showCoverBlock && currentBlock && /* @__PURE__ */ jsx20(
4205
+ "div",
3477
4206
  {
3478
- block: currentBlock,
3479
- blockTime,
3480
- basePath,
3481
- isEntering,
3482
- viewport: activeViewport,
3483
- isPlaying
3484
- }
3485
- ) }),
3486
- hasCaptions && (renderMode ? captionsEnabled : true) && /* @__PURE__ */ jsx17(
4207
+ className: `doc-player__block doc-player__block--active${swipe.phase !== "idle" ? ` doc-player__block--${swipe.phase}` : ""}`,
4208
+ style: swipe.phase !== "idle" ? { transform: `translateX(${swipe.offsetPx}px)` } : void 0,
4209
+ children: /* @__PURE__ */ jsx20(
4210
+ BlockRenderer,
4211
+ {
4212
+ block: currentBlock,
4213
+ blockTime,
4214
+ basePath,
4215
+ isEntering,
4216
+ viewport: activeViewport,
4217
+ isPlaying
4218
+ }
4219
+ )
4220
+ },
4221
+ currentBlock.id
4222
+ ),
4223
+ hasCaptions && (renderMode ? captionsEnabled : true) && /* @__PURE__ */ jsx20(
3487
4224
  CaptionOverlay,
3488
4225
  {
3489
- captions: script.captions,
4226
+ captions: doc.captions,
3490
4227
  currentTime,
3491
4228
  enabled: captionsEnabled && (renderMode || isPlaying || currentTime > 0),
3492
4229
  fontSize: 16,
@@ -3495,7 +4232,7 @@ function DocPlayer({
3495
4232
  viewport: activeViewport
3496
4233
  }
3497
4234
  ),
3498
- isDebugMode && /* @__PURE__ */ jsxs11(
4235
+ isDebugMode && /* @__PURE__ */ jsxs13(
3499
4236
  "div",
3500
4237
  {
3501
4238
  className: "doc-player__debug",
@@ -3516,27 +4253,27 @@ function DocPlayer({
3516
4253
  textAlign: "left"
3517
4254
  },
3518
4255
  children: [
3519
- /* @__PURE__ */ jsx17("div", { style: { color: "#ffcc00", fontWeight: "bold", marginBottom: "4px" }, children: "DEBUG MODE" }),
3520
- /* @__PURE__ */ jsxs11("div", { children: [
3521
- /* @__PURE__ */ jsx17("span", { style: { color: "#888" }, children: "template:" }),
4256
+ /* @__PURE__ */ jsx20("div", { style: { color: "#ffcc00", fontWeight: "bold", marginBottom: "4px" }, children: "DEBUG MODE" }),
4257
+ /* @__PURE__ */ jsxs13("div", { children: [
4258
+ /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "template:" }),
3522
4259
  " ",
3523
- /* @__PURE__ */ jsx17("span", { style: { color: "#ff6b6b" }, children: currentBlock?.template ?? "raw" })
4260
+ /* @__PURE__ */ jsx20("span", { style: { color: "#ff6b6b" }, children: currentBlock?.template ?? "raw" })
3524
4261
  ] }),
3525
- /* @__PURE__ */ jsxs11("div", { children: [
3526
- /* @__PURE__ */ jsx17("span", { style: { color: "#888" }, children: "block:" }),
4262
+ /* @__PURE__ */ jsxs13("div", { children: [
4263
+ /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "block:" }),
3527
4264
  " ",
3528
4265
  currentBlockIndex + 1,
3529
4266
  "/",
3530
4267
  expandedBlocks.length,
3531
4268
  " ",
3532
- /* @__PURE__ */ jsxs11("span", { style: { color: "#666" }, children: [
4269
+ /* @__PURE__ */ jsxs13("span", { style: { color: "#666" }, children: [
3533
4270
  "(",
3534
4271
  currentBlock?.id || "none",
3535
4272
  ")"
3536
4273
  ] })
3537
4274
  ] }),
3538
- /* @__PURE__ */ jsxs11("div", { children: [
3539
- /* @__PURE__ */ jsx17("span", { style: { color: "#888" }, children: "time:" }),
4275
+ /* @__PURE__ */ jsxs13("div", { children: [
4276
+ /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "time:" }),
3540
4277
  " ",
3541
4278
  currentTime.toFixed(2),
3542
4279
  "s /",
@@ -3544,17 +4281,16 @@ function DocPlayer({
3544
4281
  totalDuration.toFixed(1),
3545
4282
  "s",
3546
4283
  " ",
3547
- /* @__PURE__ */ jsxs11("span", { style: { color: "#666" }, children: [
4284
+ /* @__PURE__ */ jsxs13("span", { style: { color: "#666" }, children: [
3548
4285
  "(progress: ",
3549
4286
  (docProgress * 100).toFixed(1),
3550
- "%, scriptDur:",
3551
- " ",
3552
- script.duration.toFixed(1),
4287
+ "%, scriptDur: ",
4288
+ doc.duration.toFixed(1),
3553
4289
  ")"
3554
4290
  ] })
3555
4291
  ] }),
3556
- /* @__PURE__ */ jsxs11("div", { children: [
3557
- /* @__PURE__ */ jsx17("span", { style: { color: "#888" }, children: "blockTime:" }),
4292
+ /* @__PURE__ */ jsxs13("div", { children: [
4293
+ /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "blockTime:" }),
3558
4294
  " ",
3559
4295
  blockTime.toFixed(2),
3560
4296
  "s /",
@@ -3562,58 +4298,58 @@ function DocPlayer({
3562
4298
  (currentBlock?.duration || 0).toFixed(1),
3563
4299
  "s"
3564
4300
  ] }),
3565
- /* @__PURE__ */ jsxs11("div", { children: [
3566
- /* @__PURE__ */ jsx17("span", { style: { color: "#888" }, children: "segment:" }),
4301
+ /* @__PURE__ */ jsxs13("div", { children: [
4302
+ /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "segment:" }),
3567
4303
  " ",
3568
4304
  currentSegment,
3569
4305
  "/",
3570
- script.audio.segments.length - 1,
4306
+ doc.audio.segments.length - 1,
3571
4307
  " ",
3572
- /* @__PURE__ */ jsxs11("span", { style: { color: "#666" }, children: [
4308
+ /* @__PURE__ */ jsxs13("span", { style: { color: "#666" }, children: [
3573
4309
  "(",
3574
- script.audio.segments[currentSegment]?.name || "none",
4310
+ doc.audio.segments[currentSegment]?.name || "none",
3575
4311
  ")"
3576
4312
  ] })
3577
4313
  ] }),
3578
- /* @__PURE__ */ jsxs11("div", { children: [
3579
- /* @__PURE__ */ jsx17("span", { style: { color: "#888" }, children: "viewport:" }),
4314
+ /* @__PURE__ */ jsxs13("div", { children: [
4315
+ /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "viewport:" }),
3580
4316
  " ",
3581
4317
  activeViewport.name || `${activeViewport.width}x${activeViewport.height}`,
3582
4318
  " ",
3583
- /* @__PURE__ */ jsxs11("span", { style: { color: "#666" }, children: [
4319
+ /* @__PURE__ */ jsxs13("span", { style: { color: "#666" }, children: [
3584
4320
  "(",
3585
4321
  orientation,
3586
4322
  ")"
3587
4323
  ] })
3588
4324
  ] }),
3589
- /* @__PURE__ */ jsxs11("div", { children: [
3590
- /* @__PURE__ */ jsx17("span", { style: { color: "#888" }, children: "playing:" }),
4325
+ /* @__PURE__ */ jsxs13("div", { children: [
4326
+ /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "playing:" }),
3591
4327
  " ",
3592
- /* @__PURE__ */ jsx17("span", { style: { color: isPlaying ? "#4ade80" : "#f87171" }, children: isPlaying ? "yes" : "no" }),
3593
- showCoverBlock && /* @__PURE__ */ jsx17("span", { style: { color: "#60a5fa" }, children: " (cover)" })
4328
+ /* @__PURE__ */ jsx20("span", { style: { color: isPlaying ? "#4ade80" : "#f87171" }, children: isPlaying ? "yes" : "no" }),
4329
+ showCoverBlock && /* @__PURE__ */ jsx20("span", { style: { color: "#60a5fa" }, children: " (cover)" })
3594
4330
  ] }),
3595
4331
  hasCaptions && (() => {
3596
- const debugPhrase = getCaptionAtTime2(script.captions, currentTime);
4332
+ const debugPhrase = getCaptionAtTime2(doc.captions, currentTime);
3597
4333
  const debugEnabled = captionsEnabled && (isPlaying || currentTime > 0);
3598
- return /* @__PURE__ */ jsxs11(Fragment2, { children: [
3599
- /* @__PURE__ */ jsxs11("div", { children: [
3600
- /* @__PURE__ */ jsx17("span", { style: { color: "#888" }, children: "captions:" }),
4334
+ return /* @__PURE__ */ jsxs13(Fragment3, { children: [
4335
+ /* @__PURE__ */ jsxs13("div", { children: [
4336
+ /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "captions:" }),
3601
4337
  " ",
3602
- script.captions?.phrases.length || 0,
4338
+ doc.captions?.phrases.length || 0,
3603
4339
  " phrases",
3604
4340
  " ",
3605
- /* @__PURE__ */ jsxs11("span", { style: { color: captionsEnabled ? "#4ade80" : "#666" }, children: [
4341
+ /* @__PURE__ */ jsxs13("span", { style: { color: captionsEnabled ? "#4ade80" : "#666" }, children: [
3606
4342
  "(",
3607
4343
  captionsEnabled ? "on" : "off",
3608
4344
  ")"
3609
4345
  ] })
3610
4346
  ] }),
3611
- /* @__PURE__ */ jsxs11("div", { children: [
3612
- /* @__PURE__ */ jsx17("span", { style: { color: "#888" }, children: "cc.enabled:" }),
4347
+ /* @__PURE__ */ jsxs13("div", { children: [
4348
+ /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "cc.enabled:" }),
3613
4349
  " ",
3614
- /* @__PURE__ */ jsx17("span", { style: { color: debugEnabled ? "#4ade80" : "#f87171" }, children: String(debugEnabled) }),
4350
+ /* @__PURE__ */ jsx20("span", { style: { color: debugEnabled ? "#4ade80" : "#f87171" }, children: String(debugEnabled) }),
3615
4351
  " ",
3616
- /* @__PURE__ */ jsxs11("span", { style: { color: "#666" }, children: [
4352
+ /* @__PURE__ */ jsxs13("span", { style: { color: "#666" }, children: [
3617
4353
  "(playing=",
3618
4354
  String(isPlaying),
3619
4355
  " t>0=",
@@ -3621,15 +4357,15 @@ function DocPlayer({
3621
4357
  ")"
3622
4358
  ] })
3623
4359
  ] }),
3624
- /* @__PURE__ */ jsxs11("div", { children: [
3625
- /* @__PURE__ */ jsx17("span", { style: { color: "#888" }, children: "cc.phrase:" }),
4360
+ /* @__PURE__ */ jsxs13("div", { children: [
4361
+ /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "cc.phrase:" }),
3626
4362
  " ",
3627
- /* @__PURE__ */ jsx17("span", { style: { color: debugPhrase ? "#4ade80" : "#f87171" }, children: debugPhrase ? `"${debugPhrase.text.slice(0, 30)}..."` : "null" })
4363
+ /* @__PURE__ */ jsx20("span", { style: { color: debugPhrase ? "#4ade80" : "#f87171" }, children: debugPhrase ? `"${debugPhrase.text.slice(0, 30)}..."` : "null" })
3628
4364
  ] }),
3629
- debugPhrase && /* @__PURE__ */ jsxs11("div", { children: [
3630
- /* @__PURE__ */ jsx17("span", { style: { color: "#888" }, children: "cc.range:" }),
4365
+ debugPhrase && /* @__PURE__ */ jsxs13("div", { children: [
4366
+ /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "cc.range:" }),
3631
4367
  " ",
3632
- /* @__PURE__ */ jsxs11("span", { style: { color: "#60a5fa" }, children: [
4368
+ /* @__PURE__ */ jsxs13("span", { style: { color: "#60a5fa" }, children: [
3633
4369
  debugPhrase.startTime.toFixed(2),
3634
4370
  "-",
3635
4371
  debugPhrase.endTime.toFixed(2)
@@ -3641,7 +4377,7 @@ function DocPlayer({
3641
4377
  }
3642
4378
  )
3643
4379
  ] }),
3644
- !isAvailable && unavailableMessage && /* @__PURE__ */ jsxs11(
4380
+ !isAvailable && unavailableMessage && /* @__PURE__ */ jsxs13(
3645
4381
  "div",
3646
4382
  {
3647
4383
  className: "doc-player__unavailable",
@@ -3662,12 +4398,12 @@ function DocPlayer({
3662
4398
  zIndex: 50
3663
4399
  },
3664
4400
  children: [
3665
- /* @__PURE__ */ jsx17("span", { style: { fontSize: "32px" }, children: "\u{1F50A}" }),
3666
- /* @__PURE__ */ jsx17("span", { children: unavailableMessage })
4401
+ /* @__PURE__ */ jsx20("span", { style: { fontSize: "32px" }, children: "\u{1F50A}" }),
4402
+ /* @__PURE__ */ jsx20("span", { children: unavailableMessage })
3667
4403
  ]
3668
4404
  }
3669
4405
  ),
3670
- !renderMode && !isSlideshowMode && showControls && /* @__PURE__ */ jsx17(
4406
+ !renderMode && !isSlideshowMode && showControls && /* @__PURE__ */ jsx20(
3671
4407
  DocControlsOverlay,
3672
4408
  {
3673
4409
  state: playbackState,
@@ -3677,7 +4413,7 @@ function DocPlayer({
3677
4413
  getBlockTitle
3678
4414
  }
3679
4415
  ),
3680
- !renderMode && !isSlideshowMode && !showControls && showScrubber && /* @__PURE__ */ jsx17(
4416
+ !renderMode && !isSlideshowMode && !showControls && showScrubber && /* @__PURE__ */ jsx20(
3681
4417
  "div",
3682
4418
  {
3683
4419
  className: "doc-player__scrubber",
@@ -3692,7 +4428,7 @@ function DocPlayer({
3692
4428
  alignItems: "center",
3693
4429
  zIndex: 100
3694
4430
  },
3695
- children: /* @__PURE__ */ jsx17(
4431
+ children: /* @__PURE__ */ jsx20(
3696
4432
  DocProgressBar,
3697
4433
  {
3698
4434
  state: playbackState,
@@ -3704,15 +4440,15 @@ function DocPlayer({
3704
4440
  )
3705
4441
  }
3706
4442
  ),
3707
- !renderMode && isSlideshowMode && /* @__PURE__ */ jsx17(DocControlsSlideshow, { state: playbackState, slideNav: slideNavActions }),
3708
- !isSlideshowMode && tapFeedback && /* @__PURE__ */ jsx17("div", { className: "doc-player__tap-feedback", children: /* @__PURE__ */ jsx17("svg", { viewBox: "0 0 24 24", fill: "white", width: "48", height: "48", children: tapFeedback === "pause" ? /* @__PURE__ */ jsx17("path", { d: "M6 19h4V5H6v14zm8-14v14h4V5h-4z" }) : /* @__PURE__ */ jsx17("path", { d: "M8 5v14l11-7z" }) }) }, Date.now())
4443
+ !renderMode && isSlideshowMode && /* @__PURE__ */ jsx20(DocControlsSlideshow, { state: playbackState, slideNav: slideNavActions }),
4444
+ !isSlideshowMode && tapFeedback && /* @__PURE__ */ jsx20("div", { className: "doc-player__tap-feedback", children: /* @__PURE__ */ jsx20("svg", { viewBox: "0 0 24 24", fill: "white", width: "48", height: "48", children: tapFeedback === "pause" ? /* @__PURE__ */ jsx20("path", { d: "M6 19h4V5H6v14zm8-14v14h4V5h-4z" }) : /* @__PURE__ */ jsx20("path", { d: "M8 5v14l11-7z" }) }) }, Date.now())
3709
4445
  ]
3710
4446
  }
3711
4447
  );
3712
4448
  }
3713
4449
 
3714
4450
  // src/DocControlsBottom.tsx
3715
- import { jsx as jsx18, jsxs as jsxs12 } from "react/jsx-runtime";
4451
+ import { jsx as jsx21, jsxs as jsxs14 } from "react/jsx-runtime";
3716
4452
  function DocControlsBottom({
3717
4453
  state,
3718
4454
  actions,
@@ -3720,32 +4456,32 @@ function DocControlsBottom({
3720
4456
  expandedBlocks,
3721
4457
  getBlockTitle
3722
4458
  }) {
3723
- return /* @__PURE__ */ jsxs12("div", { className: "doc-controls-bottom", children: [
3724
- /* @__PURE__ */ jsx18(
4459
+ return /* @__PURE__ */ jsxs14("div", { className: "doc-controls-bottom", children: [
4460
+ /* @__PURE__ */ jsx21(
3725
4461
  "button",
3726
4462
  {
3727
4463
  className: "bottom-ctrl-btn",
3728
4464
  onClick: actions.restart,
3729
4465
  title: "Restart",
3730
4466
  "aria-label": "Restart from beginning",
3731
- children: /* @__PURE__ */ jsx18("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx18("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" }) })
4467
+ children: /* @__PURE__ */ jsx21("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx21("path", { d: "M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z" }) })
3732
4468
  }
3733
4469
  ),
3734
- /* @__PURE__ */ jsx18(
4470
+ /* @__PURE__ */ jsx21(
3735
4471
  "button",
3736
4472
  {
3737
4473
  className: "bottom-ctrl-btn bottom-play-btn",
3738
4474
  onClick: actions.toggle,
3739
4475
  "aria-label": state.isPlaying ? "Pause" : "Play",
3740
- children: state.isPlaying ? /* @__PURE__ */ jsx18("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx18("path", { d: "M6 19h4V5H6v14zm8-14v14h4V5h-4z" }) }) : /* @__PURE__ */ jsx18("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx18("path", { d: "M8 5v14l11-7z" }) })
4476
+ children: state.isPlaying ? /* @__PURE__ */ jsx21("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx21("path", { d: "M6 19h4V5H6v14zm8-14v14h4V5h-4z" }) }) : /* @__PURE__ */ jsx21("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx21("path", { d: "M8 5v14l11-7z" }) })
3741
4477
  }
3742
4478
  ),
3743
- /* @__PURE__ */ jsxs12("span", { className: "bottom-time", children: [
4479
+ /* @__PURE__ */ jsxs14("span", { className: "bottom-time", children: [
3744
4480
  formatTime(state.currentTime),
3745
4481
  " / ",
3746
4482
  formatTime(state.totalDuration)
3747
4483
  ] }),
3748
- /* @__PURE__ */ jsx18(
4484
+ /* @__PURE__ */ jsx21(
3749
4485
  DocProgressBar,
3750
4486
  {
3751
4487
  state,
@@ -3755,82 +4491,82 @@ function DocControlsBottom({
3755
4491
  getBlockTitle
3756
4492
  }
3757
4493
  ),
3758
- /* @__PURE__ */ jsxs12("span", { className: "bottom-segment", children: [
4494
+ /* @__PURE__ */ jsxs14("span", { className: "bottom-segment", children: [
3759
4495
  state.currentBlockIndex + 1,
3760
4496
  "/",
3761
4497
  state.totalBlocks
3762
4498
  ] }),
3763
- state.hasCaptions && /* @__PURE__ */ jsx18(
4499
+ state.hasCaptions && /* @__PURE__ */ jsx21(
3764
4500
  "button",
3765
4501
  {
3766
4502
  className: `bottom-ctrl-btn ${state.captionMode !== "off" ? "bottom-ctrl-btn--active" : ""}`,
3767
4503
  onClick: () => actions.cycleCaptionMode(),
3768
4504
  title: state.captionMode === "off" ? "Captions: Off" : state.captionMode === "standard" ? "Captions: Standard" : "Captions: Social",
3769
4505
  "aria-label": "Cycle caption style",
3770
- children: /* @__PURE__ */ jsx18("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx18("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" }) })
4506
+ children: /* @__PURE__ */ jsx21("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "18", height: "18", children: /* @__PURE__ */ jsx21("path", { d: "M19 4H5a2 2 0 00-2 2v12a2 2 0 002 2h14a2 2 0 002-2V6a2 2 0 00-2-2zm-8 7H9.5v-.5h-2v3h2V13H11v1a1 1 0 01-1 1H7a1 1 0 01-1-1v-4a1 1 0 011-1h3a1 1 0 011 1v1zm7 0h-1.5v-.5h-2v3h2V13H18v1a1 1 0 01-1 1h-3a1 1 0 01-1-1v-4a1 1 0 011-1h3a1 1 0 011 1v1z" }) })
3771
4507
  }
3772
4508
  )
3773
4509
  ] });
3774
4510
  }
3775
4511
 
3776
4512
  // src/DocControlsSidebar.tsx
3777
- import { jsx as jsx19, jsxs as jsxs13 } from "react/jsx-runtime";
4513
+ import { jsx as jsx22, jsxs as jsxs15 } from "react/jsx-runtime";
3778
4514
  function DocControlsSidebar({ state, actions }) {
3779
- return /* @__PURE__ */ jsxs13("div", { className: "doc-controls-sidebar", children: [
3780
- /* @__PURE__ */ jsx19(
4515
+ return /* @__PURE__ */ jsxs15("div", { className: "doc-controls-sidebar", children: [
4516
+ /* @__PURE__ */ jsx22(
3781
4517
  "button",
3782
4518
  {
3783
4519
  className: "sidebar-ctrl-btn",
3784
4520
  onClick: actions.restart,
3785
4521
  title: "Restart",
3786
4522
  "aria-label": "Restart from beginning",
3787
- children: /* @__PURE__ */ jsx19("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx19("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" }) })
4523
+ children: /* @__PURE__ */ jsx22("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx22("path", { d: "M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z" }) })
3788
4524
  }
3789
4525
  ),
3790
- /* @__PURE__ */ jsx19(
4526
+ /* @__PURE__ */ jsx22(
3791
4527
  "button",
3792
4528
  {
3793
4529
  className: "sidebar-ctrl-btn sidebar-play-btn",
3794
4530
  onClick: actions.toggle,
3795
4531
  "aria-label": state.isPlaying ? "Pause" : "Play",
3796
- children: state.isPlaying ? /* @__PURE__ */ jsx19("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "22", height: "22", children: /* @__PURE__ */ jsx19("path", { d: "M6 19h4V5H6v14zm8-14v14h4V5h-4z" }) }) : /* @__PURE__ */ jsx19("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "22", height: "22", children: /* @__PURE__ */ jsx19("path", { d: "M8 5v14l11-7z" }) })
4532
+ children: state.isPlaying ? /* @__PURE__ */ jsx22("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "22", height: "22", children: /* @__PURE__ */ jsx22("path", { d: "M6 19h4V5H6v14zm8-14v14h4V5h-4z" }) }) : /* @__PURE__ */ jsx22("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "22", height: "22", children: /* @__PURE__ */ jsx22("path", { d: "M8 5v14l11-7z" }) })
3797
4533
  }
3798
4534
  ),
3799
- /* @__PURE__ */ jsxs13("div", { className: "sidebar-time", children: [
3800
- /* @__PURE__ */ jsx19("div", { children: formatTime(state.currentTime) }),
3801
- /* @__PURE__ */ jsx19("div", { className: "sidebar-time-total", children: formatTime(state.totalDuration) })
4535
+ /* @__PURE__ */ jsxs15("div", { className: "sidebar-time", children: [
4536
+ /* @__PURE__ */ jsx22("div", { children: formatTime(state.currentTime) }),
4537
+ /* @__PURE__ */ jsx22("div", { className: "sidebar-time-total", children: formatTime(state.totalDuration) })
3802
4538
  ] }),
3803
- /* @__PURE__ */ jsxs13("div", { className: "sidebar-segment", children: [
4539
+ /* @__PURE__ */ jsxs15("div", { className: "sidebar-segment", children: [
3804
4540
  state.currentBlockIndex + 1,
3805
4541
  "/",
3806
4542
  state.totalBlocks
3807
4543
  ] }),
3808
- state.hasCaptions && /* @__PURE__ */ jsx19(
4544
+ state.hasCaptions && /* @__PURE__ */ jsx22(
3809
4545
  "button",
3810
4546
  {
3811
4547
  className: `sidebar-ctrl-btn ${state.captionMode !== "off" ? "sidebar-ctrl-btn--active" : ""}`,
3812
4548
  onClick: () => actions.cycleCaptionMode(),
3813
4549
  title: state.captionMode === "off" ? "Captions: Off" : state.captionMode === "standard" ? "Captions: Standard" : "Captions: Social",
3814
4550
  "aria-label": "Cycle caption style",
3815
- children: /* @__PURE__ */ jsx19("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx19("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" }) })
4551
+ children: /* @__PURE__ */ jsx22("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx22("path", { d: "M19 4H5a2 2 0 00-2 2v12a2 2 0 002 2h14a2 2 0 002-2V6a2 2 0 00-2-2zm-8 7H9.5v-.5h-2v3h2V13H11v1a1 1 0 01-1 1H7a1 1 0 01-1-1v-4a1 1 0 011-1h3a1 1 0 011 1v1zm7 0h-1.5v-.5h-2v3h2V13H18v1a1 1 0 01-1 1h-3a1 1 0 01-1-1v-4a1 1 0 011-1h3a1 1 0 011 1v1z" }) })
3816
4552
  }
3817
4553
  ),
3818
- actions.toggleFullscreen && /* @__PURE__ */ jsx19(
4554
+ actions.toggleFullscreen && /* @__PURE__ */ jsx22(
3819
4555
  "button",
3820
4556
  {
3821
4557
  className: `sidebar-ctrl-btn ${state.isFullscreen ? "sidebar-ctrl-btn--active" : ""}`,
3822
4558
  onClick: actions.toggleFullscreen,
3823
4559
  title: state.isFullscreen ? "Exit fullscreen (F)" : "Fullscreen (F)",
3824
4560
  "aria-label": state.isFullscreen ? "Exit fullscreen" : "Enter fullscreen",
3825
- children: state.isFullscreen ? /* @__PURE__ */ jsx19("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx19("path", { d: "M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z" }) }) : /* @__PURE__ */ jsx19("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx19("path", { d: "M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" }) })
4561
+ children: state.isFullscreen ? /* @__PURE__ */ jsx22("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx22("path", { d: "M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z" }) }) : /* @__PURE__ */ jsx22("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "20", height: "20", children: /* @__PURE__ */ jsx22("path", { d: "M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" }) })
3826
4562
  }
3827
4563
  )
3828
4564
  ] });
3829
4565
  }
3830
4566
 
3831
4567
  // src/DocPlayerWithSidebar.tsx
3832
- import { useRef as useRef6, useState as useState8, useCallback as useCallback6, useEffect as useEffect8 } from "react";
3833
- import { jsx as jsx20, jsxs as jsxs14 } from "react/jsx-runtime";
4568
+ import { useRef as useRef8, useState as useState8, useCallback as useCallback7, useEffect as useEffect9 } from "react";
4569
+ import { jsx as jsx23, jsxs as jsxs16 } from "react/jsx-runtime";
3834
4570
  var DEFAULT_STATE = {
3835
4571
  isPlaying: false,
3836
4572
  currentTime: 0,
@@ -3846,24 +4582,25 @@ var DEFAULT_STATE = {
3846
4582
  currentBlock: null
3847
4583
  };
3848
4584
  function DocPlayerWithSidebar({
3849
- script,
4585
+ doc,
3850
4586
  basePath,
3851
4587
  autoPlay = false,
3852
4588
  onEnded,
3853
4589
  onTimeUpdate,
3854
- audioProvider,
4590
+ audioController,
3855
4591
  muted,
3856
4592
  captionsEnabled,
3857
4593
  isFullscreen,
3858
4594
  onFullscreenToggle,
3859
4595
  forceViewport,
3860
- onPlayingChange
4596
+ onPlayingChange,
4597
+ theme
3861
4598
  }) {
3862
- const stateRef = useRef6(DEFAULT_STATE);
3863
- const actionsRef = useRef6(null);
3864
- const wasPlayingRef = useRef6(false);
4599
+ const stateRef = useRef8(DEFAULT_STATE);
4600
+ const actionsRef = useRef8(null);
4601
+ const wasPlayingRef = useRef8(false);
3865
4602
  const [, setTick] = useState8(0);
3866
- const handleStateChange = useCallback6(
4603
+ const handleStateChange = useCallback7(
3867
4604
  (state) => {
3868
4605
  stateRef.current = state;
3869
4606
  if (onPlayingChange && state.isPlaying !== wasPlayingRef.current) {
@@ -3873,7 +4610,7 @@ function DocPlayerWithSidebar({
3873
4610
  },
3874
4611
  [onPlayingChange]
3875
4612
  );
3876
- const handleControlsReady = useCallback6(
4613
+ const handleControlsReady = useCallback7(
3877
4614
  (controls) => {
3878
4615
  const isFirst = !actionsRef.current;
3879
4616
  actionsRef.current = controls;
@@ -3881,22 +4618,23 @@ function DocPlayerWithSidebar({
3881
4618
  },
3882
4619
  []
3883
4620
  );
3884
- useEffect8(() => {
4621
+ useEffect9(() => {
3885
4622
  const interval = setInterval(() => {
3886
4623
  setTick((t) => t + 1);
3887
4624
  }, 250);
3888
4625
  return () => clearInterval(interval);
3889
4626
  }, []);
3890
- return /* @__PURE__ */ jsxs14("div", { className: "doc-player-sidebar-layout", children: [
3891
- /* @__PURE__ */ jsx20("div", { className: "doc-player-sidebar-layout__video", children: /* @__PURE__ */ jsx20(
4627
+ return /* @__PURE__ */ jsxs16("div", { className: "doc-player-sidebar-layout", children: [
4628
+ /* @__PURE__ */ jsx23("div", { className: "doc-player-sidebar-layout__video", children: /* @__PURE__ */ jsx23(
3892
4629
  DocPlayer,
3893
4630
  {
3894
- script,
4631
+ doc,
4632
+ theme,
3895
4633
  basePath,
3896
4634
  autoPlay,
3897
4635
  onEnded,
3898
4636
  onTimeUpdate,
3899
- audioProvider,
4637
+ audioController,
3900
4638
  muted,
3901
4639
  captionsEnabled,
3902
4640
  showControls: isFullscreen,
@@ -3908,42 +4646,21 @@ function DocPlayerWithSidebar({
3908
4646
  forceViewport
3909
4647
  }
3910
4648
  ) }),
3911
- actionsRef.current && /* @__PURE__ */ jsx20(DocControlsSidebar, { state: stateRef.current, actions: actionsRef.current })
4649
+ actionsRef.current && /* @__PURE__ */ jsx23(DocControlsSidebar, { state: stateRef.current, actions: actionsRef.current })
3912
4650
  ] });
3913
4651
  }
3914
4652
 
3915
4653
  // src/jsonView/useJsonViewTokens.ts
3916
- import { useMemo as useMemo8 } from "react";
3917
- import {
3918
- applySurface as applySurface3,
3919
- resolveFontFamily as resolveFontFamily3
3920
- } from "@bendyline/squisq/schemas";
3921
- import { DEFAULT_THEME as DEFAULT_THEME3 } from "@bendyline/squisq/doc";
4654
+ import { useMemo as useMemo10 } from "react";
4655
+ import { buildJsonFormTokens, resolveJsonFormTheme } from "@bendyline/squisq/jsonForm";
3922
4656
  function useJsonViewTokens(theme, surface) {
3923
4657
  const auto = useAutoSurface(surface === "auto");
3924
4658
  const effectiveSurface = surface === "auto" ? auto : surface ?? void 0;
3925
- return useMemo8(() => {
3926
- const baseTheme = theme ?? DEFAULT_THEME3;
3927
- const finalTheme = effectiveSurface ? applySurface3(baseTheme, effectiveSurface) : baseTheme;
3928
- const titleFont = resolveFontFamily3(finalTheme.typography.titleFont, "system-ui, sans-serif");
3929
- const bodyFont = resolveFontFamily3(finalTheme.typography.bodyFont, "system-ui, sans-serif");
3930
- const monoFont = resolveFontFamily3(
3931
- finalTheme.typography.monoFont,
3932
- "ui-monospace, Consolas, monospace"
3933
- );
3934
- const style = {
3935
- ["--squisq-json-bg"]: finalTheme.colors.background,
3936
- ["--squisq-json-text"]: finalTheme.colors.text,
3937
- ["--squisq-json-muted"]: finalTheme.colors.textMuted,
3938
- ["--squisq-json-primary"]: finalTheme.colors.primary,
3939
- ["--squisq-json-accent"]: finalTheme.colors.secondary,
3940
- ["--squisq-json-border"]: `color-mix(in srgb, ${finalTheme.colors.textMuted} 35%, transparent)`,
3941
- ["--squisq-json-title-font"]: titleFont,
3942
- ["--squisq-json-body-font"]: bodyFont,
3943
- ["--squisq-json-mono-font"]: monoFont,
3944
- ["--squisq-json-radius"]: `${finalTheme.style.borderRadius ?? 8}px`
3945
- };
3946
- return { style, theme: finalTheme };
4659
+ return useMemo10(() => {
4660
+ const style = buildJsonFormTokens(theme, effectiveSurface, {
4661
+ prefix: "--squisq-json"
4662
+ });
4663
+ return { style, theme: resolveJsonFormTheme(theme, effectiveSurface) };
3947
4664
  }, [theme, effectiveSurface]);
3948
4665
  }
3949
4666
 
@@ -3955,67 +4672,67 @@ import {
3955
4672
  } from "@bendyline/squisq/jsonForm";
3956
4673
 
3957
4674
  // src/jsonView/viewers.tsx
3958
- import { Fragment as Fragment3, useMemo as useMemo9 } from "react";
4675
+ import { Fragment as Fragment4, useMemo as useMemo11 } from "react";
3959
4676
  import {
3960
4677
  arrayItemKind
3961
4678
  } from "@bendyline/squisq/jsonForm";
3962
- import { parseMarkdown } from "@bendyline/squisq/markdown";
3963
- import { Fragment as Fragment4, jsx as jsx21, jsxs as jsxs15 } from "react/jsx-runtime";
4679
+ import { parseMarkdown as parseMarkdown3 } from "@bendyline/squisq/markdown";
4680
+ import { Fragment as Fragment5, jsx as jsx24, jsxs as jsxs17 } from "react/jsx-runtime";
3964
4681
  function TextViewer({ value }) {
3965
4682
  if (value === void 0 || value === null || value === "") {
3966
- return /* @__PURE__ */ jsx21("span", { className: "squisq-jv-empty", children: "\u2014" });
4683
+ return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-empty", children: "\u2014" });
3967
4684
  }
3968
- return /* @__PURE__ */ jsx21("span", { className: "squisq-jv-value", children: String(value) });
4685
+ return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-value", children: String(value) });
3969
4686
  }
3970
4687
  function MultilineViewer({ value }) {
3971
4688
  if (value === void 0 || value === null || value === "") {
3972
- return /* @__PURE__ */ jsx21("span", { className: "squisq-jv-empty", children: "\u2014" });
4689
+ return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-empty", children: "\u2014" });
3973
4690
  }
3974
- return /* @__PURE__ */ jsx21("span", { className: "squisq-jv-value squisq-jv-value--multiline", children: String(value) });
4691
+ return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-value squisq-jv-value--multiline", children: String(value) });
3975
4692
  }
3976
4693
  function RichTextViewer({ value }) {
3977
- const nodes = useMemo9(() => {
4694
+ const nodes = useMemo11(() => {
3978
4695
  if (typeof value !== "string" || value === "") return null;
3979
4696
  try {
3980
- const doc = parseMarkdown(value);
4697
+ const doc = parseMarkdown3(value);
3981
4698
  return doc.children;
3982
4699
  } catch {
3983
4700
  return null;
3984
4701
  }
3985
4702
  }, [value]);
3986
- if (!nodes) return /* @__PURE__ */ jsx21("span", { className: "squisq-jv-empty", children: "\u2014" });
3987
- return /* @__PURE__ */ jsx21("div", { className: "squisq-jv-richtext", children: /* @__PURE__ */ jsx21(MarkdownRenderer, { nodes }) });
4703
+ if (!nodes) return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-empty", children: "\u2014" });
4704
+ return /* @__PURE__ */ jsx24("div", { className: "squisq-jv-richtext", children: /* @__PURE__ */ jsx24(MarkdownRenderer, { nodes }) });
3988
4705
  }
3989
4706
  function NumberViewer({ value }) {
3990
4707
  if (value === void 0 || value === null) {
3991
- return /* @__PURE__ */ jsx21("span", { className: "squisq-jv-empty", children: "\u2014" });
4708
+ return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-empty", children: "\u2014" });
3992
4709
  }
3993
- return /* @__PURE__ */ jsx21("span", { className: "squisq-jv-value", children: String(value) });
4710
+ return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-value", children: String(value) });
3994
4711
  }
3995
4712
  function BooleanViewer({ value }) {
3996
4713
  const on = Boolean(value);
3997
- return /* @__PURE__ */ jsx21("span", { className: `squisq-jv-toggle squisq-jv-toggle--${on ? "on" : "off"}`, children: on ? "On" : "Off" });
4714
+ return /* @__PURE__ */ jsx24("span", { className: `squisq-jv-toggle squisq-jv-toggle--${on ? "on" : "off"}`, children: on ? "On" : "Off" });
3998
4715
  }
3999
4716
  function EnumViewer({ value, schema }) {
4000
4717
  if (value === void 0 || value === null || value === "") {
4001
- return /* @__PURE__ */ jsx21("span", { className: "squisq-jv-empty", children: "\u2014" });
4718
+ return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-empty", children: "\u2014" });
4002
4719
  }
4003
4720
  const labels = schema.squisq?.enumLabels;
4004
4721
  const display = labels && typeof value === "string" ? labels[value] ?? value : String(value);
4005
- return /* @__PURE__ */ jsx21("span", { className: "squisq-jv-value", children: display });
4722
+ return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-value", children: display });
4006
4723
  }
4007
4724
  function ColorViewer({ value }) {
4008
4725
  if (typeof value !== "string" || value === "") {
4009
- return /* @__PURE__ */ jsx21("span", { className: "squisq-jv-empty", children: "\u2014" });
4726
+ return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-empty", children: "\u2014" });
4010
4727
  }
4011
- return /* @__PURE__ */ jsxs15("span", { className: "squisq-jv-color", children: [
4012
- /* @__PURE__ */ jsx21("span", { className: "squisq-jv-color__swatch", style: { background: value }, "aria-hidden": true }),
4013
- /* @__PURE__ */ jsx21("span", { className: "squisq-jv-color__hex", children: value })
4728
+ return /* @__PURE__ */ jsxs17("span", { className: "squisq-jv-color", children: [
4729
+ /* @__PURE__ */ jsx24("span", { className: "squisq-jv-color__swatch", style: { background: value }, "aria-hidden": true }),
4730
+ /* @__PURE__ */ jsx24("span", { className: "squisq-jv-color__hex", children: value })
4014
4731
  ] });
4015
4732
  }
4016
4733
  function DateViewer({ value, schema }) {
4017
4734
  if (typeof value !== "string" || value === "") {
4018
- return /* @__PURE__ */ jsx21("span", { className: "squisq-jv-empty", children: "\u2014" });
4735
+ return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-empty", children: "\u2014" });
4019
4736
  }
4020
4737
  const fmt = schema.format;
4021
4738
  let display = value;
@@ -4032,31 +4749,31 @@ function DateViewer({ value, schema }) {
4032
4749
  }
4033
4750
  } catch {
4034
4751
  }
4035
- return /* @__PURE__ */ jsx21("span", { className: "squisq-jv-value", children: display });
4752
+ return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-value", children: display });
4036
4753
  }
4037
4754
  function ChipBinViewer({ value, schema }) {
4038
4755
  if (!Array.isArray(value) || value.length === 0) {
4039
- return /* @__PURE__ */ jsx21("span", { className: "squisq-jv-empty", children: "\u2014" });
4756
+ return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-empty", children: "\u2014" });
4040
4757
  }
4041
4758
  const itemSchema = Array.isArray(schema.items) ? schema.items[0] : schema.items;
4042
4759
  const labels = itemSchema?.squisq?.enumLabels;
4043
- return /* @__PURE__ */ jsx21("div", { className: "squisq-jv-chip-bin", children: value.map((item, i) => {
4760
+ return /* @__PURE__ */ jsx24("div", { className: "squisq-jv-chip-bin", children: value.map((item, i) => {
4044
4761
  const label = labels && typeof item === "string" ? labels[item] ?? String(item) : String(item);
4045
- return /* @__PURE__ */ jsx21("span", { className: "squisq-jv-chip", children: label }, i);
4762
+ return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-chip", children: label }, i);
4046
4763
  }) });
4047
4764
  }
4048
4765
  function CardStackViewer(props) {
4049
4766
  const { value, schema, rootSchema, rootData, pointer, density } = props;
4050
4767
  if (!Array.isArray(value) || value.length === 0) {
4051
- return /* @__PURE__ */ jsx21("span", { className: "squisq-jv-empty", children: "No items" });
4768
+ return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-empty", children: "No items" });
4052
4769
  }
4053
4770
  const itemSchema = (Array.isArray(schema.items) ? schema.items[0] : schema.items) ?? {};
4054
4771
  const itemLabel = itemSchema.squisq?.itemLabel;
4055
- return /* @__PURE__ */ jsx21("div", { className: "squisq-jv-card-stack", children: value.map((item, i) => {
4772
+ return /* @__PURE__ */ jsx24("div", { className: "squisq-jv-card-stack", children: value.map((item, i) => {
4056
4773
  const title = resolveItemTitle(itemLabel, item, i);
4057
- return /* @__PURE__ */ jsxs15("div", { className: "squisq-jv-card", children: [
4058
- title ? /* @__PURE__ */ jsx21("h4", { className: "squisq-jv-card__title", children: title }) : null,
4059
- /* @__PURE__ */ jsx21(
4774
+ return /* @__PURE__ */ jsxs17("div", { className: "squisq-jv-card", children: [
4775
+ title ? /* @__PURE__ */ jsx24("h4", { className: "squisq-jv-card__title", children: title }) : null,
4776
+ /* @__PURE__ */ jsx24(
4060
4777
  RenderNode,
4061
4778
  {
4062
4779
  value: item,
@@ -4087,16 +4804,16 @@ function GroupViewer(props) {
4087
4804
  const help = schema.squisq?.help ?? schema.description;
4088
4805
  const obj = (value && typeof value === "object" ? value : {}) ?? {};
4089
4806
  const propEntries = Object.entries(schema.properties ?? {});
4090
- return /* @__PURE__ */ jsxs15("section", { className: "squisq-jv-group", children: [
4091
- title ? /* @__PURE__ */ jsx21("h3", { className: "squisq-jv-group__title", children: title }) : null,
4092
- help ? /* @__PURE__ */ jsx21("p", { className: "squisq-jv-group__help", children: help }) : null,
4093
- propEntries.map(([key, propSchema]) => /* @__PURE__ */ jsx21(Fragment3, { children: /* @__PURE__ */ jsx21(
4807
+ return /* @__PURE__ */ jsxs17("section", { className: "squisq-jv-group", children: [
4808
+ title ? /* @__PURE__ */ jsx24("h3", { className: "squisq-jv-group__title", children: title }) : null,
4809
+ help ? /* @__PURE__ */ jsx24("p", { className: "squisq-jv-group__help", children: help }) : null,
4810
+ propEntries.map(([key, propSchema]) => /* @__PURE__ */ jsx24(Fragment4, { children: /* @__PURE__ */ jsx24(
4094
4811
  RowOrSection,
4095
4812
  {
4096
4813
  label: propSchema.squisq?.label ?? propSchema.title ?? key,
4097
4814
  help: propSchema.squisq?.help ?? propSchema.description,
4098
4815
  kindHint: propSchema,
4099
- children: /* @__PURE__ */ jsx21(
4816
+ children: /* @__PURE__ */ jsx24(
4100
4817
  RenderNode,
4101
4818
  {
4102
4819
  value: obj[key],
@@ -4120,11 +4837,11 @@ function RowOrSection({
4120
4837
  }) {
4121
4838
  const composite = isCompositeKind(kindHint);
4122
4839
  if (composite) {
4123
- return /* @__PURE__ */ jsx21(Fragment4, { children });
4840
+ return /* @__PURE__ */ jsx24(Fragment5, { children });
4124
4841
  }
4125
- return /* @__PURE__ */ jsxs15("div", { className: "squisq-jv-row", children: [
4126
- /* @__PURE__ */ jsx21("div", { className: "squisq-jv-label", title: help, children: label }),
4127
- /* @__PURE__ */ jsx21("div", { children })
4842
+ return /* @__PURE__ */ jsxs17("div", { className: "squisq-jv-row", children: [
4843
+ /* @__PURE__ */ jsx24("div", { className: "squisq-jv-label", title: help, children: label }),
4844
+ /* @__PURE__ */ jsx24("div", { children })
4128
4845
  ] });
4129
4846
  }
4130
4847
  function isCompositeKind(schema) {
@@ -4145,11 +4862,11 @@ function TabsViewer(props) {
4145
4862
  const matchedIndex = pickMatchingBranch(branches, value);
4146
4863
  const branch = branches[matchedIndex];
4147
4864
  if (!branch) {
4148
- return /* @__PURE__ */ jsx21(TextViewer, { ...props });
4865
+ return /* @__PURE__ */ jsx24(TextViewer, { ...props });
4149
4866
  }
4150
- return /* @__PURE__ */ jsxs15("div", { className: "squisq-jv-tabs", children: [
4151
- /* @__PURE__ */ jsx21("span", { className: "squisq-jv-tabs__discriminator", children: branch.squisq?.label ?? branch.title ?? `Option ${matchedIndex + 1}` }),
4152
- /* @__PURE__ */ jsx21(
4867
+ return /* @__PURE__ */ jsxs17("div", { className: "squisq-jv-tabs", children: [
4868
+ /* @__PURE__ */ jsx24("span", { className: "squisq-jv-tabs__discriminator", children: branch.squisq?.label ?? branch.title ?? `Option ${matchedIndex + 1}` }),
4869
+ /* @__PURE__ */ jsx24(
4153
4870
  RenderNode,
4154
4871
  {
4155
4872
  value,
@@ -4213,7 +4930,7 @@ var VIEWERS = {
4213
4930
  };
4214
4931
 
4215
4932
  // src/jsonView/RenderNode.tsx
4216
- import { jsx as jsx22 } from "react/jsx-runtime";
4933
+ import { jsx as jsx25 } from "react/jsx-runtime";
4217
4934
  function RenderNode(props) {
4218
4935
  const resolved = resolveRef(props.schema, props.rootSchema) ?? props.schema;
4219
4936
  if (resolveFlag(resolved.squisq?.hidden, props.rootData)) return null;
@@ -4229,18 +4946,18 @@ function RenderNode(props) {
4229
4946
  };
4230
4947
  if (kind === "group" || kind === "card") {
4231
4948
  const Group = Viewer;
4232
- return /* @__PURE__ */ jsx22(Group, { ...viewerProps, suppressTitle: props.suppressTopGroupTitle });
4949
+ return /* @__PURE__ */ jsx25(Group, { ...viewerProps, suppressTitle: props.suppressTopGroupTitle });
4233
4950
  }
4234
- return /* @__PURE__ */ jsx22(Viewer, { ...viewerProps });
4951
+ return /* @__PURE__ */ jsx25(Viewer, { ...viewerProps });
4235
4952
  }
4236
4953
 
4237
4954
  // src/jsonView/JsonView.tsx
4238
- import { jsx as jsx23 } from "react/jsx-runtime";
4955
+ import { jsx as jsx26 } from "react/jsx-runtime";
4239
4956
  function JsonView(props) {
4240
4957
  const { schema, value, theme, surface, density = "comfortable", className } = props;
4241
4958
  const { style } = useJsonViewTokens(theme, surface);
4242
4959
  const cls = "squisq-json-view" + (density === "compact" ? " squisq-json-view--compact" : "") + (className ? ` ${className}` : "");
4243
- return /* @__PURE__ */ jsx23("div", { className: cls, style, children: /* @__PURE__ */ jsx23(
4960
+ return /* @__PURE__ */ jsx26("div", { className: cls, style, children: /* @__PURE__ */ jsx26(
4244
4961
  RenderNode,
4245
4962
  {
4246
4963
  value,
@@ -4269,7 +4986,9 @@ export {
4269
4986
  LinearDocView,
4270
4987
  MapLayer,
4271
4988
  MarkdownRenderer,
4989
+ MediaClipLayer,
4272
4990
  MediaContext,
4991
+ PathLayer,
4273
4992
  ShapeLayer,
4274
4993
  SocialCaptionOverlay,
4275
4994
  TableLayer,
@@ -4283,6 +5002,7 @@ export {
4283
5002
  useAutoSurface,
4284
5003
  useDocPlayback,
4285
5004
  useMediaProvider,
5005
+ useMediaSchedule,
4286
5006
  useMediaUrl,
4287
5007
  useViewportOrientation
4288
5008
  };