@bendyline/squisq-react 2.11.11 → 2.11.12

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.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  LinearDocView
3
- } from "./chunk-CLP3VWJ2.js";
3
+ } from "./chunk-P3FJ3YKS.js";
4
4
 
5
5
  // src/PaginatedDocView.tsx
6
6
  import {
@@ -9,7 +9,7 @@ import {
9
9
  TreeLayer,
10
10
  VideoLayer,
11
11
  getTransitionClass
12
- } from "./chunk-W6MKRWL6.js";
12
+ } from "./chunk-WHVSQ42V.js";
13
13
  import {
14
14
  useAutoSurface
15
15
  } from "./chunk-TT6ENR6T.js";
@@ -24,6 +24,7 @@ import {
24
24
  // src/BlockRenderer.tsx
25
25
  import { useId, useLayoutEffect, useRef, useState } from "react";
26
26
  import { resolveTransitionDuration } from "@bendyline/squisq/schemas";
27
+ import { isGeometryBoundTemplate } from "@bendyline/squisq/doc";
27
28
 
28
29
  // src/utils/textFit.ts
29
30
  function largestFittingTextScale(fits, maximum = 2) {
@@ -138,6 +139,7 @@ function BlockRenderer({
138
139
  const clipId = `vb-clip-${instanceId}-${block.id}`;
139
140
  const svgRef = useRef(null);
140
141
  const [textScale, setTextScale] = useState(1);
142
+ const growText = growTextToFit && !isGeometryBoundTemplate(block.template);
141
143
  useLayoutEffect(() => {
142
144
  const svg = svgRef.current;
143
145
  let cancelled = false;
@@ -145,7 +147,7 @@ function BlockRenderer({
145
147
  const measure = () => {
146
148
  if (cancelled) return;
147
149
  const targets = Array.from(svg.querySelectorAll("[data-squisq-text-fit]"));
148
- if (!growTextToFit || targets.length === 0) {
150
+ if (!growText || targets.length === 0) {
149
151
  applyTextScale(targets, 1);
150
152
  setTextScale(1);
151
153
  return;
@@ -175,7 +177,7 @@ function BlockRenderer({
175
177
  return () => {
176
178
  cancelled = true;
177
179
  };
178
- }, [block.layers, growTextToFit, viewport.height, viewport.width]);
180
+ }, [block.layers, growText, viewport.height, viewport.width]);
179
181
  return /* @__PURE__ */ jsxs(
180
182
  "svg",
181
183
  {
@@ -186,7 +188,7 @@ function BlockRenderer({
186
188
  preserveAspectRatio: "xMidYMid meet",
187
189
  overflow: "hidden",
188
190
  "data-block-id": block.id,
189
- "data-squisq-text-scale": growTextToFit ? textScale : void 0,
191
+ "data-squisq-text-scale": growText ? textScale : void 0,
190
192
  children: [
191
193
  /* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsx("clipPath", { id: clipId, children: /* @__PURE__ */ jsx("rect", { x: "0", y: "0", width: viewport.width, height: viewport.height }) }) }),
192
194
  /* @__PURE__ */ jsx("g", { clipPath: `url(#${clipId})`, children: (block.layers ?? []).map((layer) => /* @__PURE__ */ jsx(
@@ -1,17 +1,17 @@
1
1
  import {
2
2
  BlockRenderer,
3
3
  LinearDocView
4
- } from "./chunk-CLP3VWJ2.js";
4
+ } from "./chunk-P3FJ3YKS.js";
5
5
  import {
6
6
  isCaptureDrivenVideo,
7
7
  seekVideoToFrame
8
- } from "./chunk-W6MKRWL6.js";
8
+ } from "./chunk-WHVSQ42V.js";
9
9
  import {
10
10
  useAudioSync,
11
11
  useDocPlayback,
12
12
  useMediaSchedule,
13
13
  useViewportOrientation
14
- } from "./chunk-3SKOLZUA.js";
14
+ } from "./chunk-U2FIZWAL.js";
15
15
  import {
16
16
  useAutoSurface
17
17
  } from "./chunk-TT6ENR6T.js";
@@ -2288,7 +2288,8 @@ import {
2288
2288
  isTemplateBlock as isTemplateBlock2,
2289
2289
  getCaptionAtTime as getCaptionAtTime2,
2290
2290
  resolveMediaSchedule,
2291
- getDocPlaybackDuration
2291
+ getDocPlaybackDuration,
2292
+ resolveMotionForDoc
2292
2293
  } from "@bendyline/squisq/schemas";
2293
2294
  import { applySurface, pipStyleVars } from "@bendyline/squisq/schemas";
2294
2295
 
@@ -2585,6 +2586,7 @@ function DocPlayerContent({
2585
2586
  coverSlidePlayback,
2586
2587
  coverVisible,
2587
2588
  theme,
2589
+ motion: motionProp,
2588
2590
  surface,
2589
2591
  captionStyle = "standard",
2590
2592
  captionPosition,
@@ -2787,6 +2789,12 @@ function DocPlayerContent({
2787
2789
  }),
2788
2790
  [resolvedPipStyle]
2789
2791
  );
2792
+ const hasAnchoredBlocks = useMemo5(
2793
+ () => doc.blocks.some(
2794
+ (block) => typeof block.sourceStartTime === "number"
2795
+ ),
2796
+ [doc.blocks]
2797
+ );
2790
2798
  const {
2791
2799
  currentBlock,
2792
2800
  currentBlockIndex,
@@ -2803,6 +2811,7 @@ function DocPlayerContent({
2803
2811
  } = useDocPlayback(doc, currentTime, {
2804
2812
  viewport: activeViewport,
2805
2813
  theme: effectiveTheme,
2814
+ motion: motionProp,
2806
2815
  onSeek: seekTo,
2807
2816
  // Offline capture may visit frames out of order; derive the crossfade
2808
2817
  // partner from block order rather than from what was rendered last.
@@ -2810,13 +2819,21 @@ function DocPlayerContent({
2810
2819
  // A synthetic track is only the timer used by an unnarrated preview.
2811
2820
  // Narration pacing may compact short visual beats, but it must never
2812
2821
  // remove authored slides from the default loss-averse projection.
2813
- useAudioSegmentTiming: audioMode !== "synthetic"
2822
+ // Offline capture also runs on the synthetic clock, yet a narrated doc
2823
+ // (blocks anchored to its segments by `sourceStartTime`) still takes its
2824
+ // timeline from those segments there — the exporter muxes the audio
2825
+ // afterwards. Without them, block durations would simply stack and every
2826
+ // gap between anchored blocks would pull the rest earlier, so the render
2827
+ // would no longer match live playback.
2828
+ useAudioSegmentTiming: audioMode !== "synthetic" || renderMode && hasAnchoredBlocks
2814
2829
  });
2815
2830
  const coverBlock = useMemo5(() => {
2816
2831
  const startBlockConfig = doc.startBlock;
2817
2832
  if (!appearance.showCoverSlide) return null;
2818
2833
  if (!startBlockConfig) return null;
2819
- const context = createTemplateContext(effectiveTheme, 0, 1, activeViewport);
2834
+ const context = createTemplateContext(effectiveTheme, 0, 1, activeViewport, {
2835
+ motion: resolveMotionForDoc(doc, effectiveTheme, motionProp)
2836
+ });
2820
2837
  const layers = expandCoverBlock(startBlockConfig, context, appearance.coverSlideTemplate);
2821
2838
  return {
2822
2839
  id: "cover-block",
@@ -2828,7 +2845,8 @@ function DocPlayerContent({
2828
2845
  layers
2829
2846
  };
2830
2847
  }, [
2831
- doc.startBlock,
2848
+ doc,
2849
+ motionProp,
2832
2850
  activeViewport,
2833
2851
  effectiveTheme,
2834
2852
  appearance.showCoverSlide,
@@ -2962,6 +2980,7 @@ function DocPlayerContent({
2962
2980
  if (selector && await selector(video, targetTime)) return;
2963
2981
  await seekVideoToFrame(video, targetTime);
2964
2982
  };
2983
+ let seekedActiveAnimations = 0;
2965
2984
  (root.getAnimations?.({ subtree: true }) ?? []).forEach((anim) => {
2966
2985
  const target = anim.effect?.target;
2967
2986
  if (!target) return;
@@ -2979,6 +2998,8 @@ function DocPlayerContent({
2979
2998
  anim.pause();
2980
2999
  }
2981
3000
  anim.currentTime = Math.max(0, position);
3001
+ const end = anim.effect?.getComputedTiming?.().endTime;
3002
+ if (typeof end !== "number" || position < end) seekedActiveAnimations += 1;
2982
3003
  });
2983
3004
  const blockElapsed = time - blockStartTime;
2984
3005
  const videoSeekPromises = [];
@@ -3014,7 +3035,9 @@ function DocPlayerContent({
3014
3035
  const blocksUnchanged = previousBlocks !== null && previousBlocks.length === presentedBlocks.length && presentedBlocks.every(
3015
3036
  (block, index) => block.element === previousBlocks[index].element && block.className === previousBlocks[index].className
3016
3037
  );
3017
- if (!renderMode || !blocksUnchanged) await waitForVisualUpdate();
3038
+ if (!renderMode || !blocksUnchanged || seekedActiveAnimations > 0) {
3039
+ await waitForVisualUpdate();
3040
+ }
3018
3041
  };
3019
3042
  const getDuration = () => {
3020
3043
  const mediaDuration = getDocPlaybackDuration(doc);
@@ -3556,7 +3579,7 @@ function DocPlayerContent({
3556
3579
  BlockRenderer,
3557
3580
  {
3558
3581
  block: previousBlock,
3559
- blockTime,
3582
+ blockTime: Math.max(0, currentTime - previousBlock.startTime),
3560
3583
  basePath,
3561
3584
  isExiting: true,
3562
3585
  transition: currentBlock?.transition,
@@ -476,6 +476,7 @@ function useAudioSync(audioRef, track, basePath = "", enabled = true, mode = "me
476
476
 
477
477
  // src/hooks/useDocPlayback.ts
478
478
  import { useMemo as useMemo3, useCallback as useCallback2, useRef as useRef2 } from "react";
479
+ import { resolveMotionForDoc } from "@bendyline/squisq/schemas";
479
480
  import {
480
481
  DEFAULT_THEME,
481
482
  getBlockAtTime,
@@ -495,7 +496,8 @@ function useDocPlayback(script, currentTime, options = {}) {
495
496
  theme,
496
497
  onSeek,
497
498
  useAudioSegmentTiming = true,
498
- deterministicTransitions = false
499
+ deterministicTransitions = false,
500
+ motion: motionOverride
499
501
  } = options;
500
502
  const blocks = useMemo3(() => {
501
503
  if (!script?.blocks) {
@@ -519,6 +521,11 @@ function useDocPlayback(script, currentTime, options = {}) {
519
521
  viewport,
520
522
  persistentLayers,
521
523
  theme,
524
+ motion: resolveMotionForDoc(
525
+ { motion: script.motion, frontmatter: script.frontmatter },
526
+ resolvedTheme,
527
+ motionOverride
528
+ ),
522
529
  // Custom (user-defined) templates inlined into the doc's
523
530
  // frontmatter — see CustomTemplates.ts. Merged onto the
524
531
  // built-in registry so blocks annotated with `{[myhero]}`
@@ -536,6 +543,9 @@ function useDocPlayback(script, currentTime, options = {}) {
536
543
  script?.audio?.segments,
537
544
  script?.persistentLayers,
538
545
  script?.customTemplates,
546
+ script?.motion,
547
+ script?.frontmatter,
548
+ motionOverride,
539
549
  viewport,
540
550
  theme,
541
551
  useAudioSegmentTiming
@@ -239,8 +239,16 @@ function remapToKenBurns(anim) {
239
239
  }
240
240
 
241
241
  // src/layers/TextLayer.tsx
242
- import { useId, useLayoutEffect, useMemo, useRef, useState } from "react";
242
+ import {
243
+ Fragment,
244
+ useId,
245
+ useLayoutEffect,
246
+ useMemo,
247
+ useRef,
248
+ useState
249
+ } from "react";
243
250
  import { DEFAULT_DOC_FONT } from "@bendyline/squisq/schemas";
251
+ import { formatCountUp, wordRevealDelay } from "@bendyline/squisq/doc";
244
252
  import {
245
253
  parseHtmlToNodes,
246
254
  sanitizeHtmlNodes,
@@ -253,6 +261,16 @@ import {
253
261
  iconClass
254
262
  } from "@bendyline/squisq/icon-marker";
255
263
 
264
+ // src/utils/defsId.ts
265
+ function defsIdFor(reactId, layerId) {
266
+ const raw = `${reactId}-${layerId}`;
267
+ const safe = raw.replace(/[^A-Za-z0-9_-]/g, "_");
268
+ if (safe === raw.replace(/:/g, "_")) return safe.replace(/^_+/, "d");
269
+ let hash = 0;
270
+ for (let i = 0; i < raw.length; i++) hash = hash * 31 + raw.charCodeAt(i) >>> 0;
271
+ return `${safe.replace(/^_+/, "d")}-${hash.toString(36)}`;
272
+ }
273
+
256
274
  // src/utils/fillStyle.tsx
257
275
  import { jsx as jsx2, jsxs } from "react/jsx-runtime";
258
276
  function borderDashArray(style, strokeWidth) {
@@ -351,7 +369,7 @@ function resolveShapeFilter(layerId, filter) {
351
369
  }
352
370
 
353
371
  // src/layers/TextLayer.tsx
354
- import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
372
+ import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
355
373
  function TextLayer(props) {
356
374
  if (props.layer.content.html?.trim()) return /* @__PURE__ */ jsx3(RichTextLayer, { ...props });
357
375
  if (hasIconMarker(props.layer.content.text ?? "")) return /* @__PURE__ */ jsx3(IconTextLayer, { ...props });
@@ -401,37 +419,45 @@ function IconTextLayer({ layer, viewport, blockTime, textScale = 1 }) {
401
419
  ...style.shadow ? { textShadow: "0 2px 3px rgba(0,0,0,0.7)" } : {},
402
420
  ...animStyle.style
403
421
  };
404
- return /* @__PURE__ */ jsx3("g", { className: `block-layer block-layer--text ${animStyle.className}`, "data-layer-id": layer.id, children: /* @__PURE__ */ jsx3(
405
- "foreignObject",
422
+ return /* @__PURE__ */ jsx3(
423
+ "g",
406
424
  {
407
- x: boxX,
408
- y: boxY,
409
- width: boxWidth,
410
- height: boxHeight,
411
- style: { overflow: "visible" },
425
+ className: `block-layer block-layer--text ${animStyle.className}`,
426
+ style: animStyle.style,
427
+ "data-layer-id": layer.id,
412
428
  children: /* @__PURE__ */ jsx3(
413
- "div",
429
+ "foreignObject",
414
430
  {
415
- ...{ xmlns: "http://www.w3.org/1999/xhtml" },
416
- "data-squisq-text-fit": "html",
417
- "data-squisq-base-font-size": style.fontSize,
418
- style: boxStyle,
431
+ x: boxX,
432
+ y: boxY,
433
+ width: boxWidth,
434
+ height: boxHeight,
435
+ style: { overflow: "visible" },
419
436
  children: /* @__PURE__ */ jsx3(
420
437
  "div",
421
438
  {
422
- "data-squisq-text-content": "true",
423
- style: { width: "100%", whiteSpace: "pre-wrap", wordBreak: "break-word" },
424
- "aria-label": plain,
425
- dangerouslySetInnerHTML: { __html: html }
439
+ ...{ xmlns: "http://www.w3.org/1999/xhtml" },
440
+ "data-squisq-text-fit": "html",
441
+ "data-squisq-base-font-size": style.fontSize,
442
+ style: boxStyle,
443
+ children: /* @__PURE__ */ jsx3(
444
+ "div",
445
+ {
446
+ "data-squisq-text-content": "true",
447
+ style: { width: "100%", whiteSpace: "pre-wrap", wordBreak: "break-word" },
448
+ "aria-label": plain,
449
+ dangerouslySetInnerHTML: { __html: html }
450
+ }
451
+ )
426
452
  }
427
453
  )
428
454
  }
429
455
  )
430
456
  }
431
- ) });
457
+ );
432
458
  }
433
459
  function PlainTextLayer({ layer, viewport, blockTime, textScale = 1 }) {
434
- const defsId = `${useId().replace(/:/g, "")}-${layer.id}`;
460
+ const defsId = defsIdFor(useId(), layer.id);
435
461
  const { content, position, animation } = layer;
436
462
  const { text, style } = content;
437
463
  const rawX = resolveValue(position.x, viewport.width);
@@ -447,7 +473,10 @@ function PlainTextLayer({ layer, viewport, blockTime, textScale = 1 }) {
447
473
  const fitWidth = boxWidth ?? (textAnchor === "middle" ? Math.max(0, Math.min(x, viewport.width - x) * 2) : textAnchor === "end" ? Math.max(0, x) : Math.max(0, viewport.width - x));
448
474
  const fitHeight = boxHeight ?? (dominantBaseline === "middle" ? Math.max(0, Math.min(y, viewport.height - y) * 2) : dominantBaseline === "text-after-edge" ? Math.max(0, y) : Math.max(0, viewport.height - y));
449
475
  const animStyle = getAnimationStyle(animation, blockTime);
450
- const rawLines = (text ?? "").split("\n");
476
+ const isCountUp = animation?.type === "countUp";
477
+ const isWordReveal = animation?.type === "wordReveal";
478
+ const displayText = isCountUp && animation ? formatCountUp(text ?? "", animation, blockTime) : text;
479
+ const rawLines = (displayText ?? "").split("\n");
451
480
  let lines = maxWidth ? rawLines.reduce(
452
481
  (acc, line) => acc.concat(wrapText(line, style.fontSize, maxWidth)),
453
482
  []
@@ -517,58 +546,78 @@ function PlainTextLayer({ layer, viewport, blockTime, textScale = 1 }) {
517
546
  ...animStyle.style
518
547
  };
519
548
  const filterId = style.shadow ? `shadow-${defsId}` : void 0;
520
- return /* @__PURE__ */ jsxs2("g", { className: `block-layer block-layer--text ${animStyle.className}`, "data-layer-id": layer.id, children: [
521
- style.shadow && /* @__PURE__ */ jsx3("defs", { children: /* @__PURE__ */ jsx3("filter", { id: filterId, x: "-20%", y: "-20%", width: "140%", height: "140%", children: /* @__PURE__ */ jsx3("feDropShadow", { dx: "0", dy: "2", stdDeviation: "3", floodColor: "rgba(0,0,0,0.7)" }) }) }),
522
- /* @__PURE__ */ jsx3(
523
- TextBox,
524
- {
525
- layerId: defsId,
526
- style,
527
- box: boxWidth != null && boxHeight != null ? {
528
- x: rawX - anchorAxis(anchor, boxWidth, "x"),
529
- y: rawY - anchorAxis(anchor, boxHeight, "y"),
530
- width: boxWidth,
531
- height: boxHeight
532
- } : {
533
- x: x - (style.padding || 16),
534
- y: y - style.fontSize - (style.padding || 16),
535
- width: getTextBoxWidth(lines, style) + (style.padding || 16) * 2,
536
- height: lines.length * lineHeightPx + (style.padding || 16) * 2
537
- }
538
- }
539
- ),
540
- /* @__PURE__ */ jsx3(
541
- "g",
542
- {
543
- "data-squisq-text-fit": "svg",
544
- "data-squisq-fit-width": fitWidth,
545
- "data-squisq-fit-height": fitHeight,
546
- style: {
547
- transform: `scale(${textScale})`,
548
- transformOrigin: `${x}px ${y}px`,
549
- transformBox: "view-box"
550
- },
551
- children: /* @__PURE__ */ jsx3(
552
- "text",
549
+ let wordIndex = 0;
550
+ const renderLine = (line) => {
551
+ if (!isWordReveal || !animation || !line) return line || "\xA0";
552
+ const words = line.split(" ");
553
+ return words.map((word, w) => {
554
+ const delay = wordRevealDelay(animation, wordIndex++);
555
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
556
+ w > 0 ? " " : null,
557
+ /* @__PURE__ */ jsx3("tspan", { className: "anim-word", style: { "--anim-delay": `${delay}s` }, children: word })
558
+ ] }, w);
559
+ });
560
+ };
561
+ return /* @__PURE__ */ jsxs2(
562
+ "g",
563
+ {
564
+ className: `block-layer block-layer--text ${animStyle.className}`,
565
+ style: animStyle.style,
566
+ "data-layer-id": layer.id,
567
+ children: [
568
+ style.shadow && /* @__PURE__ */ jsx3("defs", { children: /* @__PURE__ */ jsx3("filter", { id: filterId, x: "-20%", y: "-20%", width: "140%", height: "140%", children: /* @__PURE__ */ jsx3("feDropShadow", { dx: "0", dy: "2", stdDeviation: "3", floodColor: "rgba(0,0,0,0.7)" }) }) }),
569
+ /* @__PURE__ */ jsx3(
570
+ TextBox,
571
+ {
572
+ layerId: defsId,
573
+ style,
574
+ box: boxWidth != null && boxHeight != null ? {
575
+ x: rawX - anchorAxis(anchor, boxWidth, "x"),
576
+ y: rawY - anchorAxis(anchor, boxHeight, "y"),
577
+ width: boxWidth,
578
+ height: boxHeight
579
+ } : {
580
+ x: x - (style.padding || 16),
581
+ y: y - style.fontSize - (style.padding || 16),
582
+ width: getTextBoxWidth(lines, style) + (style.padding || 16) * 2,
583
+ height: lines.length * lineHeightPx + (style.padding || 16) * 2
584
+ }
585
+ }
586
+ ),
587
+ /* @__PURE__ */ jsx3(
588
+ "g",
553
589
  {
554
- ref: textRef,
555
- "data-squisq-text-content": "true",
556
- x,
557
- y,
558
- textAnchor,
559
- dominantBaseline,
560
- style: textStyles,
561
- filter: filterId ? `url(#${filterId})` : void 0,
562
- transform: fitTransform,
563
- children: lines.map((line, i) => /* @__PURE__ */ jsxs2("tspan", { x, dy: i === 0 ? firstLineDy : lineHeightPx, children: [
564
- line || "\xA0",
565
- " "
566
- ] }, i))
590
+ "data-squisq-text-fit": "svg",
591
+ "data-squisq-fit-width": fitWidth,
592
+ "data-squisq-fit-height": fitHeight,
593
+ style: {
594
+ transform: `scale(${textScale})`,
595
+ transformOrigin: `${x}px ${y}px`,
596
+ transformBox: "view-box"
597
+ },
598
+ children: /* @__PURE__ */ jsx3(
599
+ "text",
600
+ {
601
+ ref: textRef,
602
+ "data-squisq-text-content": "true",
603
+ x,
604
+ y,
605
+ textAnchor,
606
+ dominantBaseline,
607
+ style: textStyles,
608
+ filter: filterId ? `url(#${filterId})` : void 0,
609
+ transform: fitTransform,
610
+ children: lines.map((line, i) => /* @__PURE__ */ jsxs2("tspan", { x, dy: i === 0 ? firstLineDy : lineHeightPx, children: [
611
+ renderLine(line),
612
+ " "
613
+ ] }, i))
614
+ }
615
+ )
567
616
  }
568
617
  )
569
- }
570
- )
571
- ] });
618
+ ]
619
+ }
620
+ );
572
621
  }
573
622
  function RichTextLayer({ layer, viewport, blockTime, textScale = 1 }) {
574
623
  const { content, position, animation } = layer;
@@ -618,48 +667,56 @@ function RichTextLayer({ layer, viewport, blockTime, textScale = 1 }) {
618
667
  if (target?.closest?.("[data-squisq-table-scroll]")) event.stopPropagation();
619
668
  };
620
669
  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 .7em;padding-left:1.2em;list-style-position:${listStylePosition}}.${cls} li{margin:0}.${cls} li>p{display:inline;margin:0}.${cls} [data-squisq-table-scroll]{width:100%;max-height:20em;margin:.35em 0;overflow:auto;overscroll-behavior:contain;box-sizing:border-box}.${cls} table{width:100%;min-width:48em;margin:0;border-collapse:separate;border-spacing:0;table-layout:fixed;font-size:.76em;line-height:1.2}.${cls} th,.${cls} td{box-sizing:border-box;padding:.28em .45em;border:1px solid rgba(127,127,127,.55);border-color:color-mix(in srgb,currentColor 32%,transparent);vertical-align:top;white-space:normal;overflow-wrap:anywhere}.${cls} th{position:sticky;top:0;z-index:1;background:rgba(127,127,127,.16);background:color-mix(in srgb,currentColor 12%,transparent);font-weight:600}.${cls} tbody tr:nth-child(even){background:rgba(127,127,127,.07);background:color-mix(in srgb,currentColor 5%,transparent)}.${cls} *:first-child{margin-top:0}.${cls} *:last-child{margin-bottom:0}.${cls} a{color:inherit;text-decoration:underline}`;
621
- return /* @__PURE__ */ jsx3("g", { className: `block-layer block-layer--text ${animStyle.className}`, "data-layer-id": layer.id, children: /* @__PURE__ */ jsx3(
622
- "foreignObject",
670
+ return /* @__PURE__ */ jsx3(
671
+ "g",
623
672
  {
624
- x: boxX,
625
- y: boxY,
626
- width: boxWidth,
627
- height: boxHeight,
628
- color: style.color,
629
- style: { color: style.color },
630
- children: /* @__PURE__ */ jsxs2(
631
- "div",
673
+ className: `block-layer block-layer--text ${animStyle.className}`,
674
+ style: animStyle.style,
675
+ "data-layer-id": layer.id,
676
+ children: /* @__PURE__ */ jsx3(
677
+ "foreignObject",
632
678
  {
633
- ...{ xmlns: "http://www.w3.org/1999/xhtml" },
634
- "data-squisq-text-fit": "html",
635
- "data-squisq-base-font-size": style.fontSize,
636
- style: boxStyle,
637
- onPointerDown: stopEmbeddedTableEvent,
638
- onClick: stopEmbeddedTableEvent,
639
- onWheel: stopEmbeddedTableEvent,
640
- onKeyDown: stopEmbeddedTableEvent,
641
- children: [
642
- /* @__PURE__ */ jsx3("style", { children: scopedCss }),
643
- /* @__PURE__ */ jsx3(
644
- "div",
645
- {
646
- "data-squisq-text-content": "true",
647
- className: cls,
648
- "aria-label": content.text,
649
- style: {
650
- width: "100%",
651
- color: style.color,
652
- whiteSpace: "pre-wrap",
653
- wordBreak: "break-word"
654
- },
655
- dangerouslySetInnerHTML: { __html: safeHtml }
656
- }
657
- )
658
- ]
679
+ x: boxX,
680
+ y: boxY,
681
+ width: boxWidth,
682
+ height: boxHeight,
683
+ color: style.color,
684
+ style: { color: style.color },
685
+ children: /* @__PURE__ */ jsxs2(
686
+ "div",
687
+ {
688
+ ...{ xmlns: "http://www.w3.org/1999/xhtml" },
689
+ "data-squisq-text-fit": "html",
690
+ "data-squisq-base-font-size": style.fontSize,
691
+ style: boxStyle,
692
+ onPointerDown: stopEmbeddedTableEvent,
693
+ onClick: stopEmbeddedTableEvent,
694
+ onWheel: stopEmbeddedTableEvent,
695
+ onKeyDown: stopEmbeddedTableEvent,
696
+ children: [
697
+ /* @__PURE__ */ jsx3("style", { children: scopedCss }),
698
+ /* @__PURE__ */ jsx3(
699
+ "div",
700
+ {
701
+ "data-squisq-text-content": "true",
702
+ className: cls,
703
+ "aria-label": content.text,
704
+ style: {
705
+ width: "100%",
706
+ color: style.color,
707
+ whiteSpace: "pre-wrap",
708
+ wordBreak: "break-word"
709
+ },
710
+ dangerouslySetInnerHTML: { __html: safeHtml }
711
+ }
712
+ )
713
+ ]
714
+ }
715
+ )
659
716
  }
660
717
  )
661
718
  }
662
- ) });
719
+ );
663
720
  }
664
721
  function cssId(id) {
665
722
  return id.replace(/[^a-zA-Z0-9_-]/g, "-");
@@ -674,7 +731,7 @@ function TextBox({
674
731
  if (!hasFill && !hasBorder) return null;
675
732
  const { fill, def } = resolveFill(layerId, style.background, style.backgroundGradient);
676
733
  const dash = borderDashArray(style.borderStyle, style.borderWidth);
677
- return /* @__PURE__ */ jsxs2(Fragment, { children: [
734
+ return /* @__PURE__ */ jsxs2(Fragment2, { children: [
678
735
  def && /* @__PURE__ */ jsx3("defs", { children: def }),
679
736
  /* @__PURE__ */ jsx3(
680
737
  "rect",
@@ -773,7 +830,7 @@ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
773
830
  var FULL_BLEED_OVERSCAN = 1;
774
831
  function ShapeLayer({ layer, viewport, blockTime }) {
775
832
  const { content, position, animation } = layer;
776
- const defsId = `${useId2().replace(/:/g, "")}-${layer.id}`;
833
+ const defsId = defsIdFor(useId2(), layer.id);
777
834
  const rawX = resolveValue(position.x, viewport.width);
778
835
  const rawY = resolveValue(position.y, viewport.height);
779
836
  const width = position.width ? resolveValue(position.width, viewport.width) : 100;
@@ -913,7 +970,7 @@ function effectiveMarker(explicit, legacyArrow, end) {
913
970
  }
914
971
  function PathLayer({ layer, viewport, blockTime }) {
915
972
  const { content, animation, id } = layer;
916
- const defsId = `${useId3().replace(/:/g, "")}-${id}`;
973
+ const defsId = defsIdFor(useId3(), id);
917
974
  const d = effectivePath(layer, viewport);
918
975
  const stroke = content.stroke ?? "#1e293b";
919
976
  const strokeWidth = content.strokeWidth ?? 2;
@@ -925,6 +982,9 @@ function PathLayer({ layer, viewport, blockTime }) {
925
982
  const legacyArrow = readLegacyArrow(content);
926
983
  const start = markerPath(effectiveMarker(content.startMarker, legacyArrow, "start"), "start");
927
984
  const end = markerPath(effectiveMarker(content.endMarker, legacyArrow, "end"), "end");
985
+ const markerStart = start ? `url(#${startId})` : void 0;
986
+ const markerEnd = end ? `url(#${endId})` : void 0;
987
+ const drawOn = animation?.type === "drawOn";
928
988
  return /* @__PURE__ */ jsxs4(
929
989
  "g",
930
990
  {
@@ -945,9 +1005,24 @@ function PathLayer({ layer, viewport, blockTime }) {
945
1005
  strokeWidth,
946
1006
  fill,
947
1007
  fillOpacity: content.fillOpacity,
948
- strokeDasharray: dash,
949
- markerStart: start ? `url(#${startId})` : void 0,
950
- markerEnd: end ? `url(#${endId})` : void 0
1008
+ strokeDasharray: drawOn ? void 0 : dash,
1009
+ pathLength: drawOn ? 1 : void 0,
1010
+ "data-squisq-draw": drawOn ? "stroke" : void 0,
1011
+ markerStart: drawOn ? void 0 : markerStart,
1012
+ markerEnd: drawOn ? void 0 : markerEnd
1013
+ }
1014
+ ),
1015
+ drawOn && (markerStart || markerEnd) && /* @__PURE__ */ jsx5(
1016
+ "path",
1017
+ {
1018
+ d,
1019
+ stroke,
1020
+ strokeWidth,
1021
+ strokeOpacity: 0,
1022
+ fill: "none",
1023
+ "data-squisq-draw": "markers",
1024
+ markerStart,
1025
+ markerEnd
951
1026
  }
952
1027
  )
953
1028
  ]
@@ -1185,8 +1260,11 @@ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1185
1260
  function MapLayer({ layer, basePath, viewport, blockTime }) {
1186
1261
  const { content, position, animation } = layer;
1187
1262
  const clipId = `map-clip-${useId4().replace(/:/g, "")}-${layer.id}`;
1188
- const [mapImageUrl, setMapImageUrl] = useState2(null);
1189
- const [isLoading, setIsLoading] = useState2(true);
1263
+ const staticSrc = useMediaUrl(content.staticSrc ?? "", basePath);
1264
+ const [mapImageUrl, setMapImageUrl] = useState2(
1265
+ () => content.staticSrc ? staticSrc || null : null
1266
+ );
1267
+ const [isLoading, setIsLoading] = useState2(!content.staticSrc);
1190
1268
  const [error, setError] = useState2(null);
1191
1269
  const [blockedByPolicy, setBlockedByPolicy] = useState2(false);
1192
1270
  const x = resolveValue(position.x, viewport.width);
@@ -1196,7 +1274,6 @@ function MapLayer({ layer, basePath, viewport, blockTime }) {
1196
1274
  const offset = getAnchorOffset(position.anchor, width, height);
1197
1275
  const finalX = x + offset.x;
1198
1276
  const finalY = y + offset.y;
1199
- const staticSrc = useMediaUrl(content.staticSrc ?? "", basePath);
1200
1277
  const resourcePolicy = useResourcePolicy();
1201
1278
  useEffect(() => {
1202
1279
  let cancelled = false;