@bendyline/squisq-react 1.3.2 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 useRef6, useState as useState6, useEffect as useEffect7, useCallback as useCallback5, 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,
@@ -1709,12 +2249,12 @@ function useViewportOrientation() {
1709
2249
  import {
1710
2250
  expandCoverBlock,
1711
2251
  createTemplateContext,
1712
- DEFAULT_THEME as DEFAULT_THEME2,
2252
+ DEFAULT_THEME as DEFAULT_THEME3,
1713
2253
  VIEWPORT_PRESETS as VIEWPORT_PRESETS4
1714
2254
  } from "@bendyline/squisq/doc";
1715
2255
 
1716
2256
  // src/DocProgressBar.tsx
1717
- import { useRef as useRef4, useState as useState6, useCallback as useCallback4 } from "react";
2257
+ import { useRef as useRef5, useState as useState5, useCallback as useCallback4 } from "react";
1718
2258
 
1719
2259
  // src/types.ts
1720
2260
  function formatTime(seconds) {
@@ -1724,7 +2264,7 @@ function formatTime(seconds) {
1724
2264
  }
1725
2265
 
1726
2266
  // src/DocProgressBar.tsx
1727
- import { jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
2267
+ import { jsx as jsx13, jsxs as jsxs8 } from "react/jsx-runtime";
1728
2268
  function DocProgressBar({
1729
2269
  state,
1730
2270
  actions,
@@ -1732,8 +2272,9 @@ function DocProgressBar({
1732
2272
  expandedBlocks,
1733
2273
  getBlockTitle
1734
2274
  }) {
1735
- const progressBarRef = useRef4(null);
1736
- const [hoverPosition, setHoverPosition] = useState6(null);
2275
+ const progressBarRef = useRef5(null);
2276
+ const [hoverPosition, setHoverPosition] = useState5(null);
2277
+ const playProgress = state.totalDuration > 0 ? Math.max(0, Math.min(1, state.currentTime / state.totalDuration)) : 0;
1737
2278
  const handleProgressHover = useCallback4((e) => {
1738
2279
  const bar = progressBarRef.current;
1739
2280
  if (!bar) return;
@@ -1757,7 +2298,7 @@ function DocProgressBar({
1757
2298
  },
1758
2299
  [expandedBlocks]
1759
2300
  );
1760
- return /* @__PURE__ */ jsxs6(
2301
+ return /* @__PURE__ */ jsxs8(
1761
2302
  "div",
1762
2303
  {
1763
2304
  ref: progressBarRef,
@@ -1778,7 +2319,7 @@ function DocProgressBar({
1778
2319
  onMouseMove: handleProgressHover,
1779
2320
  onMouseLeave: handleProgressLeave,
1780
2321
  children: [
1781
- /* @__PURE__ */ jsx10(
2322
+ /* @__PURE__ */ jsx13(
1782
2323
  "div",
1783
2324
  {
1784
2325
  style: {
@@ -1791,21 +2332,21 @@ function DocProgressBar({
1791
2332
  }
1792
2333
  }
1793
2334
  ),
1794
- /* @__PURE__ */ jsx10(
2335
+ /* @__PURE__ */ jsx13(
1795
2336
  "div",
1796
2337
  {
2338
+ "data-testid": "doc-progress-fill",
1797
2339
  style: {
1798
2340
  position: "absolute",
1799
2341
  left: 0,
1800
- width: `${state.docProgress * 100}%`,
2342
+ width: `${playProgress * 100}%`,
1801
2343
  height: "6px",
1802
2344
  background: "#5b9bd5",
1803
- borderRadius: "3px",
1804
- transition: "width 0.1s"
2345
+ borderRadius: "3px"
1805
2346
  }
1806
2347
  }
1807
2348
  ),
1808
- blockMarkers.map((marker, i) => /* @__PURE__ */ jsx10(
2349
+ blockMarkers.map((marker, i) => /* @__PURE__ */ jsx13(
1809
2350
  "div",
1810
2351
  {
1811
2352
  style: {
@@ -1835,7 +2376,7 @@ function DocProgressBar({
1835
2376
  },
1836
2377
  `${marker.block.id}-${i}`
1837
2378
  )),
1838
- hoverPosition !== null && /* @__PURE__ */ jsxs6(
2379
+ hoverPosition !== null && /* @__PURE__ */ jsxs8(
1839
2380
  "div",
1840
2381
  {
1841
2382
  style: {
@@ -1852,19 +2393,19 @@ function DocProgressBar({
1852
2393
  zIndex: 10
1853
2394
  },
1854
2395
  children: [
1855
- /* @__PURE__ */ jsx10("div", { style: { color: "white", fontSize: "12px", fontFamily: "monospace" }, children: formatTime(hoverPosition * state.totalDuration) }),
2396
+ /* @__PURE__ */ jsx13("div", { style: { color: "white", fontSize: "12px", fontFamily: "monospace" }, children: formatTime(hoverPosition * state.totalDuration) }),
1856
2397
  (() => {
1857
2398
  const hoverTime = hoverPosition * state.totalDuration;
1858
2399
  const slideInfo = getBlockAtTimeLocal(hoverTime);
1859
2400
  if (slideInfo && getBlockTitle) {
1860
- return /* @__PURE__ */ jsx10("div", { style: { color: "rgba(255,255,255,0.7)", fontSize: "11px", marginTop: "2px" }, children: getBlockTitle(slideInfo.block) });
2401
+ return /* @__PURE__ */ jsx13("div", { style: { color: "rgba(255,255,255,0.7)", fontSize: "11px", marginTop: "2px" }, children: getBlockTitle(slideInfo.block) });
1861
2402
  }
1862
2403
  return null;
1863
2404
  })()
1864
2405
  ]
1865
2406
  }
1866
2407
  ),
1867
- hoverPosition !== null && /* @__PURE__ */ jsx10(
2408
+ hoverPosition !== null && /* @__PURE__ */ jsx13(
1868
2409
  "div",
1869
2410
  {
1870
2411
  style: {
@@ -1886,7 +2427,7 @@ function DocProgressBar({
1886
2427
  }
1887
2428
 
1888
2429
  // src/DocControlsOverlay.tsx
1889
- import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
2430
+ import { jsx as jsx14, jsxs as jsxs9 } from "react/jsx-runtime";
1890
2431
  function DocControlsOverlay({
1891
2432
  state,
1892
2433
  actions,
@@ -1894,7 +2435,7 @@ function DocControlsOverlay({
1894
2435
  expandedBlocks,
1895
2436
  getBlockTitle
1896
2437
  }) {
1897
- return /* @__PURE__ */ jsxs7(
2438
+ return /* @__PURE__ */ jsxs9(
1898
2439
  "div",
1899
2440
  {
1900
2441
  className: "doc-player__controls",
@@ -1911,7 +2452,7 @@ function DocControlsOverlay({
1911
2452
  zIndex: 100
1912
2453
  },
1913
2454
  children: [
1914
- /* @__PURE__ */ jsx11(
2455
+ /* @__PURE__ */ jsx14(
1915
2456
  "button",
1916
2457
  {
1917
2458
  onClick: actions.restart,
@@ -1927,10 +2468,10 @@ function DocControlsOverlay({
1927
2468
  },
1928
2469
  title: "Restart",
1929
2470
  "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" }) })
2471
+ 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
2472
  }
1932
2473
  ),
1933
- /* @__PURE__ */ jsx11(
2474
+ /* @__PURE__ */ jsx14(
1934
2475
  "button",
1935
2476
  {
1936
2477
  onClick: actions.toggle,
@@ -1949,15 +2490,15 @@ function DocControlsOverlay({
1949
2490
  height: "40px"
1950
2491
  },
1951
2492
  "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" }) })
2493
+ 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
2494
  }
1954
2495
  ),
1955
- /* @__PURE__ */ jsxs7("span", { style: { color: "white", fontSize: "12px", minWidth: "80px", fontFamily: "monospace" }, children: [
2496
+ /* @__PURE__ */ jsxs9("span", { style: { color: "white", fontSize: "12px", minWidth: "80px", fontFamily: "monospace" }, children: [
1956
2497
  formatTime(state.currentTime),
1957
2498
  " / ",
1958
2499
  formatTime(state.totalDuration)
1959
2500
  ] }),
1960
- /* @__PURE__ */ jsx11(
2501
+ /* @__PURE__ */ jsx14(
1961
2502
  DocProgressBar,
1962
2503
  {
1963
2504
  state,
@@ -1967,12 +2508,12 @@ function DocControlsOverlay({
1967
2508
  getBlockTitle
1968
2509
  }
1969
2510
  ),
1970
- /* @__PURE__ */ jsxs7("span", { style: { color: "rgba(255,255,255,0.5)", fontSize: "11px", whiteSpace: "nowrap" }, children: [
2511
+ /* @__PURE__ */ jsxs9("span", { style: { color: "rgba(255,255,255,0.5)", fontSize: "11px", whiteSpace: "nowrap" }, children: [
1971
2512
  state.currentBlockIndex + 1,
1972
2513
  "/",
1973
2514
  state.totalBlocks
1974
2515
  ] }),
1975
- state.hasCaptions && /* @__PURE__ */ jsxs7(
2516
+ state.hasCaptions && /* @__PURE__ */ jsxs9(
1976
2517
  "button",
1977
2518
  {
1978
2519
  onClick: () => actions.cycleCaptionMode(),
@@ -1991,12 +2532,12 @@ function DocControlsOverlay({
1991
2532
  title: state.captionMode === "off" ? "Captions: Off (click for Standard)" : state.captionMode === "standard" ? "Captions: Standard (click for Social)" : "Captions: Social (click to turn off)",
1992
2533
  "aria-label": "Cycle caption style",
1993
2534
  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" })
2535
+ /* @__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" }) }),
2536
+ state.captionMode !== "off" && /* @__PURE__ */ jsx14("span", { style: { fontSize: "10px", textTransform: "uppercase", letterSpacing: "0.5px" }, children: state.captionMode === "standard" ? "CC" : "SM" })
1996
2537
  ]
1997
2538
  }
1998
2539
  ),
1999
- actions.toggleFullscreen && /* @__PURE__ */ jsx11(
2540
+ actions.toggleFullscreen && /* @__PURE__ */ jsx14(
2000
2541
  "button",
2001
2542
  {
2002
2543
  onClick: actions.toggleFullscreen,
@@ -2012,7 +2553,7 @@ function DocControlsOverlay({
2012
2553
  },
2013
2554
  title: state.isFullscreen ? "Exit fullscreen (F)" : "Fullscreen (F)",
2014
2555
  "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" }) })
2556
+ 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
2557
  }
2017
2558
  )
2018
2559
  ]
@@ -2021,12 +2562,12 @@ function DocControlsOverlay({
2021
2562
  }
2022
2563
 
2023
2564
  // src/DocControlsSlideshow.tsx
2024
- import { jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
2565
+ import { jsx as jsx15, jsxs as jsxs10 } from "react/jsx-runtime";
2025
2566
  function DocControlsSlideshow({ state, slideNav }) {
2026
2567
  const { currentBlockIndex, totalBlocks } = state;
2027
2568
  const isFirst = currentBlockIndex <= 0;
2028
2569
  const isLast = currentBlockIndex >= totalBlocks - 1;
2029
- return /* @__PURE__ */ jsxs8(
2570
+ return /* @__PURE__ */ jsxs10(
2030
2571
  "div",
2031
2572
  {
2032
2573
  className: "doc-controls-slideshow",
@@ -2047,7 +2588,7 @@ function DocControlsSlideshow({ state, slideNav }) {
2047
2588
  WebkitBackdropFilter: "blur(8px)"
2048
2589
  },
2049
2590
  children: [
2050
- /* @__PURE__ */ jsx12(
2591
+ /* @__PURE__ */ jsx15(
2051
2592
  "button",
2052
2593
  {
2053
2594
  onClick: (e) => {
@@ -2076,10 +2617,10 @@ function DocControlsSlideshow({ state, slideNav }) {
2076
2617
  onMouseLeave: (e) => {
2077
2618
  e.currentTarget.style.background = "none";
2078
2619
  },
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" }) })
2620
+ 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
2621
  }
2081
2622
  ),
2082
- /* @__PURE__ */ jsx12(
2623
+ /* @__PURE__ */ jsx15(
2083
2624
  "span",
2084
2625
  {
2085
2626
  "data-testid": "slide-counter",
@@ -2096,7 +2637,7 @@ function DocControlsSlideshow({ state, slideNav }) {
2096
2637
  children: totalBlocks > 0 ? `${currentBlockIndex + 1} / ${totalBlocks}` : "\u2014"
2097
2638
  }
2098
2639
  ),
2099
- /* @__PURE__ */ jsx12(
2640
+ /* @__PURE__ */ jsx15(
2100
2641
  "button",
2101
2642
  {
2102
2643
  onClick: (e) => {
@@ -2125,7 +2666,7 @@ function DocControlsSlideshow({ state, slideNav }) {
2125
2666
  onMouseLeave: (e) => {
2126
2667
  e.currentTarget.style.background = "none";
2127
2668
  },
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" }) })
2669
+ 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
2670
  }
2130
2671
  )
2131
2672
  ]
@@ -2134,20 +2675,24 @@ function DocControlsSlideshow({ state, slideNav }) {
2134
2675
  }
2135
2676
 
2136
2677
  // src/LinearDocView.tsx
2137
- import { useMemo as useMemo6 } from "react";
2678
+ import { useMemo as useMemo8 } from "react";
2138
2679
  import {
2139
2680
  applySurface,
2140
2681
  resolveFontFamily as resolveFontFamily2
2141
2682
  } from "@bendyline/squisq/schemas";
2142
2683
  import { VIEWPORT_PRESETS as VIEWPORT_PRESETS3 } from "@bendyline/squisq/schemas";
2143
- import { getLayers, hasTemplate, DEFAULT_THEME } from "@bendyline/squisq/doc";
2684
+ import { getLayers, hasTemplate, DEFAULT_THEME as DEFAULT_THEME2, deriveTemplateInputs } from "@bendyline/squisq/doc";
2144
2685
  import { extractPlainText } from "@bendyline/squisq/markdown";
2145
2686
 
2146
2687
  // src/MarkdownRenderer.tsx
2147
- import { Fragment } from "react";
2688
+ import { Fragment as Fragment2 } from "react";
2689
+ import {
2690
+ sanitizeHtmlNodes as sanitizeHtmlNodes2,
2691
+ sanitizeUrl
2692
+ } from "@bendyline/squisq/markdown";
2148
2693
 
2149
2694
  // src/InlineVideoPlayer.tsx
2150
- import { jsx as jsx13 } from "react/jsx-runtime";
2695
+ import { jsx as jsx16 } from "react/jsx-runtime";
2151
2696
  function InlineVideoPlayer({
2152
2697
  src,
2153
2698
  basePath = "",
@@ -2162,7 +2707,7 @@ function InlineVideoPlayer({
2162
2707
  const resolvedPoster = useMediaUrl(poster ?? "", basePath);
2163
2708
  const posterUrl = poster ? resolvedPoster : void 0;
2164
2709
  if (!resolvedSrc) return null;
2165
- return /* @__PURE__ */ jsx13("span", { className: `squisq-inline-video-player ${className ?? ""}`.trim(), children: /* @__PURE__ */ jsx13(
2710
+ return /* @__PURE__ */ jsx16("span", { className: `squisq-inline-video-player ${className ?? ""}`.trim(), children: /* @__PURE__ */ jsx16(
2166
2711
  "video",
2167
2712
  {
2168
2713
  src: resolvedSrc,
@@ -2177,7 +2722,7 @@ function InlineVideoPlayer({
2177
2722
  }
2178
2723
 
2179
2724
  // src/InlineAudioPlayer.tsx
2180
- import { jsx as jsx14 } from "react/jsx-runtime";
2725
+ import { jsx as jsx17 } from "react/jsx-runtime";
2181
2726
  function InlineAudioPlayer({
2182
2727
  src,
2183
2728
  basePath = "",
@@ -2187,55 +2732,61 @@ function InlineAudioPlayer({
2187
2732
  }) {
2188
2733
  const resolvedSrc = useMediaUrl(src, basePath);
2189
2734
  if (!resolvedSrc) return null;
2190
- return /* @__PURE__ */ jsx14("span", { className: `squisq-inline-audio-player ${className ?? ""}`.trim(), children: /* @__PURE__ */ jsx14("audio", { src: resolvedSrc, controls, preload }) });
2735
+ return /* @__PURE__ */ jsx17("span", { className: `squisq-inline-audio-player ${className ?? ""}`.trim(), children: /* @__PURE__ */ jsx17("audio", { src: resolvedSrc, controls, preload }) });
2191
2736
  }
2192
2737
 
2193
2738
  // src/MarkdownRenderer.tsx
2194
- import { jsx as jsx15, jsxs as jsxs9 } from "react/jsx-runtime";
2195
- function renderInline(nodes, keyPrefix = "") {
2739
+ import { jsx as jsx18, jsxs as jsxs11 } from "react/jsx-runtime";
2740
+ function renderInline(nodes, keyPrefix = "", htmlPolicy = "sanitize") {
2196
2741
  return nodes.map((node, i) => {
2197
2742
  const key = `${keyPrefix}i${i}`;
2198
2743
  switch (node.type) {
2199
2744
  case "text": {
2200
2745
  if (!node.value.includes("\n")) {
2201
- return /* @__PURE__ */ jsx15(Fragment, { children: node.value }, key);
2746
+ return /* @__PURE__ */ jsx18(Fragment2, { children: node.value }, key);
2202
2747
  }
2203
2748
  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", {}),
2749
+ return /* @__PURE__ */ jsx18(Fragment2, { children: parts.map((part, j) => /* @__PURE__ */ jsxs11(Fragment2, { children: [
2750
+ j > 0 && /* @__PURE__ */ jsx18("br", {}),
2206
2751
  part
2207
2752
  ] }, j)) }, key);
2208
2753
  }
2209
2754
  case "emphasis":
2210
- return /* @__PURE__ */ jsx15("em", { className: "squisq-md-em", children: renderInline(node.children, key) }, key);
2755
+ return /* @__PURE__ */ jsx18("em", { className: "squisq-md-em", children: renderInline(node.children, key, htmlPolicy) }, key);
2211
2756
  case "strong":
2212
- return /* @__PURE__ */ jsx15("strong", { className: "squisq-md-strong", children: renderInline(node.children, key) }, key);
2757
+ return /* @__PURE__ */ jsx18("strong", { className: "squisq-md-strong", children: renderInline(node.children, key, htmlPolicy) }, key);
2213
2758
  case "delete":
2214
- return /* @__PURE__ */ jsx15("del", { className: "squisq-md-del", children: renderInline(node.children, key) }, key);
2759
+ return /* @__PURE__ */ jsx18("del", { className: "squisq-md-del", children: renderInline(node.children, key, htmlPolicy) }, key);
2215
2760
  case "inlineCode":
2216
- return /* @__PURE__ */ jsx15("code", { className: "squisq-md-inline-code", children: node.value }, key);
2217
- case "link":
2218
- return /* @__PURE__ */ jsx15(
2761
+ return /* @__PURE__ */ jsx18("code", { className: "squisq-md-inline-code", children: node.value }, key);
2762
+ case "link": {
2763
+ const href = sanitizeUrl(node.url, "link");
2764
+ if (!href) {
2765
+ return /* @__PURE__ */ jsx18("span", { className: "squisq-md-link squisq-md-link--blocked", children: renderInline(node.children, key, htmlPolicy) }, key);
2766
+ }
2767
+ return /* @__PURE__ */ jsx18(
2219
2768
  "a",
2220
2769
  {
2221
2770
  className: "squisq-md-link",
2222
- href: node.url,
2771
+ href,
2223
2772
  title: node.title ?? void 0,
2224
2773
  target: "_blank",
2225
2774
  rel: "noopener noreferrer",
2226
- children: renderInline(node.children, key)
2775
+ children: renderInline(node.children, key, htmlPolicy)
2227
2776
  },
2228
2777
  key
2229
2778
  );
2779
+ }
2230
2780
  case "image":
2231
- return /* @__PURE__ */ jsx15(MdImage, { src: node.url, alt: node.alt ?? "", title: node.title ?? void 0 }, key);
2781
+ return /* @__PURE__ */ jsx18(MdImage, { src: node.url, alt: node.alt ?? "", title: node.title ?? void 0 }, key);
2232
2782
  case "break":
2233
- return /* @__PURE__ */ jsx15("br", {}, key);
2783
+ return /* @__PURE__ */ jsx18("br", {}, key);
2234
2784
  case "inlineMath":
2235
- return /* @__PURE__ */ jsx15("code", { className: "squisq-md-inline-math", children: node.value }, key);
2785
+ return /* @__PURE__ */ jsx18("code", { className: "squisq-md-inline-math", children: node.value }, key);
2236
2786
  case "htmlInline":
2237
- if (!containsMediaTag(node.htmlChildren)) {
2238
- return /* @__PURE__ */ jsx15(
2787
+ if (htmlPolicy === "strip") return null;
2788
+ if (htmlPolicy === "trusted" && !containsMediaTag(node.htmlChildren) && !containsDangerousTag(node.htmlChildren) && !hasDangerousRawHtml(node.rawHtml)) {
2789
+ return /* @__PURE__ */ jsx18(
2239
2790
  "span",
2240
2791
  {
2241
2792
  className: "squisq-md-html-inline",
@@ -2244,25 +2795,25 @@ function renderInline(nodes, keyPrefix = "") {
2244
2795
  key
2245
2796
  );
2246
2797
  }
2247
- return /* @__PURE__ */ jsx15("span", { className: "squisq-md-html-inline", children: renderHtmlNodes(node.htmlChildren, `${key}h`) }, key);
2798
+ return /* @__PURE__ */ jsx18("span", { className: "squisq-md-html-inline", children: renderHtmlNodes(resolveHtmlNodes(node.htmlChildren, htmlPolicy), `${key}h`) }, key);
2248
2799
  case "footnoteReference":
2249
- return /* @__PURE__ */ jsx15("sup", { className: "squisq-md-footnote-ref", children: /* @__PURE__ */ jsxs9("a", { href: `#fn-${node.identifier}`, children: [
2800
+ return /* @__PURE__ */ jsx18("sup", { className: "squisq-md-footnote-ref", children: /* @__PURE__ */ jsxs11("a", { href: `#fn-${node.identifier}`, children: [
2250
2801
  "[",
2251
2802
  node.label ?? node.identifier,
2252
2803
  "]"
2253
2804
  ] }) }, key);
2254
2805
  case "linkReference":
2255
- return /* @__PURE__ */ jsx15("span", { className: "squisq-md-link-ref", children: renderInline(node.children, key) }, key);
2806
+ return /* @__PURE__ */ jsx18("span", { className: "squisq-md-link-ref", children: renderInline(node.children, key, htmlPolicy) }, key);
2256
2807
  case "imageReference":
2257
- return /* @__PURE__ */ jsxs9("span", { className: "squisq-md-image-ref", children: [
2808
+ return /* @__PURE__ */ jsxs11("span", { className: "squisq-md-image-ref", children: [
2258
2809
  "[",
2259
2810
  node.alt ?? node.identifier,
2260
2811
  "]"
2261
2812
  ] }, key);
2262
2813
  case "textDirective":
2263
- return /* @__PURE__ */ jsx15("span", { className: "squisq-md-text-directive", "data-directive": node.name, children: renderInline(node.children, key) }, key);
2814
+ return /* @__PURE__ */ jsx18("span", { className: "squisq-md-text-directive", "data-directive": node.name, children: renderInline(node.children, key, htmlPolicy) }, key);
2264
2815
  case "mention":
2265
- return /* @__PURE__ */ jsxs9(
2816
+ return /* @__PURE__ */ jsxs11(
2266
2817
  "span",
2267
2818
  {
2268
2819
  className: "squisq-md-mention mention",
@@ -2282,30 +2833,31 @@ function renderInline(nodes, keyPrefix = "") {
2282
2833
  }
2283
2834
  });
2284
2835
  }
2285
- function renderBlock(node, key) {
2836
+ function renderBlock(node, key, htmlPolicy = "sanitize") {
2286
2837
  switch (node.type) {
2287
2838
  case "paragraph":
2288
- return /* @__PURE__ */ jsx15("p", { className: "squisq-md-p", children: renderInline(node.children, key) }, key);
2839
+ return /* @__PURE__ */ jsx18("p", { className: "squisq-md-p", children: renderInline(node.children, key, htmlPolicy) }, key);
2289
2840
  case "heading": {
2290
2841
  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);
2842
+ return /* @__PURE__ */ jsx18(Tag, { className: `squisq-md-heading squisq-md-h${node.depth}`, children: renderInline(node.children, key, htmlPolicy) }, key);
2292
2843
  }
2293
2844
  case "blockquote":
2294
- return /* @__PURE__ */ jsx15("blockquote", { className: "squisq-md-blockquote", children: renderBlocks(node.children, key) }, key);
2845
+ return /* @__PURE__ */ jsx18("blockquote", { className: "squisq-md-blockquote", children: renderBlocks(node.children, key, htmlPolicy) }, key);
2295
2846
  case "list":
2296
2847
  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);
2848
+ 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}`, htmlPolicy)) }, key);
2298
2849
  }
2299
- return /* @__PURE__ */ jsx15("ul", { className: "squisq-md-list squisq-md-ul", children: node.children.map((item, i) => renderListItem(item, `${key}li${i}`)) }, key);
2850
+ return /* @__PURE__ */ jsx18("ul", { className: "squisq-md-list squisq-md-ul", children: node.children.map((item, i) => renderListItem(item, `${key}li${i}`, htmlPolicy)) }, key);
2300
2851
  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);
2852
+ 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
2853
  case "thematicBreak":
2303
- return /* @__PURE__ */ jsx15("hr", { className: "squisq-md-hr" }, key);
2854
+ return /* @__PURE__ */ jsx18("hr", { className: "squisq-md-hr" }, key);
2304
2855
  case "table":
2305
- return renderTable(node.children, node.align, key);
2856
+ return renderTable(node.children, node.align, key, htmlPolicy);
2306
2857
  case "htmlBlock":
2307
- if (!containsMediaTag(node.htmlChildren)) {
2308
- return /* @__PURE__ */ jsx15(
2858
+ if (htmlPolicy === "strip") return null;
2859
+ if (htmlPolicy === "trusted" && !containsMediaTag(node.htmlChildren) && !containsDangerousTag(node.htmlChildren) && !hasDangerousRawHtml(node.rawHtml)) {
2860
+ return /* @__PURE__ */ jsx18(
2309
2861
  "div",
2310
2862
  {
2311
2863
  className: "squisq-md-html-block",
@@ -2314,95 +2866,126 @@ function renderBlock(node, key) {
2314
2866
  key
2315
2867
  );
2316
2868
  }
2317
- return /* @__PURE__ */ jsx15("div", { className: "squisq-md-html-block", children: renderHtmlNodes(node.htmlChildren, `${key}h`) }, key);
2869
+ return /* @__PURE__ */ jsx18("div", { className: "squisq-md-html-block", children: renderHtmlNodes(resolveHtmlNodes(node.htmlChildren, htmlPolicy), `${key}h`) }, key);
2318
2870
  case "math":
2319
- return /* @__PURE__ */ jsx15("pre", { className: "squisq-md-math-block", children: /* @__PURE__ */ jsx15("code", { children: node.value }) }, key);
2871
+ return /* @__PURE__ */ jsx18("pre", { className: "squisq-md-math-block", children: /* @__PURE__ */ jsx18("code", { children: node.value }) }, key);
2320
2872
  case "definition":
2321
2873
  return null;
2322
2874
  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)
2875
+ return /* @__PURE__ */ jsxs11("div", { className: "squisq-md-footnote-def", id: `fn-${node.identifier}`, children: [
2876
+ /* @__PURE__ */ jsx18("sup", { children: node.label ?? node.identifier }),
2877
+ renderBlocks(node.children, key, htmlPolicy)
2326
2878
  ] }, key);
2327
2879
  case "containerDirective":
2328
- return /* @__PURE__ */ jsxs9(
2880
+ return /* @__PURE__ */ jsxs11(
2329
2881
  "div",
2330
2882
  {
2331
2883
  className: `squisq-md-directive squisq-md-directive-${node.name}`,
2332
2884
  "data-directive": node.name,
2333
2885
  children: [
2334
- node.label && /* @__PURE__ */ jsx15("div", { className: "squisq-md-directive-label", children: node.label }),
2335
- renderBlocks(node.children, key)
2886
+ node.label && /* @__PURE__ */ jsx18("div", { className: "squisq-md-directive-label", children: node.label }),
2887
+ renderBlocks(node.children, key, htmlPolicy)
2336
2888
  ]
2337
2889
  },
2338
2890
  key
2339
2891
  );
2340
2892
  case "leafDirective":
2341
- return /* @__PURE__ */ jsx15(
2893
+ return /* @__PURE__ */ jsx18(
2342
2894
  "div",
2343
2895
  {
2344
2896
  className: `squisq-md-directive squisq-md-directive-${node.name}`,
2345
2897
  "data-directive": node.name,
2346
- children: renderInline(node.children, key)
2898
+ children: renderInline(node.children, key, htmlPolicy)
2347
2899
  },
2348
2900
  key
2349
2901
  );
2350
2902
  case "definitionList":
2351
- return /* @__PURE__ */ jsx15("dl", { className: "squisq-md-dl", children: node.children.map((child, i) => {
2903
+ return /* @__PURE__ */ jsx18("dl", { className: "squisq-md-dl", children: node.children.map((child, i) => {
2352
2904
  if (child.type === "definitionTerm") {
2353
- return /* @__PURE__ */ jsx15("dt", { className: "squisq-md-dt", children: renderInline(child.children, `${key}dt${i}`) }, `${key}dt${i}`);
2905
+ return /* @__PURE__ */ jsx18("dt", { className: "squisq-md-dt", children: renderInline(child.children, `${key}dt${i}`, htmlPolicy) }, `${key}dt${i}`);
2354
2906
  }
2355
- return /* @__PURE__ */ jsx15("dd", { className: "squisq-md-dd", children: renderBlocks(child.children, `${key}dd${i}`) }, `${key}dd${i}`);
2907
+ return /* @__PURE__ */ jsx18("dd", { className: "squisq-md-dd", children: renderBlocks(child.children, `${key}dd${i}`, htmlPolicy) }, `${key}dd${i}`);
2356
2908
  }) }, key);
2357
2909
  default:
2358
2910
  return null;
2359
2911
  }
2360
2912
  }
2361
- function renderListItem(item, key) {
2913
+ function renderListItem(item, key, htmlPolicy = "sanitize") {
2362
2914
  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)
2915
+ return /* @__PURE__ */ jsxs11("li", { className: `squisq-md-li${isTask ? " squisq-md-task" : ""}`, children: [
2916
+ isTask && /* @__PURE__ */ jsx18("input", { type: "checkbox", checked: !!item.checked, readOnly: true, className: "squisq-md-checkbox" }),
2917
+ renderBlocks(item.children, key, htmlPolicy)
2366
2918
  ] }, key);
2367
2919
  }
2368
- function renderTable(rows, align, key) {
2920
+ function renderTable(rows, align, key, htmlPolicy = "sanitize") {
2369
2921
  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(
2922
+ return /* @__PURE__ */ jsxs11("table", { className: "squisq-md-table", children: [
2923
+ headerRow && /* @__PURE__ */ jsx18("thead", { children: /* @__PURE__ */ jsx18("tr", { children: headerRow.children.map((cell, ci) => /* @__PURE__ */ jsx18(
2372
2924
  "th",
2373
2925
  {
2374
2926
  className: "squisq-md-th",
2375
2927
  style: align?.[ci] ? { textAlign: align[ci] } : void 0,
2376
- children: renderInline(cell.children, `${key}th${ci}`)
2928
+ children: renderInline(cell.children, `${key}th${ci}`, htmlPolicy)
2377
2929
  },
2378
2930
  `${key}th${ci}`
2379
2931
  )) }) }),
2380
- bodyRows.length > 0 && /* @__PURE__ */ jsx15("tbody", { children: bodyRows.map((row, ri) => /* @__PURE__ */ jsx15("tr", { children: row.children.map((cell, ci) => /* @__PURE__ */ jsx15(
2932
+ bodyRows.length > 0 && /* @__PURE__ */ jsx18("tbody", { children: bodyRows.map((row, ri) => /* @__PURE__ */ jsx18("tr", { children: row.children.map((cell, ci) => /* @__PURE__ */ jsx18(
2381
2933
  "td",
2382
2934
  {
2383
2935
  className: "squisq-md-td",
2384
2936
  style: align?.[ci] ? { textAlign: align[ci] } : void 0,
2385
- children: renderInline(cell.children, `${key}td${ri}-${ci}`)
2937
+ children: renderInline(cell.children, `${key}td${ri}-${ci}`, htmlPolicy)
2386
2938
  },
2387
2939
  `${key}td${ri}-${ci}`
2388
2940
  )) }, `${key}tr${ri}`)) })
2389
2941
  ] }, key);
2390
2942
  }
2391
- function renderBlocks(nodes, keyPrefix = "") {
2392
- return nodes.map((node, i) => renderBlock(node, `${keyPrefix}b${i}`));
2943
+ function renderBlocks(nodes, keyPrefix = "", htmlPolicy = "sanitize") {
2944
+ return nodes.map((node, i) => renderBlock(node, `${keyPrefix}b${i}`, htmlPolicy));
2393
2945
  }
2394
2946
  function MdImage({ src, alt, title }) {
2395
- const resolved = useMediaUrl(src, ".");
2396
- return /* @__PURE__ */ jsx15("img", { className: "squisq-md-image", src: resolved, alt, title });
2947
+ const safeSrc = sanitizeUrl(src, "media");
2948
+ const resolved = useMediaUrl(safeSrc ?? "", ".");
2949
+ if (!safeSrc) return null;
2950
+ return /* @__PURE__ */ jsx18("img", { className: "squisq-md-image", src: resolved, alt, title });
2951
+ }
2952
+ function resolveHtmlNodes(nodes, htmlPolicy) {
2953
+ if (htmlPolicy === "strip") return [];
2954
+ if (htmlPolicy === "trusted") return nodes;
2955
+ return sanitizeHtmlNodes2(nodes);
2397
2956
  }
2398
2957
  function containsMediaTag(nodes) {
2399
2958
  for (const node of nodes) {
2400
2959
  if (node.type !== "htmlElement") continue;
2401
- if (node.tagName === "video" || node.tagName === "audio") return true;
2960
+ const tagName = node.tagName.toLowerCase();
2961
+ if (tagName === "video" || tagName === "audio") return true;
2402
2962
  if (containsMediaTag(node.children)) return true;
2403
2963
  }
2404
2964
  return false;
2405
2965
  }
2966
+ var DANGEROUS_HTML_TAGS = /* @__PURE__ */ new Set([
2967
+ "base",
2968
+ "embed",
2969
+ "iframe",
2970
+ "link",
2971
+ "meta",
2972
+ "object",
2973
+ "script",
2974
+ "style",
2975
+ "title"
2976
+ ]);
2977
+ function containsDangerousTag(nodes) {
2978
+ for (const node of nodes) {
2979
+ if (node.type !== "htmlElement") continue;
2980
+ if (DANGEROUS_HTML_TAGS.has(node.tagName.toLowerCase())) return true;
2981
+ if (containsDangerousTag(node.children)) return true;
2982
+ }
2983
+ return false;
2984
+ }
2985
+ var DANGEROUS_RAW_HTML_RE = /<\s*\/?\s*(?:base|embed|iframe|link|meta|object|script|style|title)\b/i;
2986
+ function hasDangerousRawHtml(rawHtml) {
2987
+ return DANGEROUS_RAW_HTML_RE.test(rawHtml);
2988
+ }
2406
2989
  var PASSTHROUGH_ATTRS = {
2407
2990
  // common
2408
2991
  class: "className",
@@ -2412,6 +2995,10 @@ var PASSTHROUGH_ATTRS = {
2412
2995
  // media-adjacent (used when video/audio appear inside other wrappers)
2413
2996
  width: "width",
2414
2997
  height: "height",
2998
+ src: "src",
2999
+ alt: "alt",
3000
+ loading: "loading",
3001
+ decoding: "decoding",
2415
3002
  // anchor
2416
3003
  href: "href",
2417
3004
  target: "target",
@@ -2431,8 +3018,10 @@ function reactPropsFromAttrs(attrs) {
2431
3018
  return out;
2432
3019
  }
2433
3020
  function renderHtmlElement(el, key) {
2434
- if (el.tagName === "video") {
2435
- return /* @__PURE__ */ jsx15(
3021
+ const tagName = el.tagName.toLowerCase();
3022
+ if (DANGEROUS_HTML_TAGS.has(tagName)) return null;
3023
+ if (tagName === "video") {
3024
+ return /* @__PURE__ */ jsx18(
2436
3025
  InlineVideoPlayer,
2437
3026
  {
2438
3027
  src: el.attributes.src ?? "",
@@ -2445,8 +3034,8 @@ function renderHtmlElement(el, key) {
2445
3034
  key
2446
3035
  );
2447
3036
  }
2448
- if (el.tagName === "audio") {
2449
- return /* @__PURE__ */ jsx15(
3037
+ if (tagName === "audio") {
3038
+ return /* @__PURE__ */ jsx18(
2450
3039
  InlineAudioPlayer,
2451
3040
  {
2452
3041
  src: el.attributes.src ?? "",
@@ -2456,12 +3045,12 @@ function renderHtmlElement(el, key) {
2456
3045
  key
2457
3046
  );
2458
3047
  }
2459
- const Tag = el.tagName;
3048
+ const Tag = tagName;
2460
3049
  const props = reactPropsFromAttrs(el.attributes);
2461
3050
  if (el.selfClosing) {
2462
- return /* @__PURE__ */ jsx15(Tag, { ...props }, key);
3051
+ return /* @__PURE__ */ jsx18(Tag, { ...props }, key);
2463
3052
  }
2464
- return /* @__PURE__ */ jsx15(Tag, { ...props, children: renderHtmlNodes(el.children, `${key}c`) }, key);
3053
+ return /* @__PURE__ */ jsx18(Tag, { ...props, children: renderHtmlNodes(el.children, `${key}c`) }, key);
2465
3054
  }
2466
3055
  function renderHtmlNodes(nodes, keyPrefix) {
2467
3056
  return nodes.map((node, i) => {
@@ -2470,7 +3059,7 @@ function renderHtmlNodes(nodes, keyPrefix) {
2470
3059
  case "htmlElement":
2471
3060
  return renderHtmlElement(node, key);
2472
3061
  case "htmlText":
2473
- return /* @__PURE__ */ jsx15(Fragment, { children: node.value }, key);
3062
+ return /* @__PURE__ */ jsx18(Fragment2, { children: node.value }, key);
2474
3063
  case "htmlComment":
2475
3064
  return null;
2476
3065
  default:
@@ -2478,13 +3067,17 @@ function renderHtmlNodes(nodes, keyPrefix) {
2478
3067
  }
2479
3068
  });
2480
3069
  }
2481
- function MarkdownRenderer({ nodes, className }) {
3070
+ function MarkdownRenderer({
3071
+ nodes,
3072
+ className,
3073
+ htmlPolicy = "sanitize"
3074
+ }) {
2482
3075
  if (!nodes || nodes.length === 0) return null;
2483
- return /* @__PURE__ */ jsx15("div", { className: `squisq-md ${className || ""}`, children: renderBlocks(nodes) });
3076
+ return /* @__PURE__ */ jsx18("div", { className: `squisq-md ${className || ""}`, children: renderBlocks(nodes, "", htmlPolicy) });
2484
3077
  }
2485
3078
 
2486
3079
  // src/LinearDocView.tsx
2487
- import { jsx as jsx16, jsxs as jsxs10 } from "react/jsx-runtime";
3080
+ import { jsx as jsx19, jsxs as jsxs12 } from "react/jsx-runtime";
2488
3081
  function isAnnotatedBlock(block) {
2489
3082
  const annotation = block.sourceHeading?.templateAnnotation;
2490
3083
  if (!annotation) return false;
@@ -2500,11 +3093,10 @@ function countAll(blocks) {
2500
3093
  }
2501
3094
  function BlockSection({ block, basePath, viewport, renderContext, blockIndex }) {
2502
3095
  const isAnnotated = isAnnotatedBlock(block);
2503
- const visualBlock = useMemo6(() => {
3096
+ const visualBlock = useMemo8(() => {
2504
3097
  if (!isAnnotated) return null;
2505
3098
  const annotation = block.sourceHeading.templateAnnotation;
2506
3099
  const headingText = extractPlainText(block.sourceHeading);
2507
- const bodyText = extractBodyPlainText(block.contents);
2508
3100
  const templateBlock = {
2509
3101
  id: block.id,
2510
3102
  template: annotation.template,
@@ -2512,12 +3104,14 @@ function BlockSection({ block, basePath, viewport, renderContext, blockIndex })
2512
3104
  duration: 1,
2513
3105
  audioSegment: 0,
2514
3106
  title: headingText,
2515
- ...getTemplateDefaults(
3107
+ ...deriveTemplateInputs(
2516
3108
  annotation.template ?? "sectionHeader",
2517
3109
  headingText,
2518
- bodyText,
2519
- block.contents
2520
- ),
3110
+ block.contents,
3111
+ {
3112
+ placeholders: true
3113
+ }
3114
+ ) ?? {},
2521
3115
  ...annotation.params,
2522
3116
  ...block.templateOverrides
2523
3117
  };
@@ -2532,15 +3126,15 @@ function BlockSection({ block, basePath, viewport, renderContext, blockIndex })
2532
3126
  template: annotation.template
2533
3127
  };
2534
3128
  }, [block, isAnnotated, renderContext, blockIndex]);
2535
- return /* @__PURE__ */ jsxs10(
3129
+ return /* @__PURE__ */ jsxs12(
2536
3130
  "div",
2537
3131
  {
2538
3132
  className: "squisq-linear-section",
2539
3133
  "data-block-id": block.id,
2540
3134
  "data-template": isAnnotated ? block.sourceHeading?.templateAnnotation?.template : void 0,
2541
3135
  children: [
2542
- block.sourceHeading && !isAnnotated && /* @__PURE__ */ jsx16(MarkdownRenderer, { nodes: [block.sourceHeading] }),
2543
- isAnnotated && visualBlock && /* @__PURE__ */ jsx16("div", { className: "squisq-linear-card", children: /* @__PURE__ */ jsx16(
3136
+ block.sourceHeading && !isAnnotated && /* @__PURE__ */ jsx19(MarkdownRenderer, { nodes: [block.sourceHeading] }),
3137
+ isAnnotated && visualBlock && /* @__PURE__ */ jsx19("div", { className: "squisq-linear-card", children: /* @__PURE__ */ jsx19(
2544
3138
  "div",
2545
3139
  {
2546
3140
  className: "squisq-linear-card-svg",
@@ -2550,7 +3144,7 @@ function BlockSection({ block, basePath, viewport, renderContext, blockIndex })
2550
3144
  overflow: "hidden",
2551
3145
  marginBottom: "1em"
2552
3146
  },
2553
- children: /* @__PURE__ */ jsx16(
3147
+ children: /* @__PURE__ */ jsx19(
2554
3148
  BlockRenderer,
2555
3149
  {
2556
3150
  block: visualBlock,
@@ -2561,8 +3155,8 @@ function BlockSection({ block, basePath, viewport, renderContext, blockIndex })
2561
3155
  )
2562
3156
  }
2563
3157
  ) }),
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(
3158
+ !isAnnotated && block.contents && block.contents.length > 0 && /* @__PURE__ */ jsx19(MarkdownRenderer, { nodes: block.contents }),
3159
+ block.children && block.children.length > 0 && /* @__PURE__ */ jsx19("div", { className: "squisq-linear-children", children: block.children.map((child, i) => /* @__PURE__ */ jsx19(
2566
3160
  BlockSection,
2567
3161
  {
2568
3162
  block: child,
@@ -2577,139 +3171,6 @@ function BlockSection({ block, basePath, viewport, renderContext, blockIndex })
2577
3171
  }
2578
3172
  );
2579
3173
  }
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
3174
  function LinearDocView({
2714
3175
  doc,
2715
3176
  basePath = "/",
@@ -2721,15 +3182,19 @@ function LinearDocView({
2721
3182
  imageDisplayMode = "inline"
2722
3183
  }) {
2723
3184
  const activeViewport = viewport ?? VIEWPORT_PRESETS3.landscape;
2724
- const totalBlocks = useMemo6(() => countAll(doc.blocks), [doc.blocks]);
3185
+ const totalBlocks = useMemo8(() => countAll(doc.blocks), [doc.blocks]);
2725
3186
  const autoSurface = useAutoSurface(surface === "auto");
2726
3187
  const resolvedSurface = surface === "auto" ? autoSurface : surface;
2727
- const renderContext = useMemo6(() => {
2728
- const baseTheme = theme ?? DEFAULT_THEME;
3188
+ const renderContext = useMemo8(() => {
3189
+ const baseTheme = theme ?? DEFAULT_THEME2;
3190
+ const effectiveTheme = resolvedSurface ? applySurface(baseTheme, resolvedSurface) : baseTheme;
2729
3191
  return {
2730
- theme: resolvedSurface ? applySurface(baseTheme, resolvedSurface) : baseTheme,
3192
+ theme: effectiveTheme,
2731
3193
  viewport: activeViewport,
2732
- totalBlocks
3194
+ totalBlocks,
3195
+ // Theme atmosphere (vignette/grain/gradient persistent layers) shows
3196
+ // on the inline template cards so they match the player's look.
3197
+ persistentLayers: effectiveTheme.persistentLayers
2733
3198
  };
2734
3199
  }, [activeViewport, totalBlocks, theme, resolvedSurface]);
2735
3200
  const activeTheme = renderContext.theme;
@@ -2740,7 +3205,7 @@ function LinearDocView({
2740
3205
  const bodyFont = resolveFontFamily2(activeTheme.typography.bodyFont, "system-ui, sans-serif");
2741
3206
  const titleFont = resolveFontFamily2(activeTheme.typography.titleFont, "Georgia, serif");
2742
3207
  const lineHt = activeTheme.typography.lineHeight ?? 1.7;
2743
- return /* @__PURE__ */ jsx16(
3208
+ return /* @__PURE__ */ jsx19(
2744
3209
  "div",
2745
3210
  {
2746
3211
  className: `squisq-linear ${className || ""}`,
@@ -2756,7 +3221,7 @@ function LinearDocView({
2756
3221
  overflowX: "hidden",
2757
3222
  background: bgColor
2758
3223
  },
2759
- children: /* @__PURE__ */ jsxs10(
3224
+ children: /* @__PURE__ */ jsxs12(
2760
3225
  "div",
2761
3226
  {
2762
3227
  className: `squisq-linear-content squisq-md${thinMargins ? " squisq-linear-content--thin" : ""}${imageDisplayMode === "thumbnail" ? " squisq-linear-content--thumbnail-images" : ""}`,
@@ -2781,7 +3246,7 @@ function LinearDocView({
2781
3246
  "--squisq-linear-bg": bgColor
2782
3247
  },
2783
3248
  children: [
2784
- /* @__PURE__ */ jsx16("style", { children: `
3249
+ /* @__PURE__ */ jsx19("style", { children: `
2785
3250
  .squisq-linear-content h1,
2786
3251
  .squisq-linear-content h2,
2787
3252
  .squisq-linear-content h3,
@@ -2893,7 +3358,7 @@ function LinearDocView({
2893
3358
  background: color-mix(in srgb, var(--squisq-linear-primary) 8%, transparent);
2894
3359
  }
2895
3360
  ` }),
2896
- doc.blocks.map((block, i) => /* @__PURE__ */ jsx16(
3361
+ doc.blocks.map((block, i) => /* @__PURE__ */ jsx19(
2897
3362
  BlockSection,
2898
3363
  {
2899
3364
  block,
@@ -2912,7 +3377,7 @@ function LinearDocView({
2912
3377
  }
2913
3378
 
2914
3379
  // src/DocPlayer.tsx
2915
- import { jsx as jsx17, jsxs as jsxs11 } from "react/jsx-runtime";
3380
+ import { jsx as jsx20, jsxs as jsxs13 } from "react/jsx-runtime";
2916
3381
  var SMALL_WORDS = /* @__PURE__ */ new Set([
2917
3382
  "a",
2918
3383
  "an",
@@ -2984,13 +3449,13 @@ function DocPlayer({
2984
3449
  }) {
2985
3450
  const isSlideshowMode = displayMode === "slideshow";
2986
3451
  const isLinearMode = displayMode === "linear";
2987
- const audioRef = useRef5(null);
2988
- const containerRef = useRef5(null);
2989
- const [tapFeedback, setTapFeedback] = useState7(null);
2990
- const tapFeedbackTimer = useRef5();
3452
+ const audioRef = useRef6(null);
3453
+ const containerRef = useRef6(null);
3454
+ const [tapFeedback, setTapFeedback] = useState6(null);
3455
+ const tapFeedbackTimer = useRef6();
2991
3456
  const { viewport, orientation } = useViewportOrientation();
2992
3457
  const activeViewport = forceViewport || (renderMode ? VIEWPORT_PRESETS4.landscape : viewport);
2993
- const isDebugMode = useMemo7(() => {
3458
+ const isDebugMode = useMemo9(() => {
2994
3459
  if (typeof window === "undefined") return false;
2995
3460
  const params = new URLSearchParams(window.location.search);
2996
3461
  return params.get("debug") === "true";
@@ -3013,11 +3478,12 @@ function DocPlayer({
3013
3478
  skipToSegment: _skipToSegment,
3014
3479
  restart
3015
3480
  } = audio;
3016
- const currentTimeRef = useRef5(currentTime);
3481
+ const mediaSchedule = useMemo9(() => resolveMediaSchedule(script), [script]);
3482
+ const currentTimeRef = useRef6(currentTime);
3017
3483
  currentTimeRef.current = currentTime;
3018
- const totalDurationRef = useRef5(totalDuration);
3484
+ const totalDurationRef = useRef6(totalDuration);
3019
3485
  totalDurationRef.current = totalDuration;
3020
- const expandedBlocksLenRef = useRef5(0);
3486
+ const expandedBlocksLenRef = useRef6(0);
3021
3487
  const handleContainerClick = useCallback5(
3022
3488
  (e) => {
3023
3489
  if (renderMode || isSlideshowMode || isLinearMode) return;
@@ -3036,8 +3502,8 @@ function DocPlayer({
3036
3502
  );
3037
3503
  const autoSurface = useAutoSurface(surface === "auto");
3038
3504
  const resolvedSurface = surface === "auto" ? autoSurface : surface;
3039
- const effectiveTheme = useMemo7(() => {
3040
- const base = theme ?? DEFAULT_THEME2;
3505
+ const effectiveTheme = useMemo9(() => {
3506
+ const base = theme ?? DEFAULT_THEME3;
3041
3507
  return resolvedSurface ? applySurface2(base, resolvedSurface) : base;
3042
3508
  }, [theme, resolvedSurface]);
3043
3509
  const {
@@ -3053,7 +3519,7 @@ function DocPlayer({
3053
3519
  prevBlock: _prevBlock,
3054
3520
  blocks: expandedBlocks
3055
3521
  } = useDocPlayback(script, currentTime, activeViewport, renderMode, effectiveTheme);
3056
- const coverBlock = useMemo7(() => {
3522
+ const coverBlock = useMemo9(() => {
3057
3523
  const startBlockConfig = script.startBlock;
3058
3524
  if (!startBlockConfig) return null;
3059
3525
  const context = createTemplateContext(effectiveTheme, 0, 1, activeViewport);
@@ -3068,11 +3534,11 @@ function DocPlayer({
3068
3534
  layers
3069
3535
  };
3070
3536
  }, [script.startBlock, activeViewport, effectiveTheme]);
3071
- const [coverForced, setCoverForced] = useState7(false);
3072
- const [coverGraceActive, setCoverGraceActive] = useState7(false);
3073
- const coverGraceTimer = useRef5();
3074
- const coverWasShowing = useRef5(false);
3075
- const hasPlayedOnce = useRef5(false);
3537
+ const [coverForced, setCoverForced] = useState6(false);
3538
+ const [coverGraceActive, setCoverGraceActive] = useState6(false);
3539
+ const coverGraceTimer = useRef6();
3540
+ const coverWasShowing = useRef6(false);
3541
+ const hasPlayedOnce = useRef6(false);
3076
3542
  const atRest = !!(coverBlock && !isPlaying && currentTime === 0 && !hasPlayedOnce.current && !renderMode && !autoPlay);
3077
3543
  if (atRest) coverWasShowing.current = true;
3078
3544
  useEffect7(() => {
@@ -3085,7 +3551,7 @@ function DocPlayer({
3085
3551
  }, [isPlaying, coverBlock, renderMode]);
3086
3552
  useEffect7(() => () => clearTimeout(coverGraceTimer.current), []);
3087
3553
  const showCoverBlock = !isSlideshowMode && !isLinearMode && coverBlock && (coverForced || coverGraceActive || !isPlaying && currentTime === 0 && !hasPlayedOnce.current && !renderMode && !autoPlay);
3088
- const hasAutoPlayed = useRef5(false);
3554
+ const hasAutoPlayed = useRef6(false);
3089
3555
  useEffect7(() => {
3090
3556
  if (isAudioReady && autoPlay && !hasAutoPlayed.current) {
3091
3557
  hasAutoPlayed.current = true;
@@ -3133,7 +3599,11 @@ function DocPlayer({
3133
3599
  const video = el;
3134
3600
  const clipStart = parseFloat(video.dataset.clipStart || "0");
3135
3601
  const clipEnd = parseFloat(video.dataset.clipEnd || "0");
3136
- const targetTime = Math.min(clipStart + Math.max(0, blockElapsed), clipEnd);
3602
+ const startAt = parseFloat(video.dataset.startAt || "0");
3603
+ const targetTime = Math.min(
3604
+ clipStart + Math.max(0, blockElapsed - startAt),
3605
+ clipEnd
3606
+ );
3137
3607
  video.pause();
3138
3608
  video.currentTime = targetTime;
3139
3609
  videoSeekPromises.push(
@@ -3148,6 +3618,26 @@ function DocPlayer({
3148
3618
  );
3149
3619
  });
3150
3620
  }
3621
+ document.querySelectorAll("video[data-clip-id]").forEach((el) => {
3622
+ const video = el;
3623
+ const absStart = parseFloat(video.dataset.absStart || "0");
3624
+ const absEnd = parseFloat(video.dataset.absEnd || "0");
3625
+ const sourceIn = parseFloat(video.dataset.sourceIn || "0");
3626
+ video.pause();
3627
+ if (time < absStart || time >= absEnd) return;
3628
+ const targetTime = sourceIn + (time - absStart);
3629
+ video.currentTime = targetTime;
3630
+ videoSeekPromises.push(
3631
+ new Promise((r) => {
3632
+ if (Math.abs(video.currentTime - targetTime) < 0.1) {
3633
+ r();
3634
+ } else {
3635
+ video.addEventListener("seeked", () => r(), { once: true });
3636
+ setTimeout(r, 200);
3637
+ }
3638
+ })
3639
+ );
3640
+ });
3151
3641
  Promise.all(videoSeekPromises).then(() => {
3152
3642
  requestAnimationFrame(() => resolve());
3153
3643
  });
@@ -3155,12 +3645,9 @@ function DocPlayer({
3155
3645
  });
3156
3646
  };
3157
3647
  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;
3648
+ const mediaDuration = getDocPlaybackDuration(script);
3649
+ if (totalDuration > 0) return Math.max(totalDuration, mediaDuration);
3650
+ return mediaDuration;
3164
3651
  };
3165
3652
  w.getBlocks = () => expandedBlocks.map((s) => ({
3166
3653
  id: s.id,
@@ -3213,7 +3700,7 @@ function DocPlayer({
3213
3700
  };
3214
3701
  }, [renderMode, isDebugMode, seekTo, totalDuration, expandedBlocks, coverBlock]);
3215
3702
  const defaultMode = captionsEnabledProp === false ? "off" : captionStyle || "standard";
3216
- const [captionMode, setCaptionMode] = useState7(defaultMode);
3703
+ const [captionMode, setCaptionMode] = useState6(defaultMode);
3217
3704
  const captionsEnabled = captionMode !== "off";
3218
3705
  const activeCaptionStyle = captionMode === "social" ? "social" : "standard";
3219
3706
  const setCaptionsEnabled = useCallback5(
@@ -3231,8 +3718,8 @@ function DocPlayer({
3231
3718
  });
3232
3719
  }, [onCaptionsToggle]);
3233
3720
  const hasCaptions = script.captions && script.captions.phrases.length > 0;
3234
- const segmentTitleMap = useMemo7(() => buildSegmentTitleMap(script), [script]);
3235
- const playbackState = useMemo7(
3721
+ const segmentTitleMap = useMemo9(() => buildSegmentTitleMap(script), [script]);
3722
+ const playbackState = useMemo9(
3236
3723
  () => ({
3237
3724
  isPlaying,
3238
3725
  currentTime,
@@ -3265,7 +3752,7 @@ function DocPlayer({
3265
3752
  currentBlock
3266
3753
  ]
3267
3754
  );
3268
- const playbackActions = useMemo7(
3755
+ const playbackActions = useMemo9(
3269
3756
  () => ({
3270
3757
  toggle,
3271
3758
  restart,
@@ -3276,7 +3763,7 @@ function DocPlayer({
3276
3763
  }),
3277
3764
  [toggle, restart, seekTo, setCaptionsEnabled, cycleCaptionMode, onFullscreenToggle]
3278
3765
  );
3279
- const slideNavActions = useMemo7(
3766
+ const slideNavActions = useMemo9(
3280
3767
  () => ({
3281
3768
  nextSlide: () => {
3282
3769
  if (currentBlockIndex < expandedBlocks.length - 1) {
@@ -3338,7 +3825,7 @@ function DocPlayer({
3338
3825
  }
3339
3826
  return block.id.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
3340
3827
  }, []);
3341
- const blockMarkers = useMemo7(() => {
3828
+ const blockMarkers = useMemo9(() => {
3342
3829
  if (!totalDuration || !expandedBlocks.length) return [];
3343
3830
  let prevSegment = -1;
3344
3831
  return expandedBlocks.map((block, index) => {
@@ -3411,7 +3898,7 @@ function DocPlayer({
3411
3898
  return () => window.removeEventListener("keydown", handleKeyDown);
3412
3899
  }, [handleKeyDown, renderMode]);
3413
3900
  if (isLinearMode) {
3414
- return /* @__PURE__ */ jsx17(
3901
+ return /* @__PURE__ */ jsx20(
3415
3902
  "div",
3416
3903
  {
3417
3904
  ref: containerRef,
@@ -3422,7 +3909,7 @@ function DocPlayer({
3422
3909
  height: "100%",
3423
3910
  overflow: "hidden"
3424
3911
  },
3425
- children: /* @__PURE__ */ jsx17(
3912
+ children: /* @__PURE__ */ jsx20(
3426
3913
  LinearDocView,
3427
3914
  {
3428
3915
  doc: script,
@@ -3435,7 +3922,7 @@ function DocPlayer({
3435
3922
  }
3436
3923
  );
3437
3924
  }
3438
- return /* @__PURE__ */ jsxs11(
3925
+ return /* @__PURE__ */ jsxs13(
3439
3926
  "div",
3440
3927
  {
3441
3928
  ref: containerRef,
@@ -3450,9 +3937,19 @@ function DocPlayer({
3450
3937
  cursor: renderMode ? void 0 : "pointer"
3451
3938
  },
3452
3939
  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(
3940
+ /* @__PURE__ */ jsx20("audio", { ref: audioRef, preload: "auto", muted }),
3941
+ /* @__PURE__ */ jsx20(
3942
+ MediaClipLayer,
3943
+ {
3944
+ schedule: mediaSchedule,
3945
+ currentTime,
3946
+ isPlaying,
3947
+ basePath,
3948
+ renderMode
3949
+ }
3950
+ ),
3951
+ /* @__PURE__ */ jsxs13("div", { className: "doc-player__viewport", children: [
3952
+ showCoverBlock && coverBlock && /* @__PURE__ */ jsx20("div", { className: "doc-player__block doc-player__block--cover", children: /* @__PURE__ */ jsx20(
3456
3953
  BlockRenderer,
3457
3954
  {
3458
3955
  block: coverBlock,
@@ -3462,17 +3959,22 @@ function DocPlayer({
3462
3959
  viewport: activeViewport
3463
3960
  }
3464
3961
  ) }),
3465
- !showCoverBlock && previousBlock && isExiting && /* @__PURE__ */ jsx17("div", { className: "doc-player__block doc-player__block--previous", children: /* @__PURE__ */ jsx17(
3962
+ !showCoverBlock && previousBlock && isExiting && // Keyed by block id so each block is its own DOM subtree: React never
3963
+ // reconciles one block's layers onto another's (templates reuse layer
3964
+ // ids like `title`/`background`), which would otherwise reuse stale
3965
+ // DOM / skip entrance animations mid-transition.
3966
+ /* @__PURE__ */ jsx20("div", { className: "doc-player__block doc-player__block--previous", children: /* @__PURE__ */ jsx20(
3466
3967
  BlockRenderer,
3467
3968
  {
3468
3969
  block: previousBlock,
3469
3970
  blockTime,
3470
3971
  basePath,
3471
3972
  isExiting: true,
3973
+ transition: currentBlock?.transition,
3472
3974
  viewport: activeViewport
3473
3975
  }
3474
- ) }),
3475
- !showCoverBlock && currentBlock && /* @__PURE__ */ jsx17("div", { className: "doc-player__block doc-player__block--active", children: /* @__PURE__ */ jsx17(
3976
+ ) }, previousBlock.id),
3977
+ !showCoverBlock && currentBlock && /* @__PURE__ */ jsx20("div", { className: "doc-player__block doc-player__block--active", children: /* @__PURE__ */ jsx20(
3476
3978
  BlockRenderer,
3477
3979
  {
3478
3980
  block: currentBlock,
@@ -3482,8 +3984,8 @@ function DocPlayer({
3482
3984
  viewport: activeViewport,
3483
3985
  isPlaying
3484
3986
  }
3485
- ) }),
3486
- hasCaptions && (renderMode ? captionsEnabled : true) && /* @__PURE__ */ jsx17(
3987
+ ) }, currentBlock.id),
3988
+ hasCaptions && (renderMode ? captionsEnabled : true) && /* @__PURE__ */ jsx20(
3487
3989
  CaptionOverlay,
3488
3990
  {
3489
3991
  captions: script.captions,
@@ -3495,7 +3997,7 @@ function DocPlayer({
3495
3997
  viewport: activeViewport
3496
3998
  }
3497
3999
  ),
3498
- isDebugMode && /* @__PURE__ */ jsxs11(
4000
+ isDebugMode && /* @__PURE__ */ jsxs13(
3499
4001
  "div",
3500
4002
  {
3501
4003
  className: "doc-player__debug",
@@ -3516,27 +4018,27 @@ function DocPlayer({
3516
4018
  textAlign: "left"
3517
4019
  },
3518
4020
  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:" }),
4021
+ /* @__PURE__ */ jsx20("div", { style: { color: "#ffcc00", fontWeight: "bold", marginBottom: "4px" }, children: "DEBUG MODE" }),
4022
+ /* @__PURE__ */ jsxs13("div", { children: [
4023
+ /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "template:" }),
3522
4024
  " ",
3523
- /* @__PURE__ */ jsx17("span", { style: { color: "#ff6b6b" }, children: currentBlock?.template ?? "raw" })
4025
+ /* @__PURE__ */ jsx20("span", { style: { color: "#ff6b6b" }, children: currentBlock?.template ?? "raw" })
3524
4026
  ] }),
3525
- /* @__PURE__ */ jsxs11("div", { children: [
3526
- /* @__PURE__ */ jsx17("span", { style: { color: "#888" }, children: "block:" }),
4027
+ /* @__PURE__ */ jsxs13("div", { children: [
4028
+ /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "block:" }),
3527
4029
  " ",
3528
4030
  currentBlockIndex + 1,
3529
4031
  "/",
3530
4032
  expandedBlocks.length,
3531
4033
  " ",
3532
- /* @__PURE__ */ jsxs11("span", { style: { color: "#666" }, children: [
4034
+ /* @__PURE__ */ jsxs13("span", { style: { color: "#666" }, children: [
3533
4035
  "(",
3534
4036
  currentBlock?.id || "none",
3535
4037
  ")"
3536
4038
  ] })
3537
4039
  ] }),
3538
- /* @__PURE__ */ jsxs11("div", { children: [
3539
- /* @__PURE__ */ jsx17("span", { style: { color: "#888" }, children: "time:" }),
4040
+ /* @__PURE__ */ jsxs13("div", { children: [
4041
+ /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "time:" }),
3540
4042
  " ",
3541
4043
  currentTime.toFixed(2),
3542
4044
  "s /",
@@ -3544,7 +4046,7 @@ function DocPlayer({
3544
4046
  totalDuration.toFixed(1),
3545
4047
  "s",
3546
4048
  " ",
3547
- /* @__PURE__ */ jsxs11("span", { style: { color: "#666" }, children: [
4049
+ /* @__PURE__ */ jsxs13("span", { style: { color: "#666" }, children: [
3548
4050
  "(progress: ",
3549
4051
  (docProgress * 100).toFixed(1),
3550
4052
  "%, scriptDur:",
@@ -3553,8 +4055,8 @@ function DocPlayer({
3553
4055
  ")"
3554
4056
  ] })
3555
4057
  ] }),
3556
- /* @__PURE__ */ jsxs11("div", { children: [
3557
- /* @__PURE__ */ jsx17("span", { style: { color: "#888" }, children: "blockTime:" }),
4058
+ /* @__PURE__ */ jsxs13("div", { children: [
4059
+ /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "blockTime:" }),
3558
4060
  " ",
3559
4061
  blockTime.toFixed(2),
3560
4062
  "s /",
@@ -3562,58 +4064,58 @@ function DocPlayer({
3562
4064
  (currentBlock?.duration || 0).toFixed(1),
3563
4065
  "s"
3564
4066
  ] }),
3565
- /* @__PURE__ */ jsxs11("div", { children: [
3566
- /* @__PURE__ */ jsx17("span", { style: { color: "#888" }, children: "segment:" }),
4067
+ /* @__PURE__ */ jsxs13("div", { children: [
4068
+ /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "segment:" }),
3567
4069
  " ",
3568
4070
  currentSegment,
3569
4071
  "/",
3570
4072
  script.audio.segments.length - 1,
3571
4073
  " ",
3572
- /* @__PURE__ */ jsxs11("span", { style: { color: "#666" }, children: [
4074
+ /* @__PURE__ */ jsxs13("span", { style: { color: "#666" }, children: [
3573
4075
  "(",
3574
4076
  script.audio.segments[currentSegment]?.name || "none",
3575
4077
  ")"
3576
4078
  ] })
3577
4079
  ] }),
3578
- /* @__PURE__ */ jsxs11("div", { children: [
3579
- /* @__PURE__ */ jsx17("span", { style: { color: "#888" }, children: "viewport:" }),
4080
+ /* @__PURE__ */ jsxs13("div", { children: [
4081
+ /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "viewport:" }),
3580
4082
  " ",
3581
4083
  activeViewport.name || `${activeViewport.width}x${activeViewport.height}`,
3582
4084
  " ",
3583
- /* @__PURE__ */ jsxs11("span", { style: { color: "#666" }, children: [
4085
+ /* @__PURE__ */ jsxs13("span", { style: { color: "#666" }, children: [
3584
4086
  "(",
3585
4087
  orientation,
3586
4088
  ")"
3587
4089
  ] })
3588
4090
  ] }),
3589
- /* @__PURE__ */ jsxs11("div", { children: [
3590
- /* @__PURE__ */ jsx17("span", { style: { color: "#888" }, children: "playing:" }),
4091
+ /* @__PURE__ */ jsxs13("div", { children: [
4092
+ /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "playing:" }),
3591
4093
  " ",
3592
- /* @__PURE__ */ jsx17("span", { style: { color: isPlaying ? "#4ade80" : "#f87171" }, children: isPlaying ? "yes" : "no" }),
3593
- showCoverBlock && /* @__PURE__ */ jsx17("span", { style: { color: "#60a5fa" }, children: " (cover)" })
4094
+ /* @__PURE__ */ jsx20("span", { style: { color: isPlaying ? "#4ade80" : "#f87171" }, children: isPlaying ? "yes" : "no" }),
4095
+ showCoverBlock && /* @__PURE__ */ jsx20("span", { style: { color: "#60a5fa" }, children: " (cover)" })
3594
4096
  ] }),
3595
4097
  hasCaptions && (() => {
3596
4098
  const debugPhrase = getCaptionAtTime2(script.captions, currentTime);
3597
4099
  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:" }),
4100
+ return /* @__PURE__ */ jsxs13(Fragment3, { children: [
4101
+ /* @__PURE__ */ jsxs13("div", { children: [
4102
+ /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "captions:" }),
3601
4103
  " ",
3602
4104
  script.captions?.phrases.length || 0,
3603
4105
  " phrases",
3604
4106
  " ",
3605
- /* @__PURE__ */ jsxs11("span", { style: { color: captionsEnabled ? "#4ade80" : "#666" }, children: [
4107
+ /* @__PURE__ */ jsxs13("span", { style: { color: captionsEnabled ? "#4ade80" : "#666" }, children: [
3606
4108
  "(",
3607
4109
  captionsEnabled ? "on" : "off",
3608
4110
  ")"
3609
4111
  ] })
3610
4112
  ] }),
3611
- /* @__PURE__ */ jsxs11("div", { children: [
3612
- /* @__PURE__ */ jsx17("span", { style: { color: "#888" }, children: "cc.enabled:" }),
4113
+ /* @__PURE__ */ jsxs13("div", { children: [
4114
+ /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "cc.enabled:" }),
3613
4115
  " ",
3614
- /* @__PURE__ */ jsx17("span", { style: { color: debugEnabled ? "#4ade80" : "#f87171" }, children: String(debugEnabled) }),
4116
+ /* @__PURE__ */ jsx20("span", { style: { color: debugEnabled ? "#4ade80" : "#f87171" }, children: String(debugEnabled) }),
3615
4117
  " ",
3616
- /* @__PURE__ */ jsxs11("span", { style: { color: "#666" }, children: [
4118
+ /* @__PURE__ */ jsxs13("span", { style: { color: "#666" }, children: [
3617
4119
  "(playing=",
3618
4120
  String(isPlaying),
3619
4121
  " t>0=",
@@ -3621,15 +4123,15 @@ function DocPlayer({
3621
4123
  ")"
3622
4124
  ] })
3623
4125
  ] }),
3624
- /* @__PURE__ */ jsxs11("div", { children: [
3625
- /* @__PURE__ */ jsx17("span", { style: { color: "#888" }, children: "cc.phrase:" }),
4126
+ /* @__PURE__ */ jsxs13("div", { children: [
4127
+ /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "cc.phrase:" }),
3626
4128
  " ",
3627
- /* @__PURE__ */ jsx17("span", { style: { color: debugPhrase ? "#4ade80" : "#f87171" }, children: debugPhrase ? `"${debugPhrase.text.slice(0, 30)}..."` : "null" })
4129
+ /* @__PURE__ */ jsx20("span", { style: { color: debugPhrase ? "#4ade80" : "#f87171" }, children: debugPhrase ? `"${debugPhrase.text.slice(0, 30)}..."` : "null" })
3628
4130
  ] }),
3629
- debugPhrase && /* @__PURE__ */ jsxs11("div", { children: [
3630
- /* @__PURE__ */ jsx17("span", { style: { color: "#888" }, children: "cc.range:" }),
4131
+ debugPhrase && /* @__PURE__ */ jsxs13("div", { children: [
4132
+ /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "cc.range:" }),
3631
4133
  " ",
3632
- /* @__PURE__ */ jsxs11("span", { style: { color: "#60a5fa" }, children: [
4134
+ /* @__PURE__ */ jsxs13("span", { style: { color: "#60a5fa" }, children: [
3633
4135
  debugPhrase.startTime.toFixed(2),
3634
4136
  "-",
3635
4137
  debugPhrase.endTime.toFixed(2)
@@ -3641,7 +4143,7 @@ function DocPlayer({
3641
4143
  }
3642
4144
  )
3643
4145
  ] }),
3644
- !isAvailable && unavailableMessage && /* @__PURE__ */ jsxs11(
4146
+ !isAvailable && unavailableMessage && /* @__PURE__ */ jsxs13(
3645
4147
  "div",
3646
4148
  {
3647
4149
  className: "doc-player__unavailable",
@@ -3662,12 +4164,12 @@ function DocPlayer({
3662
4164
  zIndex: 50
3663
4165
  },
3664
4166
  children: [
3665
- /* @__PURE__ */ jsx17("span", { style: { fontSize: "32px" }, children: "\u{1F50A}" }),
3666
- /* @__PURE__ */ jsx17("span", { children: unavailableMessage })
4167
+ /* @__PURE__ */ jsx20("span", { style: { fontSize: "32px" }, children: "\u{1F50A}" }),
4168
+ /* @__PURE__ */ jsx20("span", { children: unavailableMessage })
3667
4169
  ]
3668
4170
  }
3669
4171
  ),
3670
- !renderMode && !isSlideshowMode && showControls && /* @__PURE__ */ jsx17(
4172
+ !renderMode && !isSlideshowMode && showControls && /* @__PURE__ */ jsx20(
3671
4173
  DocControlsOverlay,
3672
4174
  {
3673
4175
  state: playbackState,
@@ -3677,7 +4179,7 @@ function DocPlayer({
3677
4179
  getBlockTitle
3678
4180
  }
3679
4181
  ),
3680
- !renderMode && !isSlideshowMode && !showControls && showScrubber && /* @__PURE__ */ jsx17(
4182
+ !renderMode && !isSlideshowMode && !showControls && showScrubber && /* @__PURE__ */ jsx20(
3681
4183
  "div",
3682
4184
  {
3683
4185
  className: "doc-player__scrubber",
@@ -3692,7 +4194,7 @@ function DocPlayer({
3692
4194
  alignItems: "center",
3693
4195
  zIndex: 100
3694
4196
  },
3695
- children: /* @__PURE__ */ jsx17(
4197
+ children: /* @__PURE__ */ jsx20(
3696
4198
  DocProgressBar,
3697
4199
  {
3698
4200
  state: playbackState,
@@ -3704,15 +4206,15 @@ function DocPlayer({
3704
4206
  )
3705
4207
  }
3706
4208
  ),
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())
4209
+ !renderMode && isSlideshowMode && /* @__PURE__ */ jsx20(DocControlsSlideshow, { state: playbackState, slideNav: slideNavActions }),
4210
+ !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
4211
  ]
3710
4212
  }
3711
4213
  );
3712
4214
  }
3713
4215
 
3714
4216
  // src/DocControlsBottom.tsx
3715
- import { jsx as jsx18, jsxs as jsxs12 } from "react/jsx-runtime";
4217
+ import { jsx as jsx21, jsxs as jsxs14 } from "react/jsx-runtime";
3716
4218
  function DocControlsBottom({
3717
4219
  state,
3718
4220
  actions,
@@ -3720,32 +4222,32 @@ function DocControlsBottom({
3720
4222
  expandedBlocks,
3721
4223
  getBlockTitle
3722
4224
  }) {
3723
- return /* @__PURE__ */ jsxs12("div", { className: "doc-controls-bottom", children: [
3724
- /* @__PURE__ */ jsx18(
4225
+ return /* @__PURE__ */ jsxs14("div", { className: "doc-controls-bottom", children: [
4226
+ /* @__PURE__ */ jsx21(
3725
4227
  "button",
3726
4228
  {
3727
4229
  className: "bottom-ctrl-btn",
3728
4230
  onClick: actions.restart,
3729
4231
  title: "Restart",
3730
4232
  "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" }) })
4233
+ 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
4234
  }
3733
4235
  ),
3734
- /* @__PURE__ */ jsx18(
4236
+ /* @__PURE__ */ jsx21(
3735
4237
  "button",
3736
4238
  {
3737
4239
  className: "bottom-ctrl-btn bottom-play-btn",
3738
4240
  onClick: actions.toggle,
3739
4241
  "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" }) })
4242
+ 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
4243
  }
3742
4244
  ),
3743
- /* @__PURE__ */ jsxs12("span", { className: "bottom-time", children: [
4245
+ /* @__PURE__ */ jsxs14("span", { className: "bottom-time", children: [
3744
4246
  formatTime(state.currentTime),
3745
4247
  " / ",
3746
4248
  formatTime(state.totalDuration)
3747
4249
  ] }),
3748
- /* @__PURE__ */ jsx18(
4250
+ /* @__PURE__ */ jsx21(
3749
4251
  DocProgressBar,
3750
4252
  {
3751
4253
  state,
@@ -3755,82 +4257,82 @@ function DocControlsBottom({
3755
4257
  getBlockTitle
3756
4258
  }
3757
4259
  ),
3758
- /* @__PURE__ */ jsxs12("span", { className: "bottom-segment", children: [
4260
+ /* @__PURE__ */ jsxs14("span", { className: "bottom-segment", children: [
3759
4261
  state.currentBlockIndex + 1,
3760
4262
  "/",
3761
4263
  state.totalBlocks
3762
4264
  ] }),
3763
- state.hasCaptions && /* @__PURE__ */ jsx18(
4265
+ state.hasCaptions && /* @__PURE__ */ jsx21(
3764
4266
  "button",
3765
4267
  {
3766
4268
  className: `bottom-ctrl-btn ${state.captionMode !== "off" ? "bottom-ctrl-btn--active" : ""}`,
3767
4269
  onClick: () => actions.cycleCaptionMode(),
3768
4270
  title: state.captionMode === "off" ? "Captions: Off" : state.captionMode === "standard" ? "Captions: Standard" : "Captions: Social",
3769
4271
  "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" }) })
4272
+ 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
4273
  }
3772
4274
  )
3773
4275
  ] });
3774
4276
  }
3775
4277
 
3776
4278
  // src/DocControlsSidebar.tsx
3777
- import { jsx as jsx19, jsxs as jsxs13 } from "react/jsx-runtime";
4279
+ import { jsx as jsx22, jsxs as jsxs15 } from "react/jsx-runtime";
3778
4280
  function DocControlsSidebar({ state, actions }) {
3779
- return /* @__PURE__ */ jsxs13("div", { className: "doc-controls-sidebar", children: [
3780
- /* @__PURE__ */ jsx19(
4281
+ return /* @__PURE__ */ jsxs15("div", { className: "doc-controls-sidebar", children: [
4282
+ /* @__PURE__ */ jsx22(
3781
4283
  "button",
3782
4284
  {
3783
4285
  className: "sidebar-ctrl-btn",
3784
4286
  onClick: actions.restart,
3785
4287
  title: "Restart",
3786
4288
  "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" }) })
4289
+ 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
4290
  }
3789
4291
  ),
3790
- /* @__PURE__ */ jsx19(
4292
+ /* @__PURE__ */ jsx22(
3791
4293
  "button",
3792
4294
  {
3793
4295
  className: "sidebar-ctrl-btn sidebar-play-btn",
3794
4296
  onClick: actions.toggle,
3795
4297
  "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" }) })
4298
+ 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
4299
  }
3798
4300
  ),
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) })
4301
+ /* @__PURE__ */ jsxs15("div", { className: "sidebar-time", children: [
4302
+ /* @__PURE__ */ jsx22("div", { children: formatTime(state.currentTime) }),
4303
+ /* @__PURE__ */ jsx22("div", { className: "sidebar-time-total", children: formatTime(state.totalDuration) })
3802
4304
  ] }),
3803
- /* @__PURE__ */ jsxs13("div", { className: "sidebar-segment", children: [
4305
+ /* @__PURE__ */ jsxs15("div", { className: "sidebar-segment", children: [
3804
4306
  state.currentBlockIndex + 1,
3805
4307
  "/",
3806
4308
  state.totalBlocks
3807
4309
  ] }),
3808
- state.hasCaptions && /* @__PURE__ */ jsx19(
4310
+ state.hasCaptions && /* @__PURE__ */ jsx22(
3809
4311
  "button",
3810
4312
  {
3811
4313
  className: `sidebar-ctrl-btn ${state.captionMode !== "off" ? "sidebar-ctrl-btn--active" : ""}`,
3812
4314
  onClick: () => actions.cycleCaptionMode(),
3813
4315
  title: state.captionMode === "off" ? "Captions: Off" : state.captionMode === "standard" ? "Captions: Standard" : "Captions: Social",
3814
4316
  "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" }) })
4317
+ 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
4318
  }
3817
4319
  ),
3818
- actions.toggleFullscreen && /* @__PURE__ */ jsx19(
4320
+ actions.toggleFullscreen && /* @__PURE__ */ jsx22(
3819
4321
  "button",
3820
4322
  {
3821
4323
  className: `sidebar-ctrl-btn ${state.isFullscreen ? "sidebar-ctrl-btn--active" : ""}`,
3822
4324
  onClick: actions.toggleFullscreen,
3823
4325
  title: state.isFullscreen ? "Exit fullscreen (F)" : "Fullscreen (F)",
3824
4326
  "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" }) })
4327
+ 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
4328
  }
3827
4329
  )
3828
4330
  ] });
3829
4331
  }
3830
4332
 
3831
4333
  // 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";
4334
+ import { useRef as useRef7, useState as useState7, useCallback as useCallback6, useEffect as useEffect8 } from "react";
4335
+ import { jsx as jsx23, jsxs as jsxs16 } from "react/jsx-runtime";
3834
4336
  var DEFAULT_STATE = {
3835
4337
  isPlaying: false,
3836
4338
  currentTime: 0,
@@ -3859,10 +4361,10 @@ function DocPlayerWithSidebar({
3859
4361
  forceViewport,
3860
4362
  onPlayingChange
3861
4363
  }) {
3862
- const stateRef = useRef6(DEFAULT_STATE);
3863
- const actionsRef = useRef6(null);
3864
- const wasPlayingRef = useRef6(false);
3865
- const [, setTick] = useState8(0);
4364
+ const stateRef = useRef7(DEFAULT_STATE);
4365
+ const actionsRef = useRef7(null);
4366
+ const wasPlayingRef = useRef7(false);
4367
+ const [, setTick] = useState7(0);
3866
4368
  const handleStateChange = useCallback6(
3867
4369
  (state) => {
3868
4370
  stateRef.current = state;
@@ -3887,8 +4389,8 @@ function DocPlayerWithSidebar({
3887
4389
  }, 250);
3888
4390
  return () => clearInterval(interval);
3889
4391
  }, []);
3890
- return /* @__PURE__ */ jsxs14("div", { className: "doc-player-sidebar-layout", children: [
3891
- /* @__PURE__ */ jsx20("div", { className: "doc-player-sidebar-layout__video", children: /* @__PURE__ */ jsx20(
4392
+ return /* @__PURE__ */ jsxs16("div", { className: "doc-player-sidebar-layout", children: [
4393
+ /* @__PURE__ */ jsx23("div", { className: "doc-player-sidebar-layout__video", children: /* @__PURE__ */ jsx23(
3892
4394
  DocPlayer,
3893
4395
  {
3894
4396
  script,
@@ -3908,22 +4410,22 @@ function DocPlayerWithSidebar({
3908
4410
  forceViewport
3909
4411
  }
3910
4412
  ) }),
3911
- actionsRef.current && /* @__PURE__ */ jsx20(DocControlsSidebar, { state: stateRef.current, actions: actionsRef.current })
4413
+ actionsRef.current && /* @__PURE__ */ jsx23(DocControlsSidebar, { state: stateRef.current, actions: actionsRef.current })
3912
4414
  ] });
3913
4415
  }
3914
4416
 
3915
4417
  // src/jsonView/useJsonViewTokens.ts
3916
- import { useMemo as useMemo8 } from "react";
4418
+ import { useMemo as useMemo10 } from "react";
3917
4419
  import {
3918
4420
  applySurface as applySurface3,
3919
4421
  resolveFontFamily as resolveFontFamily3
3920
4422
  } from "@bendyline/squisq/schemas";
3921
- import { DEFAULT_THEME as DEFAULT_THEME3 } from "@bendyline/squisq/doc";
4423
+ import { DEFAULT_THEME as DEFAULT_THEME4 } from "@bendyline/squisq/doc";
3922
4424
  function useJsonViewTokens(theme, surface) {
3923
4425
  const auto = useAutoSurface(surface === "auto");
3924
4426
  const effectiveSurface = surface === "auto" ? auto : surface ?? void 0;
3925
- return useMemo8(() => {
3926
- const baseTheme = theme ?? DEFAULT_THEME3;
4427
+ return useMemo10(() => {
4428
+ const baseTheme = theme ?? DEFAULT_THEME4;
3927
4429
  const finalTheme = effectiveSurface ? applySurface3(baseTheme, effectiveSurface) : baseTheme;
3928
4430
  const titleFont = resolveFontFamily3(finalTheme.typography.titleFont, "system-ui, sans-serif");
3929
4431
  const bodyFont = resolveFontFamily3(finalTheme.typography.bodyFont, "system-ui, sans-serif");
@@ -3955,26 +4457,26 @@ import {
3955
4457
  } from "@bendyline/squisq/jsonForm";
3956
4458
 
3957
4459
  // src/jsonView/viewers.tsx
3958
- import { Fragment as Fragment3, useMemo as useMemo9 } from "react";
4460
+ import { Fragment as Fragment4, useMemo as useMemo11 } from "react";
3959
4461
  import {
3960
4462
  arrayItemKind
3961
4463
  } from "@bendyline/squisq/jsonForm";
3962
4464
  import { parseMarkdown } from "@bendyline/squisq/markdown";
3963
- import { Fragment as Fragment4, jsx as jsx21, jsxs as jsxs15 } from "react/jsx-runtime";
4465
+ import { Fragment as Fragment5, jsx as jsx24, jsxs as jsxs17 } from "react/jsx-runtime";
3964
4466
  function TextViewer({ value }) {
3965
4467
  if (value === void 0 || value === null || value === "") {
3966
- return /* @__PURE__ */ jsx21("span", { className: "squisq-jv-empty", children: "\u2014" });
4468
+ return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-empty", children: "\u2014" });
3967
4469
  }
3968
- return /* @__PURE__ */ jsx21("span", { className: "squisq-jv-value", children: String(value) });
4470
+ return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-value", children: String(value) });
3969
4471
  }
3970
4472
  function MultilineViewer({ value }) {
3971
4473
  if (value === void 0 || value === null || value === "") {
3972
- return /* @__PURE__ */ jsx21("span", { className: "squisq-jv-empty", children: "\u2014" });
4474
+ return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-empty", children: "\u2014" });
3973
4475
  }
3974
- return /* @__PURE__ */ jsx21("span", { className: "squisq-jv-value squisq-jv-value--multiline", children: String(value) });
4476
+ return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-value squisq-jv-value--multiline", children: String(value) });
3975
4477
  }
3976
4478
  function RichTextViewer({ value }) {
3977
- const nodes = useMemo9(() => {
4479
+ const nodes = useMemo11(() => {
3978
4480
  if (typeof value !== "string" || value === "") return null;
3979
4481
  try {
3980
4482
  const doc = parseMarkdown(value);
@@ -3983,39 +4485,39 @@ function RichTextViewer({ value }) {
3983
4485
  return null;
3984
4486
  }
3985
4487
  }, [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 }) });
4488
+ if (!nodes) return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-empty", children: "\u2014" });
4489
+ return /* @__PURE__ */ jsx24("div", { className: "squisq-jv-richtext", children: /* @__PURE__ */ jsx24(MarkdownRenderer, { nodes }) });
3988
4490
  }
3989
4491
  function NumberViewer({ value }) {
3990
4492
  if (value === void 0 || value === null) {
3991
- return /* @__PURE__ */ jsx21("span", { className: "squisq-jv-empty", children: "\u2014" });
4493
+ return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-empty", children: "\u2014" });
3992
4494
  }
3993
- return /* @__PURE__ */ jsx21("span", { className: "squisq-jv-value", children: String(value) });
4495
+ return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-value", children: String(value) });
3994
4496
  }
3995
4497
  function BooleanViewer({ value }) {
3996
4498
  const on = Boolean(value);
3997
- return /* @__PURE__ */ jsx21("span", { className: `squisq-jv-toggle squisq-jv-toggle--${on ? "on" : "off"}`, children: on ? "On" : "Off" });
4499
+ return /* @__PURE__ */ jsx24("span", { className: `squisq-jv-toggle squisq-jv-toggle--${on ? "on" : "off"}`, children: on ? "On" : "Off" });
3998
4500
  }
3999
4501
  function EnumViewer({ value, schema }) {
4000
4502
  if (value === void 0 || value === null || value === "") {
4001
- return /* @__PURE__ */ jsx21("span", { className: "squisq-jv-empty", children: "\u2014" });
4503
+ return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-empty", children: "\u2014" });
4002
4504
  }
4003
4505
  const labels = schema.squisq?.enumLabels;
4004
4506
  const display = labels && typeof value === "string" ? labels[value] ?? value : String(value);
4005
- return /* @__PURE__ */ jsx21("span", { className: "squisq-jv-value", children: display });
4507
+ return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-value", children: display });
4006
4508
  }
4007
4509
  function ColorViewer({ value }) {
4008
4510
  if (typeof value !== "string" || value === "") {
4009
- return /* @__PURE__ */ jsx21("span", { className: "squisq-jv-empty", children: "\u2014" });
4511
+ return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-empty", children: "\u2014" });
4010
4512
  }
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 })
4513
+ return /* @__PURE__ */ jsxs17("span", { className: "squisq-jv-color", children: [
4514
+ /* @__PURE__ */ jsx24("span", { className: "squisq-jv-color__swatch", style: { background: value }, "aria-hidden": true }),
4515
+ /* @__PURE__ */ jsx24("span", { className: "squisq-jv-color__hex", children: value })
4014
4516
  ] });
4015
4517
  }
4016
4518
  function DateViewer({ value, schema }) {
4017
4519
  if (typeof value !== "string" || value === "") {
4018
- return /* @__PURE__ */ jsx21("span", { className: "squisq-jv-empty", children: "\u2014" });
4520
+ return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-empty", children: "\u2014" });
4019
4521
  }
4020
4522
  const fmt = schema.format;
4021
4523
  let display = value;
@@ -4032,31 +4534,31 @@ function DateViewer({ value, schema }) {
4032
4534
  }
4033
4535
  } catch {
4034
4536
  }
4035
- return /* @__PURE__ */ jsx21("span", { className: "squisq-jv-value", children: display });
4537
+ return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-value", children: display });
4036
4538
  }
4037
4539
  function ChipBinViewer({ value, schema }) {
4038
4540
  if (!Array.isArray(value) || value.length === 0) {
4039
- return /* @__PURE__ */ jsx21("span", { className: "squisq-jv-empty", children: "\u2014" });
4541
+ return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-empty", children: "\u2014" });
4040
4542
  }
4041
4543
  const itemSchema = Array.isArray(schema.items) ? schema.items[0] : schema.items;
4042
4544
  const labels = itemSchema?.squisq?.enumLabels;
4043
- return /* @__PURE__ */ jsx21("div", { className: "squisq-jv-chip-bin", children: value.map((item, i) => {
4545
+ return /* @__PURE__ */ jsx24("div", { className: "squisq-jv-chip-bin", children: value.map((item, i) => {
4044
4546
  const label = labels && typeof item === "string" ? labels[item] ?? String(item) : String(item);
4045
- return /* @__PURE__ */ jsx21("span", { className: "squisq-jv-chip", children: label }, i);
4547
+ return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-chip", children: label }, i);
4046
4548
  }) });
4047
4549
  }
4048
4550
  function CardStackViewer(props) {
4049
4551
  const { value, schema, rootSchema, rootData, pointer, density } = props;
4050
4552
  if (!Array.isArray(value) || value.length === 0) {
4051
- return /* @__PURE__ */ jsx21("span", { className: "squisq-jv-empty", children: "No items" });
4553
+ return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-empty", children: "No items" });
4052
4554
  }
4053
4555
  const itemSchema = (Array.isArray(schema.items) ? schema.items[0] : schema.items) ?? {};
4054
4556
  const itemLabel = itemSchema.squisq?.itemLabel;
4055
- return /* @__PURE__ */ jsx21("div", { className: "squisq-jv-card-stack", children: value.map((item, i) => {
4557
+ return /* @__PURE__ */ jsx24("div", { className: "squisq-jv-card-stack", children: value.map((item, i) => {
4056
4558
  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(
4559
+ return /* @__PURE__ */ jsxs17("div", { className: "squisq-jv-card", children: [
4560
+ title ? /* @__PURE__ */ jsx24("h4", { className: "squisq-jv-card__title", children: title }) : null,
4561
+ /* @__PURE__ */ jsx24(
4060
4562
  RenderNode,
4061
4563
  {
4062
4564
  value: item,
@@ -4087,16 +4589,16 @@ function GroupViewer(props) {
4087
4589
  const help = schema.squisq?.help ?? schema.description;
4088
4590
  const obj = (value && typeof value === "object" ? value : {}) ?? {};
4089
4591
  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(
4592
+ return /* @__PURE__ */ jsxs17("section", { className: "squisq-jv-group", children: [
4593
+ title ? /* @__PURE__ */ jsx24("h3", { className: "squisq-jv-group__title", children: title }) : null,
4594
+ help ? /* @__PURE__ */ jsx24("p", { className: "squisq-jv-group__help", children: help }) : null,
4595
+ propEntries.map(([key, propSchema]) => /* @__PURE__ */ jsx24(Fragment4, { children: /* @__PURE__ */ jsx24(
4094
4596
  RowOrSection,
4095
4597
  {
4096
4598
  label: propSchema.squisq?.label ?? propSchema.title ?? key,
4097
4599
  help: propSchema.squisq?.help ?? propSchema.description,
4098
4600
  kindHint: propSchema,
4099
- children: /* @__PURE__ */ jsx21(
4601
+ children: /* @__PURE__ */ jsx24(
4100
4602
  RenderNode,
4101
4603
  {
4102
4604
  value: obj[key],
@@ -4120,11 +4622,11 @@ function RowOrSection({
4120
4622
  }) {
4121
4623
  const composite = isCompositeKind(kindHint);
4122
4624
  if (composite) {
4123
- return /* @__PURE__ */ jsx21(Fragment4, { children });
4625
+ return /* @__PURE__ */ jsx24(Fragment5, { children });
4124
4626
  }
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 })
4627
+ return /* @__PURE__ */ jsxs17("div", { className: "squisq-jv-row", children: [
4628
+ /* @__PURE__ */ jsx24("div", { className: "squisq-jv-label", title: help, children: label }),
4629
+ /* @__PURE__ */ jsx24("div", { children })
4128
4630
  ] });
4129
4631
  }
4130
4632
  function isCompositeKind(schema) {
@@ -4145,11 +4647,11 @@ function TabsViewer(props) {
4145
4647
  const matchedIndex = pickMatchingBranch(branches, value);
4146
4648
  const branch = branches[matchedIndex];
4147
4649
  if (!branch) {
4148
- return /* @__PURE__ */ jsx21(TextViewer, { ...props });
4650
+ return /* @__PURE__ */ jsx24(TextViewer, { ...props });
4149
4651
  }
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(
4652
+ return /* @__PURE__ */ jsxs17("div", { className: "squisq-jv-tabs", children: [
4653
+ /* @__PURE__ */ jsx24("span", { className: "squisq-jv-tabs__discriminator", children: branch.squisq?.label ?? branch.title ?? `Option ${matchedIndex + 1}` }),
4654
+ /* @__PURE__ */ jsx24(
4153
4655
  RenderNode,
4154
4656
  {
4155
4657
  value,
@@ -4213,7 +4715,7 @@ var VIEWERS = {
4213
4715
  };
4214
4716
 
4215
4717
  // src/jsonView/RenderNode.tsx
4216
- import { jsx as jsx22 } from "react/jsx-runtime";
4718
+ import { jsx as jsx25 } from "react/jsx-runtime";
4217
4719
  function RenderNode(props) {
4218
4720
  const resolved = resolveRef(props.schema, props.rootSchema) ?? props.schema;
4219
4721
  if (resolveFlag(resolved.squisq?.hidden, props.rootData)) return null;
@@ -4229,18 +4731,18 @@ function RenderNode(props) {
4229
4731
  };
4230
4732
  if (kind === "group" || kind === "card") {
4231
4733
  const Group = Viewer;
4232
- return /* @__PURE__ */ jsx22(Group, { ...viewerProps, suppressTitle: props.suppressTopGroupTitle });
4734
+ return /* @__PURE__ */ jsx25(Group, { ...viewerProps, suppressTitle: props.suppressTopGroupTitle });
4233
4735
  }
4234
- return /* @__PURE__ */ jsx22(Viewer, { ...viewerProps });
4736
+ return /* @__PURE__ */ jsx25(Viewer, { ...viewerProps });
4235
4737
  }
4236
4738
 
4237
4739
  // src/jsonView/JsonView.tsx
4238
- import { jsx as jsx23 } from "react/jsx-runtime";
4740
+ import { jsx as jsx26 } from "react/jsx-runtime";
4239
4741
  function JsonView(props) {
4240
4742
  const { schema, value, theme, surface, density = "comfortable", className } = props;
4241
4743
  const { style } = useJsonViewTokens(theme, surface);
4242
4744
  const cls = "squisq-json-view" + (density === "compact" ? " squisq-json-view--compact" : "") + (className ? ` ${className}` : "");
4243
- return /* @__PURE__ */ jsx23("div", { className: cls, style, children: /* @__PURE__ */ jsx23(
4745
+ return /* @__PURE__ */ jsx26("div", { className: cls, style, children: /* @__PURE__ */ jsx26(
4244
4746
  RenderNode,
4245
4747
  {
4246
4748
  value,
@@ -4269,7 +4771,9 @@ export {
4269
4771
  LinearDocView,
4270
4772
  MapLayer,
4271
4773
  MarkdownRenderer,
4774
+ MediaClipLayer,
4272
4775
  MediaContext,
4776
+ PathLayer,
4273
4777
  ShapeLayer,
4274
4778
  SocialCaptionOverlay,
4275
4779
  TableLayer,
@@ -4283,6 +4787,7 @@ export {
4283
4787
  useAutoSurface,
4284
4788
  useDocPlayback,
4285
4789
  useMediaProvider,
4790
+ useMediaSchedule,
4286
4791
  useMediaUrl,
4287
4792
  useViewportOrientation
4288
4793
  };