@bendyline/squisq-react 1.3.1 → 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.
Files changed (215) hide show
  1. package/dist/index.d.ts +766 -35
  2. package/dist/index.js +4793 -33
  3. package/dist/index.js.map +1 -1
  4. package/dist/squisq-player.css +1 -1
  5. package/dist/squisq-player.css.map +1 -1
  6. package/dist/squisq-player.global.js +17 -13
  7. package/dist/squisq-player.global.js.map +1 -1
  8. package/dist/standalone-source.js +1 -1
  9. package/package.json +3 -2
  10. package/src/BlockRenderer.tsx +15 -7
  11. package/src/DocPlayer.tsx +65 -12
  12. package/src/DocProgressBar.tsx +21 -3
  13. package/src/LinearDocView.tsx +11 -197
  14. package/src/MarkdownRenderer.tsx +165 -41
  15. package/src/MediaClipLayer.tsx +135 -0
  16. package/src/__tests__/DocPlayer.test.tsx +51 -0
  17. package/src/__tests__/DocProgressBar.test.tsx +76 -0
  18. package/src/__tests__/MarkdownRenderer.test.tsx +95 -1
  19. package/src/__tests__/PathLayer.test.tsx +73 -0
  20. package/src/__tests__/fillStyle.test.tsx +112 -0
  21. package/src/__tests__/transitionStyles.test.ts +125 -0
  22. package/src/__tests__/useDocPlayback.transition.test.ts +70 -0
  23. package/src/hooks/useAudioSync.ts +14 -1
  24. package/src/hooks/useDocPlayback.ts +81 -100
  25. package/src/hooks/useMediaSchedule.ts +39 -0
  26. package/src/index.ts +7 -0
  27. package/src/layers/ImageLayer.tsx +11 -1
  28. package/src/layers/PathLayer.tsx +146 -0
  29. package/src/layers/ShapeLayer.tsx +27 -5
  30. package/src/layers/TextLayer.tsx +395 -22
  31. package/src/layers/VideoLayer.tsx +16 -9
  32. package/src/layers/index.ts +1 -0
  33. package/src/styles/doc-animations.css +1857 -2
  34. package/src/utils/fillStyle.tsx +148 -0
  35. package/dist/BlockRenderer.d.ts +0 -37
  36. package/dist/BlockRenderer.d.ts.map +0 -1
  37. package/dist/BlockRenderer.js +0 -54
  38. package/dist/BlockRenderer.js.map +0 -1
  39. package/dist/CaptionOverlay.d.ts +0 -33
  40. package/dist/CaptionOverlay.d.ts.map +0 -1
  41. package/dist/CaptionOverlay.js +0 -44
  42. package/dist/CaptionOverlay.js.map +0 -1
  43. package/dist/DocControlsBottom.d.ts +0 -28
  44. package/dist/DocControlsBottom.d.ts.map +0 -1
  45. package/dist/DocControlsBottom.js +0 -27
  46. package/dist/DocControlsBottom.js.map +0 -1
  47. package/dist/DocControlsOverlay.d.ts +0 -28
  48. package/dist/DocControlsOverlay.d.ts.map +0 -1
  49. package/dist/DocControlsOverlay.js +0 -80
  50. package/dist/DocControlsOverlay.js.map +0 -1
  51. package/dist/DocControlsSidebar.d.ts +0 -27
  52. package/dist/DocControlsSidebar.d.ts.map +0 -1
  53. package/dist/DocControlsSidebar.js +0 -10
  54. package/dist/DocControlsSidebar.js.map +0 -1
  55. package/dist/DocControlsSlideshow.d.ts +0 -20
  56. package/dist/DocControlsSlideshow.d.ts.map +0 -1
  57. package/dist/DocControlsSlideshow.js +0 -69
  58. package/dist/DocControlsSlideshow.js.map +0 -1
  59. package/dist/DocPlayer.d.ts +0 -97
  60. package/dist/DocPlayer.d.ts.map +0 -1
  61. package/dist/DocPlayer.js +0 -657
  62. package/dist/DocPlayer.js.map +0 -1
  63. package/dist/DocPlayerWithSidebar.d.ts +0 -41
  64. package/dist/DocPlayerWithSidebar.d.ts.map +0 -1
  65. package/dist/DocPlayerWithSidebar.js +0 -69
  66. package/dist/DocPlayerWithSidebar.js.map +0 -1
  67. package/dist/DocProgressBar.d.ts +0 -31
  68. package/dist/DocProgressBar.d.ts.map +0 -1
  69. package/dist/DocProgressBar.js +0 -122
  70. package/dist/DocProgressBar.js.map +0 -1
  71. package/dist/InlineAudioPlayer.d.ts +0 -15
  72. package/dist/InlineAudioPlayer.d.ts.map +0 -1
  73. package/dist/InlineAudioPlayer.js +0 -22
  74. package/dist/InlineAudioPlayer.js.map +0 -1
  75. package/dist/InlineVideoPlayer.d.ts +0 -21
  76. package/dist/InlineVideoPlayer.d.ts.map +0 -1
  77. package/dist/InlineVideoPlayer.js +0 -29
  78. package/dist/InlineVideoPlayer.js.map +0 -1
  79. package/dist/LinearDocView.d.ts +0 -73
  80. package/dist/LinearDocView.d.ts.map +0 -1
  81. package/dist/LinearDocView.js +0 -451
  82. package/dist/LinearDocView.js.map +0 -1
  83. package/dist/MarkdownRenderer.d.ts +0 -32
  84. package/dist/MarkdownRenderer.d.ts.map +0 -1
  85. package/dist/MarkdownRenderer.js +0 -259
  86. package/dist/MarkdownRenderer.js.map +0 -1
  87. package/dist/SocialCaptionOverlay.d.ts +0 -27
  88. package/dist/SocialCaptionOverlay.d.ts.map +0 -1
  89. package/dist/SocialCaptionOverlay.js +0 -185
  90. package/dist/SocialCaptionOverlay.js.map +0 -1
  91. package/dist/__tests__/BlockRenderer.test.d.ts +0 -2
  92. package/dist/__tests__/BlockRenderer.test.d.ts.map +0 -1
  93. package/dist/__tests__/BlockRenderer.test.js +0 -82
  94. package/dist/__tests__/BlockRenderer.test.js.map +0 -1
  95. package/dist/__tests__/DocControlsSlideshow.test.d.ts +0 -2
  96. package/dist/__tests__/DocControlsSlideshow.test.d.ts.map +0 -1
  97. package/dist/__tests__/DocControlsSlideshow.test.js +0 -89
  98. package/dist/__tests__/DocControlsSlideshow.test.js.map +0 -1
  99. package/dist/__tests__/JsonView.test.d.ts +0 -2
  100. package/dist/__tests__/JsonView.test.d.ts.map +0 -1
  101. package/dist/__tests__/JsonView.test.js +0 -93
  102. package/dist/__tests__/JsonView.test.js.map +0 -1
  103. package/dist/__tests__/LinearDocView.test.d.ts +0 -2
  104. package/dist/__tests__/LinearDocView.test.d.ts.map +0 -1
  105. package/dist/__tests__/LinearDocView.test.js +0 -250
  106. package/dist/__tests__/LinearDocView.test.js.map +0 -1
  107. package/dist/__tests__/MarkdownRenderer.test.d.ts +0 -2
  108. package/dist/__tests__/MarkdownRenderer.test.d.ts.map +0 -1
  109. package/dist/__tests__/MarkdownRenderer.test.js +0 -208
  110. package/dist/__tests__/MarkdownRenderer.test.js.map +0 -1
  111. package/dist/__tests__/TableLayer.test.d.ts +0 -2
  112. package/dist/__tests__/TableLayer.test.d.ts.map +0 -1
  113. package/dist/__tests__/TableLayer.test.js +0 -121
  114. package/dist/__tests__/TableLayer.test.js.map +0 -1
  115. package/dist/__tests__/exports.test.d.ts +0 -2
  116. package/dist/__tests__/exports.test.d.ts.map +0 -1
  117. package/dist/__tests__/exports.test.js +0 -48
  118. package/dist/__tests__/exports.test.js.map +0 -1
  119. package/dist/hooks/AudioProvider.d.ts +0 -71
  120. package/dist/hooks/AudioProvider.d.ts.map +0 -1
  121. package/dist/hooks/AudioProvider.js +0 -52
  122. package/dist/hooks/AudioProvider.js.map +0 -1
  123. package/dist/hooks/MediaContext.d.ts +0 -35
  124. package/dist/hooks/MediaContext.d.ts.map +0 -1
  125. package/dist/hooks/MediaContext.js +0 -72
  126. package/dist/hooks/MediaContext.js.map +0 -1
  127. package/dist/hooks/index.d.ts +0 -6
  128. package/dist/hooks/index.d.ts.map +0 -1
  129. package/dist/hooks/index.js +0 -5
  130. package/dist/hooks/index.js.map +0 -1
  131. package/dist/hooks/useAudioSync.d.ts +0 -18
  132. package/dist/hooks/useAudioSync.d.ts.map +0 -1
  133. package/dist/hooks/useAudioSync.js +0 -340
  134. package/dist/hooks/useAudioSync.js.map +0 -1
  135. package/dist/hooks/useAutoSurface.d.ts +0 -11
  136. package/dist/hooks/useAutoSurface.d.ts.map +0 -1
  137. package/dist/hooks/useAutoSurface.js +0 -24
  138. package/dist/hooks/useAutoSurface.js.map +0 -1
  139. package/dist/hooks/useDocPlayback.d.ts +0 -47
  140. package/dist/hooks/useDocPlayback.d.ts.map +0 -1
  141. package/dist/hooks/useDocPlayback.js +0 -179
  142. package/dist/hooks/useDocPlayback.js.map +0 -1
  143. package/dist/hooks/useViewportOrientation.d.ts +0 -31
  144. package/dist/hooks/useViewportOrientation.d.ts.map +0 -1
  145. package/dist/hooks/useViewportOrientation.js +0 -92
  146. package/dist/hooks/useViewportOrientation.js.map +0 -1
  147. package/dist/index.d.ts.map +0 -1
  148. package/dist/jsonView/JsonView.d.ts +0 -26
  149. package/dist/jsonView/JsonView.d.ts.map +0 -1
  150. package/dist/jsonView/JsonView.js +0 -12
  151. package/dist/jsonView/JsonView.js.map +0 -1
  152. package/dist/jsonView/RenderNode.d.ts +0 -22
  153. package/dist/jsonView/RenderNode.d.ts.map +0 -1
  154. package/dist/jsonView/RenderNode.js +0 -30
  155. package/dist/jsonView/RenderNode.js.map +0 -1
  156. package/dist/jsonView/index.d.ts +0 -3
  157. package/dist/jsonView/index.d.ts.map +0 -1
  158. package/dist/jsonView/index.js +0 -2
  159. package/dist/jsonView/index.js.map +0 -1
  160. package/dist/jsonView/useJsonViewTokens.d.ts +0 -14
  161. package/dist/jsonView/useJsonViewTokens.d.ts.map +0 -1
  162. package/dist/jsonView/useJsonViewTokens.js +0 -34
  163. package/dist/jsonView/useJsonViewTokens.js.map +0 -1
  164. package/dist/jsonView/viewers.d.ts +0 -30
  165. package/dist/jsonView/viewers.d.ts.map +0 -1
  166. package/dist/jsonView/viewers.js +0 -226
  167. package/dist/jsonView/viewers.js.map +0 -1
  168. package/dist/layers/ImageLayer.d.ts +0 -23
  169. package/dist/layers/ImageLayer.d.ts.map +0 -1
  170. package/dist/layers/ImageLayer.js +0 -104
  171. package/dist/layers/ImageLayer.js.map +0 -1
  172. package/dist/layers/MapLayer.d.ts +0 -29
  173. package/dist/layers/MapLayer.d.ts.map +0 -1
  174. package/dist/layers/MapLayer.js +0 -97
  175. package/dist/layers/MapLayer.js.map +0 -1
  176. package/dist/layers/ShapeLayer.d.ts +0 -20
  177. package/dist/layers/ShapeLayer.d.ts.map +0 -1
  178. package/dist/layers/ShapeLayer.js +0 -39
  179. package/dist/layers/ShapeLayer.js.map +0 -1
  180. package/dist/layers/TableLayer.d.ts +0 -23
  181. package/dist/layers/TableLayer.d.ts.map +0 -1
  182. package/dist/layers/TableLayer.js +0 -58
  183. package/dist/layers/TableLayer.js.map +0 -1
  184. package/dist/layers/TextLayer.d.ts +0 -23
  185. package/dist/layers/TextLayer.d.ts.map +0 -1
  186. package/dist/layers/TextLayer.js +0 -126
  187. package/dist/layers/TextLayer.js.map +0 -1
  188. package/dist/layers/VideoLayer.d.ts +0 -40
  189. package/dist/layers/VideoLayer.d.ts.map +0 -1
  190. package/dist/layers/VideoLayer.js +0 -105
  191. package/dist/layers/VideoLayer.js.map +0 -1
  192. package/dist/layers/index.d.ts +0 -6
  193. package/dist/layers/index.d.ts.map +0 -1
  194. package/dist/layers/index.js +0 -6
  195. package/dist/layers/index.js.map +0 -1
  196. package/dist/standalone-entry.d.ts +0 -69
  197. package/dist/standalone-entry.d.ts.map +0 -1
  198. package/dist/standalone-entry.js +0 -185
  199. package/dist/standalone-entry.js.map +0 -1
  200. package/dist/types.d.ts +0 -154
  201. package/dist/types.d.ts.map +0 -1
  202. package/dist/types.js +0 -21
  203. package/dist/types.js.map +0 -1
  204. package/dist/utils/animationUtils.d.ts +0 -8
  205. package/dist/utils/animationUtils.d.ts.map +0 -1
  206. package/dist/utils/animationUtils.js +0 -8
  207. package/dist/utils/animationUtils.js.map +0 -1
  208. package/dist/utils/layerUtils.d.ts +0 -17
  209. package/dist/utils/layerUtils.d.ts.map +0 -1
  210. package/dist/utils/layerUtils.js +0 -37
  211. package/dist/utils/layerUtils.js.map +0 -1
  212. package/dist/utils/mapTileUtils.d.ts +0 -105
  213. package/dist/utils/mapTileUtils.d.ts.map +0 -1
  214. package/dist/utils/mapTileUtils.js +0 -268
  215. package/dist/utils/mapTileUtils.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,34 +1,4794 @@
1
- // Main components
2
- export { DocPlayer } from './DocPlayer.js';
3
- export { BlockRenderer, VIEWPORT } from './BlockRenderer.js';
4
- export { CaptionOverlay } from './CaptionOverlay.js';
5
- export { SocialCaptionOverlay } from './SocialCaptionOverlay.js';
6
- export { DocControlsOverlay } from './DocControlsOverlay.js';
7
- export { DocControlsBottom } from './DocControlsBottom.js';
8
- export { DocControlsSidebar } from './DocControlsSidebar.js';
9
- export { DocControlsSlideshow } from './DocControlsSlideshow.js';
10
- export { DocPlayerWithSidebar } from './DocPlayerWithSidebar.js';
11
- export { DocProgressBar } from './DocProgressBar.js';
12
- export { MarkdownRenderer } from './MarkdownRenderer.js';
13
- export { LinearDocView } from './LinearDocView.js';
14
- export { InlineVideoPlayer } from './InlineVideoPlayer.js';
15
- export { InlineAudioPlayer } from './InlineAudioPlayer.js';
16
- // Layer components
17
- export { ImageLayer } from './layers/ImageLayer.js';
18
- export { TextLayer } from './layers/TextLayer.js';
19
- export { ShapeLayer } from './layers/ShapeLayer.js';
20
- export { VideoLayer } from './layers/VideoLayer.js';
21
- export { TableLayer } from './layers/TableLayer.js';
22
- export { MapLayer } from './layers/MapLayer.js';
23
- // Hooks
24
- export { useAudioSync } from './hooks/useAudioSync.js';
25
- export { useDocPlayback } from './hooks/useDocPlayback.js';
26
- export { useViewportOrientation } from './hooks/useViewportOrientation.js';
27
- export { MediaContext, useMediaProvider, useMediaUrl } from './hooks/MediaContext.js';
28
- export { useAutoSurface } from './hooks/useAutoSurface.js';
29
- export { formatTime } from './types.js';
30
- // Utilities
31
- export { getAnimationStyle, getTransitionClass } from './utils/animationUtils.js';
32
- // JSON Form — read-only viewer
33
- export { JsonView } from './jsonView/index.js';
1
+ // src/DocPlayer.tsx
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
148
+ import { applySurface as applySurface2 } from "@bendyline/squisq/schemas";
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
+
156
+ // src/utils/animationUtils.ts
157
+ import {
158
+ getAnimationStyle,
159
+ getDefaultAnimationDuration,
160
+ getTransitionClass,
161
+ getAnimationProgress
162
+ } from "@bendyline/squisq/doc";
163
+
164
+ // src/utils/layerUtils.ts
165
+ function resolveValue(value, dimension) {
166
+ if (typeof value === "number") {
167
+ return value;
168
+ }
169
+ if (value.endsWith("%")) {
170
+ const percent = parseFloat(value);
171
+ return percent / 100 * dimension;
172
+ }
173
+ return parseFloat(value);
174
+ }
175
+ function getAnchorOffset(anchor, width, height) {
176
+ switch (anchor) {
177
+ case "center":
178
+ return { x: -width / 2, y: -height / 2 };
179
+ case "top-right":
180
+ return { x: -width, y: 0 };
181
+ case "bottom-left":
182
+ return { x: 0, y: -height };
183
+ case "bottom-right":
184
+ return { x: -width, y: -height };
185
+ case "top-left":
186
+ default:
187
+ return { x: 0, y: 0 };
188
+ }
189
+ }
190
+
191
+ // src/layers/ImageLayer.tsx
192
+ import { jsx as jsx2 } from "react/jsx-runtime";
193
+ function ImageLayer({ layer, basePath, viewport, blockTime }) {
194
+ const { content, position, animation } = layer;
195
+ const x = resolveValue(position.x, viewport.width);
196
+ const y = resolveValue(position.y, viewport.height);
197
+ const width = position.width ? resolveValue(position.width, viewport.width) : viewport.width;
198
+ const height = position.height ? resolveValue(position.height, viewport.height) : viewport.height;
199
+ const offset = getAnchorOffset(position.anchor, width, height);
200
+ const finalX = x + offset.x;
201
+ const finalY = y + offset.y;
202
+ const src = useMediaUrl(content.src, basePath);
203
+ const animStyle = getAnimationStyle(animation, blockTime);
204
+ const filter = cssFilterForTreatment(content.treatment, content.blur);
205
+ const preserveAspectRatio = getPreserveAspectRatio(content.fit);
206
+ const isCover = content.fit === "cover";
207
+ const isSpatialAnim = animation && SPATIAL_ANIMATION_TYPES.has(animation.type);
208
+ if (isCover && isSpatialAnim && animation) {
209
+ const kbAnim = remapToKenBurns(animation);
210
+ const kbStyle = getAnimationStyle(kbAnim, blockTime);
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(
212
+ "div",
213
+ {
214
+ style: {
215
+ width: `${width}px`,
216
+ height: `${height}px`,
217
+ overflow: "hidden"
218
+ },
219
+ children: /* @__PURE__ */ jsx2(
220
+ "img",
221
+ {
222
+ src,
223
+ alt: content.alt || "",
224
+ className: kbStyle.className,
225
+ style: {
226
+ width: `${width}px`,
227
+ height: `${height}px`,
228
+ objectFit: "cover",
229
+ objectPosition: "center",
230
+ display: "block",
231
+ pointerEvents: "none",
232
+ transformOrigin: "center center",
233
+ ...filter ? { filter } : {},
234
+ ...kbStyle.style
235
+ }
236
+ }
237
+ )
238
+ }
239
+ ) }) });
240
+ }
241
+ if (isCover) {
242
+ return /* @__PURE__ */ jsx2(
243
+ "g",
244
+ {
245
+ className: `block-layer block-layer--image ${animStyle.className}`,
246
+ style: animStyle.style,
247
+ "data-layer-id": layer.id,
248
+ children: /* @__PURE__ */ jsx2("foreignObject", { x: finalX, y: finalY, width, height, children: /* @__PURE__ */ jsx2(
249
+ "img",
250
+ {
251
+ src,
252
+ alt: content.alt || "",
253
+ style: {
254
+ width: `${width}px`,
255
+ height: `${height}px`,
256
+ objectFit: "cover",
257
+ objectPosition: "center",
258
+ display: "block",
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)" } : {}
264
+ }
265
+ }
266
+ ) })
267
+ }
268
+ );
269
+ }
270
+ return /* @__PURE__ */ jsx2(
271
+ "g",
272
+ {
273
+ className: `block-layer block-layer--image ${animStyle.className}`,
274
+ style: animStyle.style,
275
+ "data-layer-id": layer.id,
276
+ children: /* @__PURE__ */ jsx2(
277
+ "image",
278
+ {
279
+ href: src,
280
+ x: finalX,
281
+ y: finalY,
282
+ width,
283
+ height,
284
+ preserveAspectRatio,
285
+ style: { pointerEvents: "none", ...filter ? { filter } : {} }
286
+ }
287
+ )
288
+ }
289
+ );
290
+ }
291
+ function getPreserveAspectRatio(fit) {
292
+ switch (fit) {
293
+ case "cover":
294
+ return "xMidYMid slice";
295
+ case "fill":
296
+ return "none";
297
+ case "contain":
298
+ default:
299
+ return "xMidYMid meet";
300
+ }
301
+ }
302
+ var SPATIAL_ANIMATION_TYPES = /* @__PURE__ */ new Set(["panLeft", "panRight", "slowZoom", "zoomIn", "zoomOut"]);
303
+ function remapToKenBurns(anim) {
304
+ switch (anim.type) {
305
+ case "panLeft":
306
+ return { ...anim, type: "slowZoom", panDirection: "left" };
307
+ case "panRight":
308
+ return { ...anim, type: "slowZoom", panDirection: "right" };
309
+ case "zoomIn":
310
+ return { ...anim, type: "slowZoom", direction: "in" };
311
+ case "zoomOut":
312
+ return { ...anim, type: "slowZoom", direction: "out" };
313
+ default:
314
+ return anim;
315
+ }
316
+ }
317
+
318
+ // src/layers/TextLayer.tsx
319
+ import { useMemo as useMemo3 } from "react";
320
+ import { DEFAULT_DOC_FONT } from "@bendyline/squisq/schemas";
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 }) {
446
+ const { content, position, animation } = layer;
447
+ const { text, style } = content;
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;
515
+ const textAnchor = getTextAnchor(style.textAlign, 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);
520
+ const animStyle = getAnimationStyle(animation, blockTime);
521
+ const rawLines = (text ?? "").split("\n");
522
+ let lines = maxWidth ? rawLines.reduce(
523
+ (acc, line) => acc.concat(wrapText(line, style.fontSize, maxWidth)),
524
+ []
525
+ ) : rawLines;
526
+ if (style.maxLines && lines.length > style.maxLines) {
527
+ lines = lines.slice(0, style.maxLines);
528
+ const last = lines[lines.length - 1];
529
+ lines[lines.length - 1] = last.replace(/\s*$/, "") + "...";
530
+ }
531
+ const lineHeight = style.lineHeight || 1.4;
532
+ const lineHeightPx = style.fontSize * lineHeight;
533
+ const textStyles = {
534
+ fontSize: `${style.fontSize}px`,
535
+ fontFamily: style.fontFamily || DEFAULT_DOC_FONT,
536
+ fontWeight: style.fontWeight || "normal",
537
+ fontStyle: style.fontStyle || "normal",
538
+ fill: style.color,
539
+ ...animStyle.style
540
+ };
541
+ const filterId = style.shadow ? `shadow-${layer.id}` : void 0;
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,
546
+ {
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
+ }
560
+ }
561
+ ),
562
+ /* @__PURE__ */ jsx4(
563
+ "text",
564
+ {
565
+ x,
566
+ y,
567
+ textAnchor,
568
+ dominantBaseline,
569
+ style: textStyles,
570
+ filter: filterId ? `url(#${filterId})` : void 0,
571
+ children: lines.map((line, i) => /* @__PURE__ */ jsxs2("tspan", { x, dy: i === 0 ? 0 : lineHeightPx, children: [
572
+ line || "\xA0",
573
+ " "
574
+ ] }, i))
575
+ }
576
+ )
577
+ ] });
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
+ }
675
+ function getTextAnchor(align, anchor) {
676
+ if (align === "center") return "middle";
677
+ if (align === "right") return "end";
678
+ if (align === "left") return "start";
679
+ if (anchor?.includes("right")) return "end";
680
+ if (anchor === "center") return "middle";
681
+ return "start";
682
+ }
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";
687
+ if (anchor?.includes("bottom")) return "text-after-edge";
688
+ if (anchor === "center") return "middle";
689
+ return "text-before-edge";
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
+ }
710
+ function getTextBoxWidth(lines, style) {
711
+ const maxLineLength = Math.max(...lines.map((l) => l.length));
712
+ return maxLineLength * style.fontSize * 0.55;
713
+ }
714
+ function wrapText(text, fontSize, maxWidth) {
715
+ if (!text.trim()) return [""];
716
+ const avgCharWidth = fontSize * 0.5;
717
+ const charsPerLine = Math.floor(maxWidth / avgCharWidth);
718
+ if (charsPerLine <= 0) return [text];
719
+ const words = text.split(/\s+/);
720
+ const lines = [];
721
+ let currentLine = "";
722
+ for (const word of words) {
723
+ const testLine = currentLine ? `${currentLine} ${word}` : word;
724
+ if (testLine.length <= charsPerLine) {
725
+ currentLine = testLine;
726
+ } else {
727
+ if (currentLine) {
728
+ lines.push(currentLine);
729
+ }
730
+ if (word.length > charsPerLine) {
731
+ let remaining = word;
732
+ while (remaining.length > charsPerLine) {
733
+ lines.push(remaining.slice(0, charsPerLine));
734
+ remaining = remaining.slice(charsPerLine);
735
+ }
736
+ currentLine = remaining;
737
+ } else {
738
+ currentLine = word;
739
+ }
740
+ }
741
+ }
742
+ if (currentLine) {
743
+ lines.push(currentLine);
744
+ }
745
+ return lines.length > 0 ? lines : [""];
746
+ }
747
+
748
+ // src/layers/ShapeLayer.tsx
749
+ import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
750
+ function ShapeLayer({ layer, viewport, blockTime }) {
751
+ const { content, position, animation } = layer;
752
+ const rawX = resolveValue(position.x, viewport.width);
753
+ const rawY = resolveValue(position.y, viewport.height);
754
+ const width = position.width ? resolveValue(position.width, viewport.width) : 100;
755
+ const height = position.height ? resolveValue(position.height, viewport.height) : 100;
756
+ const anchorOffset = getAnchorOffset(position.anchor, width, height);
757
+ const x = rawX + anchorOffset.x;
758
+ const y = rawY + anchorOffset.y;
759
+ const animStyle = getAnimationStyle(animation, blockTime);
760
+ const fill = content.fill || "none";
761
+ const isCSSGradient = typeof fill === "string" && fill.includes("gradient(");
762
+ if (content.shape === "rect" && isCSSGradient && !content.gradient) {
763
+ return /* @__PURE__ */ jsx5(
764
+ "g",
765
+ {
766
+ className: `block-layer block-layer--shape ${animStyle.className}`,
767
+ style: animStyle.style,
768
+ "data-layer-id": layer.id,
769
+ children: /* @__PURE__ */ jsx5("foreignObject", { x, y, width, height, children: /* @__PURE__ */ jsx5(
770
+ "div",
771
+ {
772
+ style: {
773
+ width: `${width}px`,
774
+ height: `${height}px`,
775
+ background: fill,
776
+ borderRadius: content.borderRadius ? `${content.borderRadius}px` : void 0,
777
+ pointerEvents: "none"
778
+ }
779
+ }
780
+ ) })
781
+ }
782
+ );
783
+ }
784
+ const { fill: fillValue, def: fillDef } = resolveFill(
785
+ layer.id,
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,
795
+ stroke: content.stroke,
796
+ strokeWidth: content.strokeWidth,
797
+ strokeDasharray: dash,
798
+ ...filterAttr ? { filter: filterAttr } : {}
799
+ };
800
+ return /* @__PURE__ */ jsxs3(
801
+ "g",
802
+ {
803
+ className: `block-layer block-layer--shape ${animStyle.className}`,
804
+ style: animStyle.style,
805
+ "data-layer-id": layer.id,
806
+ children: [
807
+ (fillDef || filterDef) && /* @__PURE__ */ jsxs3("defs", { children: [
808
+ fillDef,
809
+ filterDef
810
+ ] }),
811
+ content.shape === "rect" && /* @__PURE__ */ jsx5(
812
+ "rect",
813
+ {
814
+ x,
815
+ y,
816
+ width,
817
+ height,
818
+ rx: content.borderRadius,
819
+ ry: content.borderRadius,
820
+ ...shapeProps
821
+ }
822
+ ),
823
+ content.shape === "circle" && /* @__PURE__ */ jsx5(
824
+ "circle",
825
+ {
826
+ cx: x + width / 2,
827
+ cy: y + height / 2,
828
+ r: Math.min(width, height) / 2,
829
+ ...shapeProps
830
+ }
831
+ ),
832
+ content.shape === "line" && /* @__PURE__ */ jsx5(
833
+ "line",
834
+ {
835
+ x1: x,
836
+ y1: y,
837
+ x2: x + width,
838
+ y2: y + height,
839
+ stroke: content.stroke || "#ffffff",
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
903
+ }
904
+ )
905
+ ]
906
+ }
907
+ );
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
+ }
939
+
940
+ // src/layers/MapLayer.tsx
941
+ import { useState as useState2, useEffect as useEffect3 } from "react";
942
+
943
+ // src/utils/mapTileUtils.ts
944
+ var TILE_PROVIDERS = {
945
+ terrain: {
946
+ url: "https://tile.opentopomap.org/{z}/{x}/{y}.png",
947
+ attribution: "Map: OpenTopoMap (CC-BY-SA)",
948
+ maxZoom: 17
949
+ },
950
+ road: {
951
+ url: "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
952
+ attribution: "\xA9 OpenStreetMap contributors",
953
+ maxZoom: 19
954
+ },
955
+ satellite: {
956
+ url: "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",
957
+ attribution: "Imagery: Esri, Maxar, Earthstar",
958
+ maxZoom: 18
959
+ },
960
+ toner: {
961
+ url: "https://tiles.stadiamaps.com/tiles/stamen_toner/{z}/{x}/{y}.png",
962
+ attribution: "Map: Stadia Maps, Stamen Design",
963
+ maxZoom: 20
964
+ },
965
+ watercolor: {
966
+ url: "https://tiles.stadiamaps.com/tiles/stamen_watercolor/{z}/{x}/{y}.jpg",
967
+ attribution: "Map: Stadia Maps, Stamen Design",
968
+ maxZoom: 16
969
+ }
970
+ };
971
+ function latLngToTile(lat, lng, zoom) {
972
+ const n = Math.pow(2, zoom);
973
+ const x = Math.floor((lng + 180) / 360 * n);
974
+ const latRad = lat * Math.PI / 180;
975
+ const y = Math.floor((1 - Math.log(Math.tan(latRad) + 1 / Math.cos(latRad)) / Math.PI) / 2 * n);
976
+ return { x, y };
977
+ }
978
+ function getPixelOffset(lat, lng, zoom, tileSize = 256) {
979
+ const n = Math.pow(2, zoom);
980
+ const xTile = (lng + 180) / 360 * n;
981
+ const latRad = lat * Math.PI / 180;
982
+ const yTile = (1 - Math.log(Math.tan(latRad) + 1 / Math.cos(latRad)) / Math.PI) / 2 * n;
983
+ return {
984
+ x: (xTile - Math.floor(xTile)) * tileSize,
985
+ y: (yTile - Math.floor(yTile)) * tileSize
986
+ };
987
+ }
988
+ function getTilesForViewport(centerLat, centerLng, zoom, viewportWidth, viewportHeight, tileSize = 256) {
989
+ const centerTile = latLngToTile(centerLat, centerLng, zoom);
990
+ const pixelOffset = getPixelOffset(centerLat, centerLng, zoom, tileSize);
991
+ const tilesX = Math.ceil(viewportWidth / tileSize) + 1;
992
+ const tilesY = Math.ceil(viewportHeight / tileSize) + 1;
993
+ const startX = centerTile.x - Math.floor(tilesX / 2);
994
+ const startY = centerTile.y - Math.floor(tilesY / 2);
995
+ const centerScreenX = viewportWidth / 2 - pixelOffset.x;
996
+ const centerScreenY = viewportHeight / 2 - pixelOffset.y;
997
+ const tiles = [];
998
+ for (let dy = 0; dy < tilesY; dy++) {
999
+ for (let dx = 0; dx < tilesX; dx++) {
1000
+ const tileX = startX + dx;
1001
+ const tileY = startY + dy;
1002
+ const screenX = centerScreenX + (tileX - centerTile.x) * tileSize;
1003
+ const screenY = centerScreenY + (tileY - centerTile.y) * tileSize;
1004
+ tiles.push({ x: tileX, y: tileY, screenX, screenY });
1005
+ }
1006
+ }
1007
+ return tiles;
1008
+ }
1009
+ function buildTileUrl(provider, x, y, z) {
1010
+ return provider.url.replace("{z}", String(z)).replace("{x}", String(x)).replace("{y}", String(y));
1011
+ }
1012
+ async function fetchTileImage(url) {
1013
+ return new Promise((resolve, reject) => {
1014
+ const img = new Image();
1015
+ img.crossOrigin = "anonymous";
1016
+ img.onload = () => resolve(img);
1017
+ img.onerror = () => reject(new Error(`Failed to load tile: ${url}`));
1018
+ img.src = url;
1019
+ });
1020
+ }
1021
+ async function composeMapImage(options) {
1022
+ const { center, zoom, style, width, height, markers = [], showAttribution = true } = options;
1023
+ const provider = TILE_PROVIDERS[style];
1024
+ const tileSize = provider.tileSize || 256;
1025
+ const clampedZoom = Math.min(zoom, provider.maxZoom);
1026
+ const canvas = document.createElement("canvas");
1027
+ canvas.width = width;
1028
+ canvas.height = height;
1029
+ const ctx = canvas.getContext("2d");
1030
+ if (!ctx) throw new Error("Failed to get canvas context");
1031
+ ctx.fillStyle = style === "toner" ? "#ffffff" : "#e5e7eb";
1032
+ ctx.fillRect(0, 0, width, height);
1033
+ const tiles = getTilesForViewport(center.lat, center.lng, clampedZoom, width, height, tileSize);
1034
+ const tilePromises = tiles.map(async (tile) => {
1035
+ const url = buildTileUrl(provider, tile.x, tile.y, clampedZoom);
1036
+ try {
1037
+ const img = await fetchTileImage(url);
1038
+ ctx.drawImage(img, tile.screenX, tile.screenY, tileSize, tileSize);
1039
+ } catch (err) {
1040
+ console.warn(`Tile load failed: ${url}`, err);
1041
+ }
1042
+ });
1043
+ await Promise.all(tilePromises);
1044
+ for (const marker of markers) {
1045
+ drawMarker(ctx, marker, center, clampedZoom, width, height, tileSize);
1046
+ }
1047
+ if (showAttribution) {
1048
+ drawAttribution(ctx, provider.attribution, width, height);
1049
+ }
1050
+ return canvas.toDataURL("image/png");
1051
+ }
1052
+ function drawMarker(ctx, marker, center, zoom, width, height, tileSize) {
1053
+ const centerTile = latLngToTile(center.lat, center.lng, zoom);
1054
+ const markerTile = latLngToTile(marker.lat, marker.lng, zoom);
1055
+ const centerOffset = getPixelOffset(center.lat, center.lng, zoom, tileSize);
1056
+ const markerOffset = getPixelOffset(marker.lat, marker.lng, zoom, tileSize);
1057
+ const dx = (markerTile.x - centerTile.x) * tileSize + (markerOffset.x - centerOffset.x);
1058
+ const dy = (markerTile.y - centerTile.y) * tileSize + (markerOffset.y - centerOffset.y);
1059
+ const screenX = width / 2 + dx;
1060
+ const screenY = height / 2 + dy;
1061
+ const color = marker.color || "#ef4444";
1062
+ const icon = marker.icon || "pin";
1063
+ ctx.save();
1064
+ if (icon === "pin") {
1065
+ ctx.fillStyle = color;
1066
+ ctx.beginPath();
1067
+ ctx.arc(screenX, screenY - 12, 8, Math.PI, 0, false);
1068
+ ctx.lineTo(screenX, screenY);
1069
+ ctx.closePath();
1070
+ ctx.fill();
1071
+ ctx.fillStyle = "#ffffff";
1072
+ ctx.beginPath();
1073
+ ctx.arc(screenX, screenY - 12, 3, 0, Math.PI * 2);
1074
+ ctx.fill();
1075
+ } else if (icon === "circle") {
1076
+ ctx.fillStyle = color;
1077
+ ctx.beginPath();
1078
+ ctx.arc(screenX, screenY, 8, 0, Math.PI * 2);
1079
+ ctx.fill();
1080
+ ctx.strokeStyle = "#ffffff";
1081
+ ctx.lineWidth = 2;
1082
+ ctx.stroke();
1083
+ } else if (icon === "star") {
1084
+ ctx.fillStyle = color;
1085
+ drawStar(ctx, screenX, screenY, 5, 10, 5);
1086
+ ctx.fill();
1087
+ }
1088
+ if (marker.label) {
1089
+ ctx.fillStyle = "#1f2937";
1090
+ ctx.font = "bold 12px system-ui, sans-serif";
1091
+ ctx.textAlign = "center";
1092
+ ctx.fillText(marker.label, screenX, screenY + 20);
1093
+ }
1094
+ ctx.restore();
1095
+ }
1096
+ function drawStar(ctx, cx, cy, spikes, outerRadius, innerRadius) {
1097
+ let rot = Math.PI / 2 * 3;
1098
+ const step = Math.PI / spikes;
1099
+ ctx.beginPath();
1100
+ ctx.moveTo(cx, cy - outerRadius);
1101
+ for (let i = 0; i < spikes; i++) {
1102
+ ctx.lineTo(cx + Math.cos(rot) * outerRadius, cy + Math.sin(rot) * outerRadius);
1103
+ rot += step;
1104
+ ctx.lineTo(cx + Math.cos(rot) * innerRadius, cy + Math.sin(rot) * innerRadius);
1105
+ rot += step;
1106
+ }
1107
+ ctx.lineTo(cx, cy - outerRadius);
1108
+ ctx.closePath();
1109
+ }
1110
+ function drawAttribution(ctx, text, width, height) {
1111
+ ctx.save();
1112
+ ctx.fillStyle = "rgba(255, 255, 255, 0.8)";
1113
+ const padding = 4;
1114
+ ctx.font = "10px system-ui, sans-serif";
1115
+ const textWidth = ctx.measureText(text).width;
1116
+ ctx.fillRect(width - textWidth - padding * 2 - 4, height - 16, textWidth + padding * 2, 14);
1117
+ ctx.fillStyle = "#374151";
1118
+ ctx.textAlign = "right";
1119
+ ctx.fillText(text, width - padding - 4, height - 5);
1120
+ ctx.restore();
1121
+ }
1122
+
1123
+ // src/layers/MapLayer.tsx
1124
+ import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
1125
+ function MapLayer({ layer, basePath, viewport, blockTime }) {
1126
+ const { content, position, animation } = layer;
1127
+ const [mapImageUrl, setMapImageUrl] = useState2(null);
1128
+ const [isLoading, setIsLoading] = useState2(true);
1129
+ const [error, setError] = useState2(null);
1130
+ const x = resolveValue(position.x, viewport.width);
1131
+ const y = resolveValue(position.y, viewport.height);
1132
+ const width = position.width ? resolveValue(position.width, viewport.width) : viewport.width;
1133
+ const height = position.height ? resolveValue(position.height, viewport.height) : viewport.height;
1134
+ const offset = getAnchorOffset(position.anchor, width, height);
1135
+ const finalX = x + offset.x;
1136
+ const finalY = y + offset.y;
1137
+ useEffect3(() => {
1138
+ let cancelled = false;
1139
+ if (content.staticSrc) {
1140
+ const src = content.staticSrc.startsWith("http") ? content.staticSrc : `${basePath}/${content.staticSrc}`;
1141
+ setMapImageUrl(src);
1142
+ setIsLoading(false);
1143
+ return;
1144
+ }
1145
+ setIsLoading(true);
1146
+ setError(null);
1147
+ composeMapImage({
1148
+ center: content.center,
1149
+ zoom: content.zoom,
1150
+ style: content.style,
1151
+ width,
1152
+ height,
1153
+ markers: content.markers,
1154
+ showAttribution: content.showAttribution !== false
1155
+ }).then((dataUrl) => {
1156
+ if (!cancelled) {
1157
+ setMapImageUrl(dataUrl);
1158
+ setIsLoading(false);
1159
+ }
1160
+ }).catch((err) => {
1161
+ if (!cancelled) {
1162
+ console.error("Failed to compose map:", err);
1163
+ setError(err instanceof Error ? err.message : String(err));
1164
+ setIsLoading(false);
1165
+ }
1166
+ });
1167
+ return () => {
1168
+ cancelled = true;
1169
+ };
1170
+ }, [
1171
+ content.center.lat,
1172
+ content.center.lng,
1173
+ content.zoom,
1174
+ content.style,
1175
+ content.staticSrc,
1176
+ width,
1177
+ height,
1178
+ basePath
1179
+ ]);
1180
+ const animStyle = getAnimationStyle(animation, blockTime);
1181
+ if (isLoading) {
1182
+ return /* @__PURE__ */ jsxs5(
1183
+ "g",
1184
+ {
1185
+ className: `block-layer block-layer--map ${animStyle.className}`,
1186
+ style: animStyle.style,
1187
+ "data-layer-id": layer.id,
1188
+ children: [
1189
+ /* @__PURE__ */ jsx7("rect", { x: finalX, y: finalY, width, height, fill: "#e5e7eb" }),
1190
+ /* @__PURE__ */ jsx7(
1191
+ "text",
1192
+ {
1193
+ x: finalX + width / 2,
1194
+ y: finalY + height / 2,
1195
+ textAnchor: "middle",
1196
+ dominantBaseline: "middle",
1197
+ fill: "#9ca3af",
1198
+ fontSize: "24",
1199
+ fontFamily: "system-ui, sans-serif",
1200
+ children: "Loading map..."
1201
+ }
1202
+ )
1203
+ ]
1204
+ }
1205
+ );
1206
+ }
1207
+ if (error || !mapImageUrl) {
1208
+ return /* @__PURE__ */ jsxs5(
1209
+ "g",
1210
+ {
1211
+ className: `block-layer block-layer--map ${animStyle.className}`,
1212
+ style: animStyle.style,
1213
+ "data-layer-id": layer.id,
1214
+ children: [
1215
+ /* @__PURE__ */ jsx7("rect", { x: finalX, y: finalY, width, height, fill: "#fef2f2" }),
1216
+ /* @__PURE__ */ jsx7(
1217
+ "text",
1218
+ {
1219
+ x: finalX + width / 2,
1220
+ y: finalY + height / 2,
1221
+ textAnchor: "middle",
1222
+ dominantBaseline: "middle",
1223
+ fill: "#dc2626",
1224
+ fontSize: "18",
1225
+ fontFamily: "system-ui, sans-serif",
1226
+ children: "Map failed to load"
1227
+ }
1228
+ )
1229
+ ]
1230
+ }
1231
+ );
1232
+ }
1233
+ return /* @__PURE__ */ jsxs5(
1234
+ "g",
1235
+ {
1236
+ className: `block-layer block-layer--map ${animStyle.className}`,
1237
+ style: animStyle.style,
1238
+ "data-layer-id": layer.id,
1239
+ children: [
1240
+ /* @__PURE__ */ jsx7("defs", { children: /* @__PURE__ */ jsx7("clipPath", { id: `clip-${layer.id}`, children: /* @__PURE__ */ jsx7("rect", { x: finalX, y: finalY, width, height }) }) }),
1241
+ /* @__PURE__ */ jsx7("g", { clipPath: `url(#clip-${layer.id})`, children: /* @__PURE__ */ jsx7(
1242
+ "image",
1243
+ {
1244
+ href: mapImageUrl,
1245
+ x: finalX,
1246
+ y: finalY,
1247
+ width,
1248
+ height,
1249
+ preserveAspectRatio: "xMidYMid slice",
1250
+ style: { pointerEvents: "none" }
1251
+ }
1252
+ ) })
1253
+ ]
1254
+ }
1255
+ );
1256
+ }
1257
+
1258
+ // src/layers/VideoLayer.tsx
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 }) {
1262
+ const { content, position } = layer;
1263
+ const videoRef = useRef2(null);
1264
+ const hasStartedRef = useRef2(false);
1265
+ const startAt = content.startAt ?? 0;
1266
+ const gated = blockTime < startAt;
1267
+ const x = resolveValue(position.x, viewport.width);
1268
+ const y = resolveValue(position.y, viewport.height);
1269
+ const width = position.width ? resolveValue(position.width, viewport.width) : viewport.width;
1270
+ const height = position.height ? resolveValue(position.height, viewport.height) : viewport.height;
1271
+ const offset = getAnchorOffset(position.anchor, width, height);
1272
+ const finalX = x + offset.x;
1273
+ const finalY = y + offset.y;
1274
+ const src = useMediaUrl(content.src, basePath);
1275
+ const resolvedPoster = useMediaUrl(content.posterSrc || "", basePath);
1276
+ const posterSrc = content.posterSrc ? resolvedPoster : void 0;
1277
+ useEffect4(() => {
1278
+ const video = videoRef.current;
1279
+ if (!video) return;
1280
+ video.currentTime = content.clipStart;
1281
+ hasStartedRef.current = true;
1282
+ if (isPlaying) {
1283
+ const playPromise = video.play();
1284
+ if (playPromise) {
1285
+ playPromise.catch(() => {
1286
+ });
1287
+ }
1288
+ }
1289
+ const handleTimeUpdate = () => {
1290
+ if (video.currentTime >= content.clipEnd) {
1291
+ video.pause();
1292
+ video.currentTime = content.clipEnd;
1293
+ }
1294
+ };
1295
+ video.addEventListener("timeupdate", handleTimeUpdate);
1296
+ return () => {
1297
+ video.removeEventListener("timeupdate", handleTimeUpdate);
1298
+ video.pause();
1299
+ };
1300
+ }, [content.src, content.clipStart, content.clipEnd]);
1301
+ useEffect4(() => {
1302
+ const video = videoRef.current;
1303
+ if (!video || !hasStartedRef.current) return;
1304
+ if (gated) {
1305
+ video.pause();
1306
+ video.currentTime = content.clipStart;
1307
+ return;
1308
+ }
1309
+ if (video.currentTime >= content.clipEnd) return;
1310
+ if (isPlaying) {
1311
+ const playPromise = video.play();
1312
+ if (playPromise) {
1313
+ playPromise.catch(() => {
1314
+ });
1315
+ }
1316
+ } else {
1317
+ video.pause();
1318
+ }
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(
1321
+ "video",
1322
+ {
1323
+ ref: videoRef,
1324
+ src,
1325
+ poster: posterSrc,
1326
+ muted: true,
1327
+ playsInline: true,
1328
+ preload: "auto",
1329
+ "data-clip-start": content.clipStart,
1330
+ "data-clip-end": content.clipEnd,
1331
+ "data-start-at": startAt,
1332
+ style: {
1333
+ width: `${width}px`,
1334
+ height: `${height}px`,
1335
+ objectFit: content.fit || "cover",
1336
+ objectPosition: "center",
1337
+ display: "block",
1338
+ pointerEvents: "none"
1339
+ }
1340
+ }
1341
+ ) }) });
1342
+ }
1343
+
1344
+ // src/layers/TableLayer.tsx
1345
+ import { jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
1346
+ function TableLayer({ layer, viewport, blockTime }) {
1347
+ const { content, position, animation } = layer;
1348
+ const { headers, rows, align, style } = content;
1349
+ const x = resolveValue(position.x, viewport.width);
1350
+ const y = resolveValue(position.y, viewport.height);
1351
+ const width = position.width ? resolveValue(position.width, viewport.width) : viewport.width;
1352
+ const height = position.height ? resolveValue(position.height, viewport.height) : viewport.height;
1353
+ const offset = getAnchorOffset(position.anchor, width, height);
1354
+ const finalX = x + offset.x;
1355
+ const finalY = y + offset.y;
1356
+ const animStyle = animation ? getAnimationStyle(animation, blockTime) : {};
1357
+ const cellAlign = (ci) => {
1358
+ const a = align?.[ci];
1359
+ return a ? { textAlign: a } : void 0;
1360
+ };
1361
+ const borderRadius = style.borderRadius ?? 8;
1362
+ return /* @__PURE__ */ jsx9("foreignObject", { x: finalX, y: finalY, width, height, style: animStyle, children: /* @__PURE__ */ jsx9(
1363
+ "div",
1364
+ {
1365
+ ...{ xmlns: "http://www.w3.org/1999/xhtml" },
1366
+ style: {
1367
+ width: `${width}px`,
1368
+ height: `${height}px`,
1369
+ display: "flex",
1370
+ alignItems: "center",
1371
+ justifyContent: "center",
1372
+ padding: "16px",
1373
+ boxSizing: "border-box"
1374
+ },
1375
+ children: /* @__PURE__ */ jsxs6(
1376
+ "table",
1377
+ {
1378
+ style: {
1379
+ width: "100%",
1380
+ borderCollapse: "separate",
1381
+ borderSpacing: 0,
1382
+ fontSize: `${style.fontSize}px`,
1383
+ fontFamily: style.fontFamily ?? "system-ui, sans-serif",
1384
+ overflow: "hidden",
1385
+ borderRadius: `${borderRadius}px`,
1386
+ border: `1px solid ${style.borderColor}`
1387
+ },
1388
+ children: [
1389
+ headers.length > 0 && /* @__PURE__ */ jsx9("thead", { children: /* @__PURE__ */ jsx9("tr", { children: headers.map((header, ci) => /* @__PURE__ */ jsx9(
1390
+ "th",
1391
+ {
1392
+ style: {
1393
+ background: style.headerBackground,
1394
+ color: style.headerColor,
1395
+ fontFamily: style.headerFontFamily ?? style.fontFamily ?? "system-ui, sans-serif",
1396
+ fontWeight: 600,
1397
+ padding: "12px 16px",
1398
+ borderBottom: `2px solid ${style.borderColor}`,
1399
+ borderRight: ci < headers.length - 1 ? `1px solid ${style.borderColor}` : void 0,
1400
+ ...cellAlign(ci)
1401
+ },
1402
+ children: header
1403
+ },
1404
+ ci
1405
+ )) }) }),
1406
+ rows.length > 0 && /* @__PURE__ */ jsx9("tbody", { children: rows.map((row, ri) => /* @__PURE__ */ jsx9("tr", { children: row.map((cell, ci) => /* @__PURE__ */ jsx9(
1407
+ "td",
1408
+ {
1409
+ style: {
1410
+ background: style.cellBackground,
1411
+ color: style.cellColor,
1412
+ padding: "10px 16px",
1413
+ borderBottom: ri < rows.length - 1 ? `1px solid ${style.borderColor}` : void 0,
1414
+ borderRight: ci < row.length - 1 ? `1px solid ${style.borderColor}` : void 0,
1415
+ ...cellAlign(ci)
1416
+ },
1417
+ children: cell
1418
+ },
1419
+ ci
1420
+ )) }, ri)) })
1421
+ ]
1422
+ }
1423
+ )
1424
+ }
1425
+ ) });
1426
+ }
1427
+
1428
+ // src/BlockRenderer.tsx
1429
+ import { jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
1430
+ var VIEWPORT = {
1431
+ width: 1920,
1432
+ height: 1080
1433
+ };
1434
+ function BlockRenderer({
1435
+ block,
1436
+ blockTime,
1437
+ basePath,
1438
+ isEntering = false,
1439
+ isExiting = false,
1440
+ transition,
1441
+ viewport = VIEWPORT,
1442
+ isPlaying
1443
+ }) {
1444
+ let transitionClass = "";
1445
+ const transitionStyle = {};
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`;
1453
+ }
1454
+ const clipId = `vb-clip-${block.id}`;
1455
+ return /* @__PURE__ */ jsxs7(
1456
+ "svg",
1457
+ {
1458
+ className: `block-svg ${transitionClass}`,
1459
+ style: transitionStyle,
1460
+ viewBox: `0 0 ${viewport.width} ${viewport.height}`,
1461
+ preserveAspectRatio: "xMidYMid meet",
1462
+ overflow: "hidden",
1463
+ "data-block-id": block.id,
1464
+ children: [
1465
+ /* @__PURE__ */ jsx10("defs", { children: /* @__PURE__ */ jsx10("clipPath", { id: clipId, children: /* @__PURE__ */ jsx10("rect", { x: "0", y: "0", width: viewport.width, height: viewport.height }) }) }),
1466
+ /* @__PURE__ */ jsx10("g", { clipPath: `url(#${clipId})`, children: (block.layers ?? []).map((layer) => /* @__PURE__ */ jsx10(
1467
+ LayerRenderer,
1468
+ {
1469
+ layer,
1470
+ basePath,
1471
+ viewport,
1472
+ blockTime,
1473
+ isPlaying
1474
+ },
1475
+ layer.id
1476
+ )) })
1477
+ ]
1478
+ }
1479
+ );
1480
+ }
1481
+ function LayerRenderer({ layer, basePath, viewport, blockTime, isPlaying }) {
1482
+ switch (layer.type) {
1483
+ case "image":
1484
+ return /* @__PURE__ */ jsx10(ImageLayer, { layer, basePath, viewport, blockTime });
1485
+ case "text":
1486
+ return /* @__PURE__ */ jsx10(TextLayer, { layer, viewport, blockTime });
1487
+ case "shape":
1488
+ return /* @__PURE__ */ jsx10(ShapeLayer, { layer, viewport, blockTime });
1489
+ case "path":
1490
+ return /* @__PURE__ */ jsx10(PathLayer, { layer, viewport, blockTime });
1491
+ case "map":
1492
+ return /* @__PURE__ */ jsx10(MapLayer, { layer, basePath, viewport, blockTime });
1493
+ case "video":
1494
+ return /* @__PURE__ */ jsx10(
1495
+ VideoLayer,
1496
+ {
1497
+ layer,
1498
+ basePath,
1499
+ viewport,
1500
+ blockTime,
1501
+ isPlaying
1502
+ }
1503
+ );
1504
+ case "table":
1505
+ return /* @__PURE__ */ jsx10(TableLayer, { layer, viewport, blockTime });
1506
+ default:
1507
+ console.warn(`Unknown layer type: ${layer.type}`);
1508
+ return null;
1509
+ }
1510
+ }
1511
+
1512
+ // src/CaptionOverlay.tsx
1513
+ import { getCaptionAtTime } from "@bendyline/squisq/schemas";
1514
+
1515
+ // src/SocialCaptionOverlay.tsx
1516
+ import { useMemo as useMemo4 } from "react";
1517
+ import { resolveFontFamily } from "@bendyline/squisq/schemas";
1518
+ import { jsx as jsx11 } from "react/jsx-runtime";
1519
+ var TARGET_CHUNK_SIZE = 4;
1520
+ var MIN_CHUNK_SIZE = 2;
1521
+ var MAX_CHUNK_SIZE = 6;
1522
+ function resolvePhraseTiming(phrase) {
1523
+ if (phrase.words && phrase.words.length > 0) {
1524
+ return phrase.words.map((w) => ({
1525
+ text: w.text,
1526
+ startTime: w.startTime,
1527
+ endTime: w.endTime
1528
+ }));
1529
+ }
1530
+ const rawWords = phrase.text.split(/\s+/).filter((w) => w.length > 0);
1531
+ if (rawWords.length === 0) return [];
1532
+ const duration = phrase.endTime - phrase.startTime;
1533
+ const wordDuration = duration / rawWords.length;
1534
+ return rawWords.map((text, i) => ({
1535
+ text,
1536
+ startTime: phrase.startTime + i * wordDuration,
1537
+ endTime: phrase.startTime + (i + 1) * wordDuration
1538
+ }));
1539
+ }
1540
+ function buildWordStream(captions) {
1541
+ const allWords = [];
1542
+ for (const phrase of captions.phrases) {
1543
+ allWords.push(...resolvePhraseTiming(phrase));
1544
+ }
1545
+ if (allWords.length === 0) return { words: [], chunks: [] };
1546
+ let chunkSize = TARGET_CHUNK_SIZE;
1547
+ if (allWords.length > chunkSize) {
1548
+ const numChunks = Math.ceil(allWords.length / chunkSize);
1549
+ chunkSize = Math.ceil(allWords.length / numChunks);
1550
+ chunkSize = Math.min(MAX_CHUNK_SIZE, Math.max(MIN_CHUNK_SIZE, chunkSize));
1551
+ } else {
1552
+ chunkSize = Math.max(MIN_CHUNK_SIZE, allWords.length);
1553
+ }
1554
+ const chunks = [];
1555
+ for (let i = 0; i < allWords.length; i += chunkSize) {
1556
+ const chunkWords = allWords.slice(i, i + chunkSize);
1557
+ chunks.push({
1558
+ words: chunkWords,
1559
+ startTime: chunkWords[0].startTime,
1560
+ endTime: chunkWords[chunkWords.length - 1].endTime
1561
+ });
1562
+ }
1563
+ return { words: allWords, chunks };
1564
+ }
1565
+ function SocialCaptionOverlay({
1566
+ captions,
1567
+ currentTime,
1568
+ enabled = true,
1569
+ theme,
1570
+ viewport
1571
+ }) {
1572
+ const { chunks } = useMemo4(
1573
+ () => captions ? buildWordStream(captions) : { words: [], chunks: [] },
1574
+ [captions]
1575
+ );
1576
+ if (!enabled || chunks.length === 0) {
1577
+ return /* @__PURE__ */ jsx11(
1578
+ "div",
1579
+ {
1580
+ className: "social-caption-overlay",
1581
+ style: {
1582
+ position: "absolute",
1583
+ bottom: "18%",
1584
+ left: 0,
1585
+ right: 0,
1586
+ zIndex: 50,
1587
+ pointerEvents: "none",
1588
+ opacity: 0,
1589
+ transition: "opacity 0.15s ease-in-out"
1590
+ }
1591
+ }
1592
+ );
1593
+ }
1594
+ let activeChunk = null;
1595
+ let activeWordIndex = -1;
1596
+ for (const chunk of chunks) {
1597
+ if (currentTime >= chunk.startTime && currentTime < chunk.endTime) {
1598
+ activeChunk = chunk;
1599
+ break;
1600
+ }
1601
+ }
1602
+ if (!activeChunk) {
1603
+ for (let i = 0; i < chunks.length - 1; i++) {
1604
+ if (currentTime >= chunks[i].endTime && currentTime < chunks[i + 1].startTime) {
1605
+ activeChunk = chunks[i];
1606
+ activeWordIndex = activeChunk.words.length - 1;
1607
+ break;
1608
+ }
1609
+ }
1610
+ if (!activeChunk && chunks.length > 0 && currentTime >= chunks[chunks.length - 1].startTime) {
1611
+ activeChunk = chunks[chunks.length - 1];
1612
+ activeWordIndex = activeChunk.words.length - 1;
1613
+ }
1614
+ }
1615
+ if (!activeChunk) return null;
1616
+ if (activeWordIndex === -1) {
1617
+ for (let i = 0; i < activeChunk.words.length; i++) {
1618
+ const word = activeChunk.words[i];
1619
+ if (currentTime >= word.startTime && currentTime < word.endTime) {
1620
+ activeWordIndex = i;
1621
+ break;
1622
+ }
1623
+ }
1624
+ if (activeWordIndex === -1) {
1625
+ for (let i = activeChunk.words.length - 1; i >= 0; i--) {
1626
+ if (currentTime >= activeChunk.words[i].startTime) {
1627
+ activeWordIndex = i;
1628
+ break;
1629
+ }
1630
+ }
1631
+ }
1632
+ if (activeWordIndex === -1) activeWordIndex = 0;
1633
+ }
1634
+ const primaryColor = theme?.colors?.primary ?? "#5b9bd5";
1635
+ const fontFamily = theme?.typography?.titleFont ? resolveFontFamily(theme.typography.titleFont, '"PT Serif", Georgia, serif') : '"PT Serif", Georgia, serif';
1636
+ const viewportHeight = viewport?.height ?? 720;
1637
+ const baseFontSize = Math.round(viewportHeight * 0.055);
1638
+ const fontSize = Math.max(24, Math.min(72, baseFontSize));
1639
+ return /* @__PURE__ */ jsx11(
1640
+ "div",
1641
+ {
1642
+ className: "social-caption-overlay",
1643
+ style: {
1644
+ position: "absolute",
1645
+ bottom: "18%",
1646
+ left: 0,
1647
+ right: 0,
1648
+ zIndex: 50,
1649
+ pointerEvents: "none",
1650
+ textAlign: "center",
1651
+ padding: "0 8%",
1652
+ boxSizing: "border-box",
1653
+ opacity: 1,
1654
+ transition: "opacity 0.15s ease-in-out"
1655
+ },
1656
+ children: /* @__PURE__ */ jsx11(
1657
+ "div",
1658
+ {
1659
+ style: {
1660
+ display: "inline-block",
1661
+ lineHeight: 1.3
1662
+ },
1663
+ children: activeChunk.words.map((word, i) => {
1664
+ const isActive = i === activeWordIndex;
1665
+ return /* @__PURE__ */ jsx11(
1666
+ "span",
1667
+ {
1668
+ style: {
1669
+ fontFamily,
1670
+ fontSize: `${fontSize}px`,
1671
+ fontWeight: isActive ? 800 : 600,
1672
+ color: isActive ? primaryColor : "rgba(255, 255, 255, 0.9)",
1673
+ textShadow: "0 2px 8px rgba(0,0,0,0.7), 0 0 20px rgba(0,0,0,0.4)",
1674
+ marginRight: i < activeChunk.words.length - 1 ? "0.3em" : void 0,
1675
+ transition: "color 0.1s ease, font-weight 0.1s ease",
1676
+ textTransform: "uppercase"
1677
+ },
1678
+ children: word.text
1679
+ },
1680
+ `${word.startTime}-${i}`
1681
+ );
1682
+ })
1683
+ }
1684
+ )
1685
+ }
1686
+ );
1687
+ }
1688
+
1689
+ // src/CaptionOverlay.tsx
1690
+ import { jsx as jsx12 } from "react/jsx-runtime";
1691
+ function CaptionOverlay({
1692
+ captions,
1693
+ currentTime,
1694
+ enabled = true,
1695
+ fontSize = 16,
1696
+ captionStyle = "standard",
1697
+ theme,
1698
+ viewport
1699
+ }) {
1700
+ if (captionStyle === "social") {
1701
+ return /* @__PURE__ */ jsx12(
1702
+ SocialCaptionOverlay,
1703
+ {
1704
+ captions,
1705
+ currentTime,
1706
+ enabled,
1707
+ theme,
1708
+ viewport
1709
+ }
1710
+ );
1711
+ }
1712
+ const phrase = enabled && captions ? getCaptionAtTime(captions, currentTime) : null;
1713
+ const captionText = phrase?.text ?? null;
1714
+ return /* @__PURE__ */ jsx12(
1715
+ "div",
1716
+ {
1717
+ className: "caption-overlay",
1718
+ style: {
1719
+ position: "absolute",
1720
+ top: "6px",
1721
+ left: "50%",
1722
+ transform: "translateX(-50%)",
1723
+ zIndex: 50,
1724
+ pointerEvents: "none",
1725
+ maxWidth: "100%",
1726
+ width: "100%",
1727
+ textAlign: "center",
1728
+ opacity: captionText ? 1 : 0,
1729
+ transition: "opacity 0.15s ease-in-out",
1730
+ padding: "0 4px",
1731
+ boxSizing: "border-box"
1732
+ },
1733
+ children: captionText && /* @__PURE__ */ jsx12(
1734
+ "div",
1735
+ {
1736
+ style: {
1737
+ display: "inline-block",
1738
+ padding: "3px 10px",
1739
+ background: "rgba(0, 0, 0, 0.65)",
1740
+ borderRadius: "4px",
1741
+ backdropFilter: "blur(4px)"
1742
+ },
1743
+ children: /* @__PURE__ */ jsx12(
1744
+ "span",
1745
+ {
1746
+ style: {
1747
+ color: "#ffffff",
1748
+ fontSize: `${fontSize}px`,
1749
+ fontFamily: "'Hanken Grotesk', system-ui, sans-serif",
1750
+ fontWeight: 500,
1751
+ lineHeight: 1.25,
1752
+ textShadow: "0 1px 3px rgba(0,0,0,0.5)",
1753
+ whiteSpace: "pre-wrap",
1754
+ wordWrap: "break-word"
1755
+ },
1756
+ children: captionText
1757
+ }
1758
+ )
1759
+ }
1760
+ )
1761
+ }
1762
+ );
1763
+ }
1764
+
1765
+ // src/hooks/useAutoSurface.ts
1766
+ import { useCallback, useMemo as useMemo5, useSyncExternalStore } from "react";
1767
+ import { DARK_SURFACE, LIGHT_SURFACE } from "@bendyline/squisq/schemas";
1768
+ var DARK_QUERY = "(prefers-color-scheme: dark)";
1769
+ var getServerSnapshot = () => LIGHT_SURFACE;
1770
+ function useAutoSurface(enabled) {
1771
+ const mql = useMemo5(
1772
+ () => enabled && typeof window !== "undefined" ? window.matchMedia(DARK_QUERY) : null,
1773
+ [enabled]
1774
+ );
1775
+ const subscribe = useCallback(
1776
+ (cb) => {
1777
+ if (!mql) return () => {
1778
+ };
1779
+ mql.addEventListener("change", cb);
1780
+ return () => mql.removeEventListener("change", cb);
1781
+ },
1782
+ [mql]
1783
+ );
1784
+ const getSnapshot = useCallback(() => mql?.matches ? DARK_SURFACE : LIGHT_SURFACE, [mql]);
1785
+ return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
1786
+ }
1787
+
1788
+ // src/hooks/useAudioSync.ts
1789
+ import { useState as useState3, useEffect as useEffect5, useRef as useRef3, useCallback as useCallback2 } from "react";
1790
+ function useAudioSync(audioRef, audioTrack, basePath = "") {
1791
+ const [currentTime, setCurrentTime] = useState3(0);
1792
+ const [isPlaying, setIsPlaying] = useState3(false);
1793
+ const [currentSegment, setCurrentSegment] = useState3(0);
1794
+ const [isEnded, setIsEnded] = useState3(false);
1795
+ const [isAudioReady, setIsAudioReady] = useState3(false);
1796
+ const [totalDuration, setTotalDuration] = useState3(0);
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(() => {
1804
+ if (!audioTrack?.segments) {
1805
+ return;
1806
+ }
1807
+ let time = 0;
1808
+ segmentStarts.current = audioTrack.segments.map((seg) => {
1809
+ const start = time;
1810
+ time += seg.duration;
1811
+ return start;
1812
+ });
1813
+ setTotalDuration(time);
1814
+ }, [audioTrack]);
1815
+ const preloadAudio = useCallback2(
1816
+ async (src) => {
1817
+ const audioUrl = basePath ? `${basePath}/${src}` : src;
1818
+ if (blobUrls.current.has(src)) {
1819
+ return blobUrls.current.get(src);
1820
+ }
1821
+ if (loadingPromises.current.has(src)) {
1822
+ return loadingPromises.current.get(src);
1823
+ }
1824
+ const loadPromise = (async () => {
1825
+ try {
1826
+ const response = await fetch(audioUrl);
1827
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
1828
+ const blob = await response.blob();
1829
+ const blobUrl = URL.createObjectURL(blob);
1830
+ blobUrls.current.set(src, blobUrl);
1831
+ return blobUrl;
1832
+ } catch {
1833
+ return audioUrl;
1834
+ } finally {
1835
+ loadingPromises.current.delete(src);
1836
+ }
1837
+ })();
1838
+ loadingPromises.current.set(src, loadPromise);
1839
+ return loadPromise;
1840
+ },
1841
+ [basePath]
1842
+ );
1843
+ useEffect5(() => {
1844
+ if (!audioTrack?.segments) return;
1845
+ audioTrack.segments.forEach((segment) => {
1846
+ preloadAudio(segment.src);
1847
+ });
1848
+ const currentBlobUrls = blobUrls.current;
1849
+ return () => {
1850
+ currentBlobUrls.forEach((url) => {
1851
+ URL.revokeObjectURL(url);
1852
+ });
1853
+ currentBlobUrls.clear();
1854
+ };
1855
+ }, [audioTrack, preloadAudio]);
1856
+ useEffect5(() => {
1857
+ const audio = audioRef.current;
1858
+ if (!audio) return;
1859
+ const handleTimeUpdate = () => {
1860
+ if (fallbackMode.current) return;
1861
+ const segmentStart = segmentStarts.current[currentSegment] || 0;
1862
+ const overallTime = segmentStart + audio.currentTime;
1863
+ setCurrentTime(overallTime);
1864
+ };
1865
+ const handlePlay = () => {
1866
+ setIsPlaying(true);
1867
+ };
1868
+ const handlePause = () => setIsPlaying(false);
1869
+ const handleError = () => {
1870
+ setIsAudioReady(true);
1871
+ };
1872
+ const handleEnded = () => {
1873
+ if (audioTrack && currentSegment < audioTrack.segments.length - 1) {
1874
+ shouldPlayAfterLoad.current = true;
1875
+ setCurrentSegment((prev) => prev + 1);
1876
+ } else {
1877
+ setIsEnded(true);
1878
+ setIsPlaying(false);
1879
+ }
1880
+ };
1881
+ audio.addEventListener("timeupdate", handleTimeUpdate);
1882
+ audio.addEventListener("play", handlePlay);
1883
+ audio.addEventListener("pause", handlePause);
1884
+ audio.addEventListener("ended", handleEnded);
1885
+ audio.addEventListener("error", handleError);
1886
+ return () => {
1887
+ audio.removeEventListener("timeupdate", handleTimeUpdate);
1888
+ audio.removeEventListener("play", handlePlay);
1889
+ audio.removeEventListener("pause", handlePause);
1890
+ audio.removeEventListener("ended", handleEnded);
1891
+ audio.removeEventListener("error", handleError);
1892
+ };
1893
+ }, [audioRef, currentSegment, audioTrack]);
1894
+ useEffect5(() => {
1895
+ const audio = audioRef.current;
1896
+ if (!audio || !audioTrack?.segments) return;
1897
+ const segment = audioTrack.segments[currentSegment];
1898
+ if (!segment) return;
1899
+ const applyPendingSeek = () => {
1900
+ if (pendingSeekTime.current !== null) {
1901
+ const segmentStart = segmentStarts.current[currentSegment] || 0;
1902
+ const segmentTime = pendingSeekTime.current - segmentStart;
1903
+ audio.currentTime = Math.max(0, segmentTime);
1904
+ setCurrentTime(pendingSeekTime.current);
1905
+ pendingSeekTime.current = null;
1906
+ }
1907
+ if (shouldPlayAfterLoad.current) {
1908
+ audio.play().catch(() => {
1909
+ });
1910
+ shouldPlayAfterLoad.current = false;
1911
+ }
1912
+ };
1913
+ const currentSrc = audio.src;
1914
+ const cachedBlobUrl = blobUrls.current.get(segment.src);
1915
+ const isSameSource = currentSrc && (currentSrc === cachedBlobUrl || currentSrc.endsWith(segment.src));
1916
+ if (!isSameSource) {
1917
+ const loadAndPlay = async () => {
1918
+ const blobUrl = await preloadAudio(segment.src);
1919
+ const handleCanPlay = () => {
1920
+ setIsAudioReady(true);
1921
+ applyPendingSeek();
1922
+ audio.removeEventListener("canplay", handleCanPlay);
1923
+ };
1924
+ audio.addEventListener("canplay", handleCanPlay);
1925
+ audio.src = blobUrl;
1926
+ audio.load();
1927
+ await Promise.resolve();
1928
+ if (audio.readyState >= 3) {
1929
+ audio.removeEventListener("canplay", handleCanPlay);
1930
+ setIsAudioReady(true);
1931
+ applyPendingSeek();
1932
+ }
1933
+ };
1934
+ loadAndPlay();
1935
+ } else {
1936
+ applyPendingSeek();
1937
+ }
1938
+ }, [audioRef, currentSegment, audioTrack, preloadAudio]);
1939
+ const play = useCallback2(() => {
1940
+ const audio = audioRef.current;
1941
+ if (audio) {
1942
+ if (isEnded) {
1943
+ setCurrentSegment(0);
1944
+ setIsEnded(false);
1945
+ }
1946
+ audio.play().then(() => {
1947
+ fallbackMode.current = false;
1948
+ }).catch(() => {
1949
+ fallbackMode.current = true;
1950
+ setIsPlaying(true);
1951
+ });
1952
+ }
1953
+ }, [audioRef, isEnded]);
1954
+ const pause = useCallback2(() => {
1955
+ const audio = audioRef.current;
1956
+ if (audio) {
1957
+ audio.pause();
1958
+ }
1959
+ setIsPlaying(false);
1960
+ }, [audioRef]);
1961
+ const toggle = useCallback2(() => {
1962
+ const audio = audioRef.current;
1963
+ if (!audio) return;
1964
+ if (!isPlaying) {
1965
+ play();
1966
+ } else {
1967
+ pause();
1968
+ }
1969
+ }, [audioRef, isPlaying, play, pause]);
1970
+ const seekTo = useCallback2(
1971
+ (time) => {
1972
+ const audio = audioRef.current;
1973
+ if (!audio || !audioTrack?.segments) return;
1974
+ const clampedTime = totalDuration > 0 ? Math.max(0, Math.min(time, totalDuration)) : Math.max(0, time);
1975
+ let segmentIndex = 0;
1976
+ let segmentStart = 0;
1977
+ for (let i = 0; i < audioTrack.segments.length; i++) {
1978
+ const segEnd = segmentStart + audioTrack.segments[i].duration;
1979
+ if (clampedTime < segEnd) {
1980
+ segmentIndex = i;
1981
+ break;
1982
+ }
1983
+ segmentStart = segEnd;
1984
+ if (i === audioTrack.segments.length - 1) {
1985
+ segmentIndex = i;
1986
+ }
1987
+ }
1988
+ const wasPlaying = !audio.paused;
1989
+ setIsEnded(false);
1990
+ if (segmentIndex !== currentSegment) {
1991
+ pendingSeekTime.current = clampedTime;
1992
+ shouldPlayAfterLoad.current = wasPlaying;
1993
+ setCurrentSegment(segmentIndex);
1994
+ } else {
1995
+ const segmentTime = clampedTime - segmentStart;
1996
+ audio.currentTime = Math.max(0, segmentTime);
1997
+ setCurrentTime(clampedTime);
1998
+ }
1999
+ },
2000
+ [audioRef, audioTrack, currentSegment, totalDuration]
2001
+ );
2002
+ const skipToSegment = useCallback2(
2003
+ (index) => {
2004
+ if (!audioTrack?.segments || index < 0 || index >= audioTrack.segments.length) {
2005
+ return;
2006
+ }
2007
+ setCurrentSegment(index);
2008
+ setIsEnded(false);
2009
+ },
2010
+ [audioTrack]
2011
+ );
2012
+ const restart = useCallback2(async () => {
2013
+ seekTo(0);
2014
+ await new Promise((resolve) => setTimeout(resolve, 50));
2015
+ play();
2016
+ }, [seekTo, play]);
2017
+ useEffect5(() => {
2018
+ if (!isPlaying || !fallbackMode.current || !totalDuration) return;
2019
+ let lastTime = performance.now();
2020
+ let raf;
2021
+ const tick = (now) => {
2022
+ if (!fallbackMode.current) return;
2023
+ const dt = (now - lastTime) / 1e3;
2024
+ lastTime = now;
2025
+ setCurrentTime((prev) => {
2026
+ const next = prev + dt;
2027
+ if (next >= totalDuration) {
2028
+ fallbackMode.current = false;
2029
+ setIsEnded(true);
2030
+ setIsPlaying(false);
2031
+ return totalDuration;
2032
+ }
2033
+ return next;
2034
+ });
2035
+ raf = requestAnimationFrame(tick);
2036
+ };
2037
+ raf = requestAnimationFrame(tick);
2038
+ return () => cancelAnimationFrame(raf);
2039
+ }, [isPlaying, totalDuration]);
2040
+ return {
2041
+ // State
2042
+ currentTime,
2043
+ isPlaying,
2044
+ currentSegment,
2045
+ totalDuration,
2046
+ isEnded,
2047
+ isReady: isAudioReady,
2048
+ isAvailable: true,
2049
+ // HTML5 audio is always available in browsers
2050
+ // Actions
2051
+ play: async () => play(),
2052
+ pause: async () => pause(),
2053
+ toggle: async () => toggle(),
2054
+ seekTo: async (time) => seekTo(time),
2055
+ skipToSegment: async (index) => skipToSegment(index),
2056
+ restart
2057
+ };
2058
+ }
2059
+
2060
+ // src/hooks/useDocPlayback.ts
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";
2068
+ import {
2069
+ expandDocBlocks,
2070
+ flattenRenderableBlocks,
2071
+ isTemplateBlock,
2072
+ resolvePersistentLayers,
2073
+ VIEWPORT_PRESETS
2074
+ } from "@bendyline/squisq/doc";
2075
+ function useDocPlayback(script, currentTime, viewport = VIEWPORT_PRESETS.landscape, renderMode = false, theme) {
2076
+ void renderMode;
2077
+ const blocks = useMemo6(() => {
2078
+ if (!script?.blocks) {
2079
+ return [];
2080
+ }
2081
+ const hasChildren = script.blocks.some((b) => b.children && b.children.length > 0);
2082
+ const flatBlocks = hasChildren ? flattenRenderableBlocks(script.blocks) : script.blocks;
2083
+ const hasTemplates = flatBlocks.some(isTemplateBlock);
2084
+ const resolvedTheme = theme ?? DEFAULT_THEME;
2085
+ const persistentLayers = resolvePersistentLayers(
2086
+ { persistentLayers: script.persistentLayers },
2087
+ resolvedTheme
2088
+ );
2089
+ if (hasTemplates) {
2090
+ const audioSegments = script.audio?.segments?.map((seg) => ({
2091
+ startTime: seg.startTime,
2092
+ duration: seg.duration
2093
+ }));
2094
+ const expanded = expandDocBlocks(flatBlocks, {
2095
+ audioSegments,
2096
+ viewport,
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
2104
+ });
2105
+ return expanded;
2106
+ }
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(
2121
+ () => currentBlock ? blocks.indexOf(currentBlock) : -1,
2122
+ [blocks, currentBlock]
2123
+ );
2124
+ const blockTime = useMemo6(() => {
2125
+ if (!currentBlock) return 0;
2126
+ return Math.max(0, currentTime - currentBlock.startTime);
2127
+ }, [currentBlock, currentTime]);
2128
+ const blockProgress = useMemo6(() => {
2129
+ if (!currentBlock || currentBlock.duration === 0) return 0;
2130
+ return Math.min(1, blockTime / currentBlock.duration);
2131
+ }, [currentBlock, blockTime]);
2132
+ const docProgress = useMemo6(() => {
2133
+ if (!script || script.duration === 0) return 0;
2134
+ return Math.min(1, currentTime / script.duration);
2135
+ }, [script, currentTime]);
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;
2149
+ const goToBlock = useCallback3(
2150
+ (index) => {
2151
+ if (!script || index < 0 || index >= blocks.length) return;
2152
+ const targetBlock = blocks[index];
2153
+ if (targetBlock) {
2154
+ return targetBlock.startTime;
2155
+ }
2156
+ },
2157
+ [script, blocks]
2158
+ );
2159
+ const nextBlock = useCallback3(() => {
2160
+ if (currentBlockIndex < blocks.length - 1) {
2161
+ return goToBlock(currentBlockIndex + 1);
2162
+ }
2163
+ }, [currentBlockIndex, blocks.length, goToBlock]);
2164
+ const prevBlock = useCallback3(() => {
2165
+ if (currentBlockIndex > 0) {
2166
+ return goToBlock(currentBlockIndex - 1);
2167
+ }
2168
+ }, [currentBlockIndex, goToBlock]);
2169
+ return {
2170
+ currentBlock,
2171
+ currentBlockIndex,
2172
+ previousBlock,
2173
+ isEntering,
2174
+ isExiting,
2175
+ blockTime,
2176
+ blockProgress,
2177
+ docProgress,
2178
+ nextBlock,
2179
+ prevBlock,
2180
+ goToBlock,
2181
+ /** Expanded blocks (templates converted to full blocks with layers) */
2182
+ blocks
2183
+ };
2184
+ }
2185
+
2186
+ // src/hooks/useViewportOrientation.ts
2187
+ import { useState as useState4, useEffect as useEffect6, useMemo as useMemo7 } from "react";
2188
+ import {
2189
+ VIEWPORT_PRESETS as VIEWPORT_PRESETS2
2190
+ } from "@bendyline/squisq/doc";
2191
+ function getOrientationFromWindow(width, height) {
2192
+ const ratio = width / height;
2193
+ if (ratio > 1.2) {
2194
+ return "landscape";
2195
+ } else if (ratio < 0.83) {
2196
+ return "portrait";
2197
+ } else {
2198
+ return "landscape";
2199
+ }
2200
+ }
2201
+ function getViewportForOrientation(orientation) {
2202
+ switch (orientation) {
2203
+ case "portrait":
2204
+ return VIEWPORT_PRESETS2.portrait;
2205
+ case "square":
2206
+ return VIEWPORT_PRESETS2.square;
2207
+ case "landscape":
2208
+ default:
2209
+ return VIEWPORT_PRESETS2.landscape;
2210
+ }
2211
+ }
2212
+ function useViewportOrientation() {
2213
+ const [windowSize, setWindowSize] = useState4(() => ({
2214
+ width: typeof window !== "undefined" ? window.innerWidth : 1920,
2215
+ height: typeof window !== "undefined" ? window.innerHeight : 1080
2216
+ }));
2217
+ useEffect6(() => {
2218
+ if (typeof window === "undefined") return;
2219
+ const handleResize = () => {
2220
+ setWindowSize({
2221
+ width: window.innerWidth,
2222
+ height: window.innerHeight
2223
+ });
2224
+ };
2225
+ let timeoutId;
2226
+ const debouncedResize = () => {
2227
+ clearTimeout(timeoutId);
2228
+ timeoutId = setTimeout(handleResize, 100);
2229
+ };
2230
+ window.addEventListener("resize", debouncedResize);
2231
+ return () => {
2232
+ window.removeEventListener("resize", debouncedResize);
2233
+ clearTimeout(timeoutId);
2234
+ };
2235
+ }, []);
2236
+ const orientation = useMemo7(
2237
+ () => getOrientationFromWindow(windowSize.width, windowSize.height),
2238
+ [windowSize.width, windowSize.height]
2239
+ );
2240
+ const viewport = useMemo7(() => getViewportForOrientation(orientation), [orientation]);
2241
+ return {
2242
+ viewport,
2243
+ orientation,
2244
+ windowSize
2245
+ };
2246
+ }
2247
+
2248
+ // src/DocPlayer.tsx
2249
+ import {
2250
+ expandCoverBlock,
2251
+ createTemplateContext,
2252
+ DEFAULT_THEME as DEFAULT_THEME3,
2253
+ VIEWPORT_PRESETS as VIEWPORT_PRESETS4
2254
+ } from "@bendyline/squisq/doc";
2255
+
2256
+ // src/DocProgressBar.tsx
2257
+ import { useRef as useRef5, useState as useState5, useCallback as useCallback4 } from "react";
2258
+
2259
+ // src/types.ts
2260
+ function formatTime(seconds) {
2261
+ const mins = Math.floor(seconds / 60);
2262
+ const secs = Math.floor(seconds % 60);
2263
+ return `${mins}:${secs.toString().padStart(2, "0")}`;
2264
+ }
2265
+
2266
+ // src/DocProgressBar.tsx
2267
+ import { jsx as jsx13, jsxs as jsxs8 } from "react/jsx-runtime";
2268
+ function DocProgressBar({
2269
+ state,
2270
+ actions,
2271
+ blockMarkers,
2272
+ expandedBlocks,
2273
+ getBlockTitle
2274
+ }) {
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;
2278
+ const handleProgressHover = useCallback4((e) => {
2279
+ const bar = progressBarRef.current;
2280
+ if (!bar) return;
2281
+ const rect = bar.getBoundingClientRect();
2282
+ const x = e.clientX - rect.left;
2283
+ const progress = Math.max(0, Math.min(1, x / rect.width));
2284
+ setHoverPosition(progress);
2285
+ }, []);
2286
+ const handleProgressLeave = useCallback4(() => {
2287
+ setHoverPosition(null);
2288
+ }, []);
2289
+ const getBlockAtTimeLocal = useCallback4(
2290
+ (time) => {
2291
+ for (let i = expandedBlocks.length - 1; i >= 0; i--) {
2292
+ const blk = expandedBlocks[i];
2293
+ if (time >= blk.startTime) {
2294
+ return { block: blk, index: i };
2295
+ }
2296
+ }
2297
+ return expandedBlocks.length > 0 ? { block: expandedBlocks[0], index: 0 } : null;
2298
+ },
2299
+ [expandedBlocks]
2300
+ );
2301
+ return /* @__PURE__ */ jsxs8(
2302
+ "div",
2303
+ {
2304
+ ref: progressBarRef,
2305
+ style: {
2306
+ flex: 1,
2307
+ height: "24px",
2308
+ cursor: "pointer",
2309
+ position: "relative",
2310
+ display: "flex",
2311
+ alignItems: "center"
2312
+ },
2313
+ onClick: (e) => {
2314
+ const rect = e.currentTarget.getBoundingClientRect();
2315
+ const x = e.clientX - rect.left;
2316
+ const progress = x / rect.width;
2317
+ actions.seekTo(progress * state.totalDuration);
2318
+ },
2319
+ onMouseMove: handleProgressHover,
2320
+ onMouseLeave: handleProgressLeave,
2321
+ children: [
2322
+ /* @__PURE__ */ jsx13(
2323
+ "div",
2324
+ {
2325
+ style: {
2326
+ position: "absolute",
2327
+ left: 0,
2328
+ right: 0,
2329
+ height: "6px",
2330
+ background: "rgba(255,255,255,0.2)",
2331
+ borderRadius: "3px"
2332
+ }
2333
+ }
2334
+ ),
2335
+ /* @__PURE__ */ jsx13(
2336
+ "div",
2337
+ {
2338
+ "data-testid": "doc-progress-fill",
2339
+ style: {
2340
+ position: "absolute",
2341
+ left: 0,
2342
+ width: `${playProgress * 100}%`,
2343
+ height: "6px",
2344
+ background: "#5b9bd5",
2345
+ borderRadius: "3px"
2346
+ }
2347
+ }
2348
+ ),
2349
+ blockMarkers.map((marker, i) => /* @__PURE__ */ jsx13(
2350
+ "div",
2351
+ {
2352
+ style: {
2353
+ position: "absolute",
2354
+ left: `${marker.position}%`,
2355
+ transform: "translateX(-50%)",
2356
+ width: "10px",
2357
+ height: "10px",
2358
+ borderRadius: "50%",
2359
+ background: marker.index === state.currentBlockIndex ? "#ffffff" : "rgba(255,255,255,0.5)",
2360
+ border: "2px solid #5b9bd5",
2361
+ cursor: "pointer",
2362
+ zIndex: 2,
2363
+ transition: "transform 0.15s, background 0.15s"
2364
+ },
2365
+ title: marker.title,
2366
+ onClick: (e) => {
2367
+ e.stopPropagation();
2368
+ actions.seekTo(marker.block.startTime);
2369
+ },
2370
+ onMouseEnter: (e) => {
2371
+ e.currentTarget.style.transform = "translateX(-50%) scale(1.3)";
2372
+ },
2373
+ onMouseLeave: (e) => {
2374
+ e.currentTarget.style.transform = "translateX(-50%)";
2375
+ }
2376
+ },
2377
+ `${marker.block.id}-${i}`
2378
+ )),
2379
+ hoverPosition !== null && /* @__PURE__ */ jsxs8(
2380
+ "div",
2381
+ {
2382
+ style: {
2383
+ position: "absolute",
2384
+ left: `${hoverPosition * 100}%`,
2385
+ bottom: "100%",
2386
+ transform: "translateX(-50%)",
2387
+ marginBottom: "8px",
2388
+ padding: "6px 10px",
2389
+ background: "rgba(0,0,0,0.9)",
2390
+ borderRadius: "4px",
2391
+ whiteSpace: "nowrap",
2392
+ pointerEvents: "none",
2393
+ zIndex: 10
2394
+ },
2395
+ children: [
2396
+ /* @__PURE__ */ jsx13("div", { style: { color: "white", fontSize: "12px", fontFamily: "monospace" }, children: formatTime(hoverPosition * state.totalDuration) }),
2397
+ (() => {
2398
+ const hoverTime = hoverPosition * state.totalDuration;
2399
+ const slideInfo = getBlockAtTimeLocal(hoverTime);
2400
+ if (slideInfo && getBlockTitle) {
2401
+ return /* @__PURE__ */ jsx13("div", { style: { color: "rgba(255,255,255,0.7)", fontSize: "11px", marginTop: "2px" }, children: getBlockTitle(slideInfo.block) });
2402
+ }
2403
+ return null;
2404
+ })()
2405
+ ]
2406
+ }
2407
+ ),
2408
+ hoverPosition !== null && /* @__PURE__ */ jsx13(
2409
+ "div",
2410
+ {
2411
+ style: {
2412
+ position: "absolute",
2413
+ left: `${hoverPosition * 100}%`,
2414
+ top: "50%",
2415
+ transform: "translate(-50%, -50%)",
2416
+ width: "2px",
2417
+ height: "16px",
2418
+ background: "rgba(255,255,255,0.6)",
2419
+ pointerEvents: "none",
2420
+ zIndex: 1
2421
+ }
2422
+ }
2423
+ )
2424
+ ]
2425
+ }
2426
+ );
2427
+ }
2428
+
2429
+ // src/DocControlsOverlay.tsx
2430
+ import { jsx as jsx14, jsxs as jsxs9 } from "react/jsx-runtime";
2431
+ function DocControlsOverlay({
2432
+ state,
2433
+ actions,
2434
+ blockMarkers,
2435
+ expandedBlocks,
2436
+ getBlockTitle
2437
+ }) {
2438
+ return /* @__PURE__ */ jsxs9(
2439
+ "div",
2440
+ {
2441
+ className: "doc-player__controls",
2442
+ style: {
2443
+ position: "absolute",
2444
+ bottom: 0,
2445
+ left: 0,
2446
+ right: 0,
2447
+ padding: "12px 16px",
2448
+ background: "linear-gradient(transparent, rgba(0,0,0,0.8))",
2449
+ display: "flex",
2450
+ alignItems: "center",
2451
+ gap: "8px",
2452
+ zIndex: 100
2453
+ },
2454
+ children: [
2455
+ /* @__PURE__ */ jsx14(
2456
+ "button",
2457
+ {
2458
+ onClick: actions.restart,
2459
+ style: {
2460
+ background: "none",
2461
+ border: "none",
2462
+ color: "rgba(255,255,255,0.7)",
2463
+ cursor: "pointer",
2464
+ padding: "8px",
2465
+ fontSize: "14px",
2466
+ display: "flex",
2467
+ alignItems: "center"
2468
+ },
2469
+ title: "Restart",
2470
+ "aria-label": "Restart from beginning",
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" }) })
2472
+ }
2473
+ ),
2474
+ /* @__PURE__ */ jsx14(
2475
+ "button",
2476
+ {
2477
+ onClick: actions.toggle,
2478
+ style: {
2479
+ background: "rgba(255,255,255,0.2)",
2480
+ border: "none",
2481
+ borderRadius: "50%",
2482
+ color: "white",
2483
+ cursor: "pointer",
2484
+ padding: "10px",
2485
+ fontSize: "16px",
2486
+ display: "flex",
2487
+ alignItems: "center",
2488
+ justifyContent: "center",
2489
+ width: "40px",
2490
+ height: "40px"
2491
+ },
2492
+ "aria-label": state.isPlaying ? "Pause" : "Play",
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" }) })
2494
+ }
2495
+ ),
2496
+ /* @__PURE__ */ jsxs9("span", { style: { color: "white", fontSize: "12px", minWidth: "80px", fontFamily: "monospace" }, children: [
2497
+ formatTime(state.currentTime),
2498
+ " / ",
2499
+ formatTime(state.totalDuration)
2500
+ ] }),
2501
+ /* @__PURE__ */ jsx14(
2502
+ DocProgressBar,
2503
+ {
2504
+ state,
2505
+ actions,
2506
+ blockMarkers,
2507
+ expandedBlocks,
2508
+ getBlockTitle
2509
+ }
2510
+ ),
2511
+ /* @__PURE__ */ jsxs9("span", { style: { color: "rgba(255,255,255,0.5)", fontSize: "11px", whiteSpace: "nowrap" }, children: [
2512
+ state.currentBlockIndex + 1,
2513
+ "/",
2514
+ state.totalBlocks
2515
+ ] }),
2516
+ state.hasCaptions && /* @__PURE__ */ jsxs9(
2517
+ "button",
2518
+ {
2519
+ onClick: () => actions.cycleCaptionMode(),
2520
+ style: {
2521
+ background: state.captionMode !== "off" ? "rgba(255,255,255,0.2)" : "none",
2522
+ border: "none",
2523
+ color: state.captionMode !== "off" ? "white" : "rgba(255,255,255,0.5)",
2524
+ cursor: "pointer",
2525
+ padding: "8px",
2526
+ fontSize: "12px",
2527
+ display: "flex",
2528
+ alignItems: "center",
2529
+ gap: "4px",
2530
+ borderRadius: "4px"
2531
+ },
2532
+ title: state.captionMode === "off" ? "Captions: Off (click for Standard)" : state.captionMode === "standard" ? "Captions: Standard (click for Social)" : "Captions: Social (click to turn off)",
2533
+ "aria-label": "Cycle caption style",
2534
+ children: [
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" })
2537
+ ]
2538
+ }
2539
+ ),
2540
+ actions.toggleFullscreen && /* @__PURE__ */ jsx14(
2541
+ "button",
2542
+ {
2543
+ onClick: actions.toggleFullscreen,
2544
+ style: {
2545
+ background: state.isFullscreen ? "rgba(255,255,255,0.2)" : "none",
2546
+ border: "none",
2547
+ color: state.isFullscreen ? "white" : "rgba(255,255,255,0.5)",
2548
+ cursor: "pointer",
2549
+ padding: "8px",
2550
+ display: "flex",
2551
+ alignItems: "center",
2552
+ borderRadius: "4px"
2553
+ },
2554
+ title: state.isFullscreen ? "Exit fullscreen (F)" : "Fullscreen (F)",
2555
+ "aria-label": state.isFullscreen ? "Exit fullscreen" : "Enter fullscreen",
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" }) })
2557
+ }
2558
+ )
2559
+ ]
2560
+ }
2561
+ );
2562
+ }
2563
+
2564
+ // src/DocControlsSlideshow.tsx
2565
+ import { jsx as jsx15, jsxs as jsxs10 } from "react/jsx-runtime";
2566
+ function DocControlsSlideshow({ state, slideNav }) {
2567
+ const { currentBlockIndex, totalBlocks } = state;
2568
+ const isFirst = currentBlockIndex <= 0;
2569
+ const isLast = currentBlockIndex >= totalBlocks - 1;
2570
+ return /* @__PURE__ */ jsxs10(
2571
+ "div",
2572
+ {
2573
+ className: "doc-controls-slideshow",
2574
+ "data-testid": "slideshow-controls",
2575
+ style: {
2576
+ position: "absolute",
2577
+ bottom: "16px",
2578
+ right: "16px",
2579
+ display: "flex",
2580
+ alignItems: "center",
2581
+ gap: "2px",
2582
+ background: "rgba(0, 0, 0, 0.65)",
2583
+ borderRadius: "8px",
2584
+ padding: "4px 6px",
2585
+ zIndex: 100,
2586
+ userSelect: "none",
2587
+ backdropFilter: "blur(8px)",
2588
+ WebkitBackdropFilter: "blur(8px)"
2589
+ },
2590
+ children: [
2591
+ /* @__PURE__ */ jsx15(
2592
+ "button",
2593
+ {
2594
+ onClick: (e) => {
2595
+ e.stopPropagation();
2596
+ slideNav.prevSlide();
2597
+ },
2598
+ disabled: isFirst,
2599
+ "data-testid": "slide-prev",
2600
+ "aria-label": "Previous slide",
2601
+ title: "Previous slide",
2602
+ style: {
2603
+ background: "none",
2604
+ border: "none",
2605
+ color: isFirst ? "rgba(255,255,255,0.3)" : "rgba(255,255,255,0.9)",
2606
+ cursor: isFirst ? "default" : "pointer",
2607
+ padding: "6px 8px",
2608
+ display: "flex",
2609
+ alignItems: "center",
2610
+ justifyContent: "center",
2611
+ borderRadius: "4px",
2612
+ transition: "background 0.15s"
2613
+ },
2614
+ onMouseEnter: (e) => {
2615
+ if (!isFirst) e.currentTarget.style.background = "rgba(255,255,255,0.1)";
2616
+ },
2617
+ onMouseLeave: (e) => {
2618
+ e.currentTarget.style.background = "none";
2619
+ },
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" }) })
2621
+ }
2622
+ ),
2623
+ /* @__PURE__ */ jsx15(
2624
+ "span",
2625
+ {
2626
+ "data-testid": "slide-counter",
2627
+ style: {
2628
+ color: "rgba(255,255,255,0.9)",
2629
+ fontSize: "13px",
2630
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
2631
+ fontVariantNumeric: "tabular-nums",
2632
+ minWidth: "48px",
2633
+ textAlign: "center",
2634
+ padding: "0 4px",
2635
+ letterSpacing: "0.02em"
2636
+ },
2637
+ children: totalBlocks > 0 ? `${currentBlockIndex + 1} / ${totalBlocks}` : "\u2014"
2638
+ }
2639
+ ),
2640
+ /* @__PURE__ */ jsx15(
2641
+ "button",
2642
+ {
2643
+ onClick: (e) => {
2644
+ e.stopPropagation();
2645
+ slideNav.nextSlide();
2646
+ },
2647
+ disabled: isLast,
2648
+ "data-testid": "slide-next",
2649
+ "aria-label": "Next slide",
2650
+ title: "Next slide",
2651
+ style: {
2652
+ background: "none",
2653
+ border: "none",
2654
+ color: isLast ? "rgba(255,255,255,0.3)" : "rgba(255,255,255,0.9)",
2655
+ cursor: isLast ? "default" : "pointer",
2656
+ padding: "6px 8px",
2657
+ display: "flex",
2658
+ alignItems: "center",
2659
+ justifyContent: "center",
2660
+ borderRadius: "4px",
2661
+ transition: "background 0.15s"
2662
+ },
2663
+ onMouseEnter: (e) => {
2664
+ if (!isLast) e.currentTarget.style.background = "rgba(255,255,255,0.1)";
2665
+ },
2666
+ onMouseLeave: (e) => {
2667
+ e.currentTarget.style.background = "none";
2668
+ },
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" }) })
2670
+ }
2671
+ )
2672
+ ]
2673
+ }
2674
+ );
2675
+ }
2676
+
2677
+ // src/LinearDocView.tsx
2678
+ import { useMemo as useMemo8 } from "react";
2679
+ import {
2680
+ applySurface,
2681
+ resolveFontFamily as resolveFontFamily2
2682
+ } from "@bendyline/squisq/schemas";
2683
+ import { VIEWPORT_PRESETS as VIEWPORT_PRESETS3 } from "@bendyline/squisq/schemas";
2684
+ import { getLayers, hasTemplate, DEFAULT_THEME as DEFAULT_THEME2, deriveTemplateInputs } from "@bendyline/squisq/doc";
2685
+ import { extractPlainText } from "@bendyline/squisq/markdown";
2686
+
2687
+ // src/MarkdownRenderer.tsx
2688
+ import { Fragment as Fragment2 } from "react";
2689
+ import {
2690
+ sanitizeHtmlNodes as sanitizeHtmlNodes2,
2691
+ sanitizeUrl
2692
+ } from "@bendyline/squisq/markdown";
2693
+
2694
+ // src/InlineVideoPlayer.tsx
2695
+ import { jsx as jsx16 } from "react/jsx-runtime";
2696
+ function InlineVideoPlayer({
2697
+ src,
2698
+ basePath = "",
2699
+ width,
2700
+ height,
2701
+ poster,
2702
+ controls = true,
2703
+ preload = "metadata",
2704
+ className
2705
+ }) {
2706
+ const resolvedSrc = useMediaUrl(src, basePath);
2707
+ const resolvedPoster = useMediaUrl(poster ?? "", basePath);
2708
+ const posterUrl = poster ? resolvedPoster : void 0;
2709
+ if (!resolvedSrc) return null;
2710
+ return /* @__PURE__ */ jsx16("span", { className: `squisq-inline-video-player ${className ?? ""}`.trim(), children: /* @__PURE__ */ jsx16(
2711
+ "video",
2712
+ {
2713
+ src: resolvedSrc,
2714
+ poster: posterUrl,
2715
+ controls,
2716
+ preload,
2717
+ width,
2718
+ height,
2719
+ playsInline: true
2720
+ }
2721
+ ) });
2722
+ }
2723
+
2724
+ // src/InlineAudioPlayer.tsx
2725
+ import { jsx as jsx17 } from "react/jsx-runtime";
2726
+ function InlineAudioPlayer({
2727
+ src,
2728
+ basePath = "",
2729
+ controls = true,
2730
+ preload = "metadata",
2731
+ className
2732
+ }) {
2733
+ const resolvedSrc = useMediaUrl(src, basePath);
2734
+ if (!resolvedSrc) return null;
2735
+ return /* @__PURE__ */ jsx17("span", { className: `squisq-inline-audio-player ${className ?? ""}`.trim(), children: /* @__PURE__ */ jsx17("audio", { src: resolvedSrc, controls, preload }) });
2736
+ }
2737
+
2738
+ // src/MarkdownRenderer.tsx
2739
+ import { jsx as jsx18, jsxs as jsxs11 } from "react/jsx-runtime";
2740
+ function renderInline(nodes, keyPrefix = "", htmlPolicy = "sanitize") {
2741
+ return nodes.map((node, i) => {
2742
+ const key = `${keyPrefix}i${i}`;
2743
+ switch (node.type) {
2744
+ case "text": {
2745
+ if (!node.value.includes("\n")) {
2746
+ return /* @__PURE__ */ jsx18(Fragment2, { children: node.value }, key);
2747
+ }
2748
+ const parts = node.value.split("\n");
2749
+ return /* @__PURE__ */ jsx18(Fragment2, { children: parts.map((part, j) => /* @__PURE__ */ jsxs11(Fragment2, { children: [
2750
+ j > 0 && /* @__PURE__ */ jsx18("br", {}),
2751
+ part
2752
+ ] }, j)) }, key);
2753
+ }
2754
+ case "emphasis":
2755
+ return /* @__PURE__ */ jsx18("em", { className: "squisq-md-em", children: renderInline(node.children, key, htmlPolicy) }, key);
2756
+ case "strong":
2757
+ return /* @__PURE__ */ jsx18("strong", { className: "squisq-md-strong", children: renderInline(node.children, key, htmlPolicy) }, key);
2758
+ case "delete":
2759
+ return /* @__PURE__ */ jsx18("del", { className: "squisq-md-del", children: renderInline(node.children, key, htmlPolicy) }, key);
2760
+ case "inlineCode":
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(
2768
+ "a",
2769
+ {
2770
+ className: "squisq-md-link",
2771
+ href,
2772
+ title: node.title ?? void 0,
2773
+ target: "_blank",
2774
+ rel: "noopener noreferrer",
2775
+ children: renderInline(node.children, key, htmlPolicy)
2776
+ },
2777
+ key
2778
+ );
2779
+ }
2780
+ case "image":
2781
+ return /* @__PURE__ */ jsx18(MdImage, { src: node.url, alt: node.alt ?? "", title: node.title ?? void 0 }, key);
2782
+ case "break":
2783
+ return /* @__PURE__ */ jsx18("br", {}, key);
2784
+ case "inlineMath":
2785
+ return /* @__PURE__ */ jsx18("code", { className: "squisq-md-inline-math", children: node.value }, key);
2786
+ case "htmlInline":
2787
+ if (htmlPolicy === "strip") return null;
2788
+ if (htmlPolicy === "trusted" && !containsMediaTag(node.htmlChildren) && !containsDangerousTag(node.htmlChildren) && !hasDangerousRawHtml(node.rawHtml)) {
2789
+ return /* @__PURE__ */ jsx18(
2790
+ "span",
2791
+ {
2792
+ className: "squisq-md-html-inline",
2793
+ dangerouslySetInnerHTML: { __html: node.rawHtml }
2794
+ },
2795
+ key
2796
+ );
2797
+ }
2798
+ return /* @__PURE__ */ jsx18("span", { className: "squisq-md-html-inline", children: renderHtmlNodes(resolveHtmlNodes(node.htmlChildren, htmlPolicy), `${key}h`) }, key);
2799
+ case "footnoteReference":
2800
+ return /* @__PURE__ */ jsx18("sup", { className: "squisq-md-footnote-ref", children: /* @__PURE__ */ jsxs11("a", { href: `#fn-${node.identifier}`, children: [
2801
+ "[",
2802
+ node.label ?? node.identifier,
2803
+ "]"
2804
+ ] }) }, key);
2805
+ case "linkReference":
2806
+ return /* @__PURE__ */ jsx18("span", { className: "squisq-md-link-ref", children: renderInline(node.children, key, htmlPolicy) }, key);
2807
+ case "imageReference":
2808
+ return /* @__PURE__ */ jsxs11("span", { className: "squisq-md-image-ref", children: [
2809
+ "[",
2810
+ node.alt ?? node.identifier,
2811
+ "]"
2812
+ ] }, key);
2813
+ case "textDirective":
2814
+ return /* @__PURE__ */ jsx18("span", { className: "squisq-md-text-directive", "data-directive": node.name, children: renderInline(node.children, key, htmlPolicy) }, key);
2815
+ case "mention":
2816
+ return /* @__PURE__ */ jsxs11(
2817
+ "span",
2818
+ {
2819
+ className: "squisq-md-mention mention",
2820
+ "data-mention": "true",
2821
+ "data-kind": node.targetKind,
2822
+ "data-id": node.targetId,
2823
+ "data-label": node.displayName,
2824
+ children: [
2825
+ "@",
2826
+ node.displayName
2827
+ ]
2828
+ },
2829
+ key
2830
+ );
2831
+ default:
2832
+ return null;
2833
+ }
2834
+ });
2835
+ }
2836
+ function renderBlock(node, key, htmlPolicy = "sanitize") {
2837
+ switch (node.type) {
2838
+ case "paragraph":
2839
+ return /* @__PURE__ */ jsx18("p", { className: "squisq-md-p", children: renderInline(node.children, key, htmlPolicy) }, key);
2840
+ case "heading": {
2841
+ const Tag = `h${node.depth}`;
2842
+ return /* @__PURE__ */ jsx18(Tag, { className: `squisq-md-heading squisq-md-h${node.depth}`, children: renderInline(node.children, key, htmlPolicy) }, key);
2843
+ }
2844
+ case "blockquote":
2845
+ return /* @__PURE__ */ jsx18("blockquote", { className: "squisq-md-blockquote", children: renderBlocks(node.children, key, htmlPolicy) }, key);
2846
+ case "list":
2847
+ if (node.ordered) {
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);
2849
+ }
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);
2851
+ case "code":
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);
2853
+ case "thematicBreak":
2854
+ return /* @__PURE__ */ jsx18("hr", { className: "squisq-md-hr" }, key);
2855
+ case "table":
2856
+ return renderTable(node.children, node.align, key, htmlPolicy);
2857
+ case "htmlBlock":
2858
+ if (htmlPolicy === "strip") return null;
2859
+ if (htmlPolicy === "trusted" && !containsMediaTag(node.htmlChildren) && !containsDangerousTag(node.htmlChildren) && !hasDangerousRawHtml(node.rawHtml)) {
2860
+ return /* @__PURE__ */ jsx18(
2861
+ "div",
2862
+ {
2863
+ className: "squisq-md-html-block",
2864
+ dangerouslySetInnerHTML: { __html: node.rawHtml }
2865
+ },
2866
+ key
2867
+ );
2868
+ }
2869
+ return /* @__PURE__ */ jsx18("div", { className: "squisq-md-html-block", children: renderHtmlNodes(resolveHtmlNodes(node.htmlChildren, htmlPolicy), `${key}h`) }, key);
2870
+ case "math":
2871
+ return /* @__PURE__ */ jsx18("pre", { className: "squisq-md-math-block", children: /* @__PURE__ */ jsx18("code", { children: node.value }) }, key);
2872
+ case "definition":
2873
+ return null;
2874
+ case "footnoteDefinition":
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)
2878
+ ] }, key);
2879
+ case "containerDirective":
2880
+ return /* @__PURE__ */ jsxs11(
2881
+ "div",
2882
+ {
2883
+ className: `squisq-md-directive squisq-md-directive-${node.name}`,
2884
+ "data-directive": node.name,
2885
+ children: [
2886
+ node.label && /* @__PURE__ */ jsx18("div", { className: "squisq-md-directive-label", children: node.label }),
2887
+ renderBlocks(node.children, key, htmlPolicy)
2888
+ ]
2889
+ },
2890
+ key
2891
+ );
2892
+ case "leafDirective":
2893
+ return /* @__PURE__ */ jsx18(
2894
+ "div",
2895
+ {
2896
+ className: `squisq-md-directive squisq-md-directive-${node.name}`,
2897
+ "data-directive": node.name,
2898
+ children: renderInline(node.children, key, htmlPolicy)
2899
+ },
2900
+ key
2901
+ );
2902
+ case "definitionList":
2903
+ return /* @__PURE__ */ jsx18("dl", { className: "squisq-md-dl", children: node.children.map((child, i) => {
2904
+ if (child.type === "definitionTerm") {
2905
+ return /* @__PURE__ */ jsx18("dt", { className: "squisq-md-dt", children: renderInline(child.children, `${key}dt${i}`, htmlPolicy) }, `${key}dt${i}`);
2906
+ }
2907
+ return /* @__PURE__ */ jsx18("dd", { className: "squisq-md-dd", children: renderBlocks(child.children, `${key}dd${i}`, htmlPolicy) }, `${key}dd${i}`);
2908
+ }) }, key);
2909
+ default:
2910
+ return null;
2911
+ }
2912
+ }
2913
+ function renderListItem(item, key, htmlPolicy = "sanitize") {
2914
+ const isTask = item.checked !== null && item.checked !== void 0;
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)
2918
+ ] }, key);
2919
+ }
2920
+ function renderTable(rows, align, key, htmlPolicy = "sanitize") {
2921
+ const [headerRow, ...bodyRows] = rows;
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(
2924
+ "th",
2925
+ {
2926
+ className: "squisq-md-th",
2927
+ style: align?.[ci] ? { textAlign: align[ci] } : void 0,
2928
+ children: renderInline(cell.children, `${key}th${ci}`, htmlPolicy)
2929
+ },
2930
+ `${key}th${ci}`
2931
+ )) }) }),
2932
+ bodyRows.length > 0 && /* @__PURE__ */ jsx18("tbody", { children: bodyRows.map((row, ri) => /* @__PURE__ */ jsx18("tr", { children: row.children.map((cell, ci) => /* @__PURE__ */ jsx18(
2933
+ "td",
2934
+ {
2935
+ className: "squisq-md-td",
2936
+ style: align?.[ci] ? { textAlign: align[ci] } : void 0,
2937
+ children: renderInline(cell.children, `${key}td${ri}-${ci}`, htmlPolicy)
2938
+ },
2939
+ `${key}td${ri}-${ci}`
2940
+ )) }, `${key}tr${ri}`)) })
2941
+ ] }, key);
2942
+ }
2943
+ function renderBlocks(nodes, keyPrefix = "", htmlPolicy = "sanitize") {
2944
+ return nodes.map((node, i) => renderBlock(node, `${keyPrefix}b${i}`, htmlPolicy));
2945
+ }
2946
+ function MdImage({ src, 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);
2956
+ }
2957
+ function containsMediaTag(nodes) {
2958
+ for (const node of nodes) {
2959
+ if (node.type !== "htmlElement") continue;
2960
+ const tagName = node.tagName.toLowerCase();
2961
+ if (tagName === "video" || tagName === "audio") return true;
2962
+ if (containsMediaTag(node.children)) return true;
2963
+ }
2964
+ return false;
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
+ }
2989
+ var PASSTHROUGH_ATTRS = {
2990
+ // common
2991
+ class: "className",
2992
+ id: "id",
2993
+ title: "title",
2994
+ style: "style",
2995
+ // media-adjacent (used when video/audio appear inside other wrappers)
2996
+ width: "width",
2997
+ height: "height",
2998
+ src: "src",
2999
+ alt: "alt",
3000
+ loading: "loading",
3001
+ decoding: "decoding",
3002
+ // anchor
3003
+ href: "href",
3004
+ target: "target",
3005
+ rel: "rel"
3006
+ };
3007
+ function reactPropsFromAttrs(attrs) {
3008
+ const out = {};
3009
+ for (const [name, value] of Object.entries(attrs)) {
3010
+ const propName = PASSTHROUGH_ATTRS[name];
3011
+ if (!propName) continue;
3012
+ if (propName === "style") {
3013
+ out["data-style"] = value;
3014
+ continue;
3015
+ }
3016
+ out[propName] = value;
3017
+ }
3018
+ return out;
3019
+ }
3020
+ function renderHtmlElement(el, key) {
3021
+ const tagName = el.tagName.toLowerCase();
3022
+ if (DANGEROUS_HTML_TAGS.has(tagName)) return null;
3023
+ if (tagName === "video") {
3024
+ return /* @__PURE__ */ jsx18(
3025
+ InlineVideoPlayer,
3026
+ {
3027
+ src: el.attributes.src ?? "",
3028
+ width: el.attributes.width,
3029
+ height: el.attributes.height,
3030
+ poster: el.attributes.poster,
3031
+ controls: "controls" in el.attributes,
3032
+ preload: el.attributes.preload === "none" || el.attributes.preload === "metadata" || el.attributes.preload === "auto" ? el.attributes.preload : void 0
3033
+ },
3034
+ key
3035
+ );
3036
+ }
3037
+ if (tagName === "audio") {
3038
+ return /* @__PURE__ */ jsx18(
3039
+ InlineAudioPlayer,
3040
+ {
3041
+ src: el.attributes.src ?? "",
3042
+ controls: "controls" in el.attributes,
3043
+ preload: el.attributes.preload === "none" || el.attributes.preload === "metadata" || el.attributes.preload === "auto" ? el.attributes.preload : void 0
3044
+ },
3045
+ key
3046
+ );
3047
+ }
3048
+ const Tag = tagName;
3049
+ const props = reactPropsFromAttrs(el.attributes);
3050
+ if (el.selfClosing) {
3051
+ return /* @__PURE__ */ jsx18(Tag, { ...props }, key);
3052
+ }
3053
+ return /* @__PURE__ */ jsx18(Tag, { ...props, children: renderHtmlNodes(el.children, `${key}c`) }, key);
3054
+ }
3055
+ function renderHtmlNodes(nodes, keyPrefix) {
3056
+ return nodes.map((node, i) => {
3057
+ const key = `${keyPrefix}${i}`;
3058
+ switch (node.type) {
3059
+ case "htmlElement":
3060
+ return renderHtmlElement(node, key);
3061
+ case "htmlText":
3062
+ return /* @__PURE__ */ jsx18(Fragment2, { children: node.value }, key);
3063
+ case "htmlComment":
3064
+ return null;
3065
+ default:
3066
+ return null;
3067
+ }
3068
+ });
3069
+ }
3070
+ function MarkdownRenderer({
3071
+ nodes,
3072
+ className,
3073
+ htmlPolicy = "sanitize"
3074
+ }) {
3075
+ if (!nodes || nodes.length === 0) return null;
3076
+ return /* @__PURE__ */ jsx18("div", { className: `squisq-md ${className || ""}`, children: renderBlocks(nodes, "", htmlPolicy) });
3077
+ }
3078
+
3079
+ // src/LinearDocView.tsx
3080
+ import { jsx as jsx19, jsxs as jsxs12 } from "react/jsx-runtime";
3081
+ function isAnnotatedBlock(block) {
3082
+ const annotation = block.sourceHeading?.templateAnnotation;
3083
+ if (!annotation) return false;
3084
+ return !!annotation.template && hasTemplate(annotation.template);
3085
+ }
3086
+ function countAll(blocks) {
3087
+ let count = 0;
3088
+ for (const b of blocks) {
3089
+ count++;
3090
+ if (b.children) count += countAll(b.children);
3091
+ }
3092
+ return count;
3093
+ }
3094
+ function BlockSection({ block, basePath, viewport, renderContext, blockIndex }) {
3095
+ const isAnnotated = isAnnotatedBlock(block);
3096
+ const visualBlock = useMemo8(() => {
3097
+ if (!isAnnotated) return null;
3098
+ const annotation = block.sourceHeading.templateAnnotation;
3099
+ const headingText = extractPlainText(block.sourceHeading);
3100
+ const templateBlock = {
3101
+ id: block.id,
3102
+ template: annotation.template,
3103
+ startTime: 0,
3104
+ duration: 1,
3105
+ audioSegment: 0,
3106
+ title: headingText,
3107
+ ...deriveTemplateInputs(
3108
+ annotation.template ?? "sectionHeader",
3109
+ headingText,
3110
+ block.contents,
3111
+ {
3112
+ placeholders: true
3113
+ }
3114
+ ) ?? {},
3115
+ ...annotation.params,
3116
+ ...block.templateOverrides
3117
+ };
3118
+ const ctx = {
3119
+ ...renderContext,
3120
+ blockIndex
3121
+ };
3122
+ const layers = getLayers(templateBlock, ctx);
3123
+ return {
3124
+ ...block,
3125
+ layers,
3126
+ template: annotation.template
3127
+ };
3128
+ }, [block, isAnnotated, renderContext, blockIndex]);
3129
+ return /* @__PURE__ */ jsxs12(
3130
+ "div",
3131
+ {
3132
+ className: "squisq-linear-section",
3133
+ "data-block-id": block.id,
3134
+ "data-template": isAnnotated ? block.sourceHeading?.templateAnnotation?.template : void 0,
3135
+ children: [
3136
+ block.sourceHeading && !isAnnotated && /* @__PURE__ */ jsx19(MarkdownRenderer, { nodes: [block.sourceHeading] }),
3137
+ isAnnotated && visualBlock && /* @__PURE__ */ jsx19("div", { className: "squisq-linear-card", children: /* @__PURE__ */ jsx19(
3138
+ "div",
3139
+ {
3140
+ className: "squisq-linear-card-svg",
3141
+ style: {
3142
+ width: "100%",
3143
+ aspectRatio: `${viewport.width} / ${viewport.height}`,
3144
+ overflow: "hidden",
3145
+ marginBottom: "1em"
3146
+ },
3147
+ children: /* @__PURE__ */ jsx19(
3148
+ BlockRenderer,
3149
+ {
3150
+ block: visualBlock,
3151
+ blockTime: 0,
3152
+ basePath,
3153
+ viewport
3154
+ }
3155
+ )
3156
+ }
3157
+ ) }),
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(
3160
+ BlockSection,
3161
+ {
3162
+ block: child,
3163
+ basePath,
3164
+ viewport,
3165
+ renderContext,
3166
+ blockIndex: blockIndex + i + 1
3167
+ },
3168
+ child.id
3169
+ )) })
3170
+ ]
3171
+ }
3172
+ );
3173
+ }
3174
+ function LinearDocView({
3175
+ doc,
3176
+ basePath = "/",
3177
+ viewport,
3178
+ className,
3179
+ theme,
3180
+ surface,
3181
+ thinMargins = false,
3182
+ imageDisplayMode = "inline"
3183
+ }) {
3184
+ const activeViewport = viewport ?? VIEWPORT_PRESETS3.landscape;
3185
+ const totalBlocks = useMemo8(() => countAll(doc.blocks), [doc.blocks]);
3186
+ const autoSurface = useAutoSurface(surface === "auto");
3187
+ const resolvedSurface = surface === "auto" ? autoSurface : surface;
3188
+ const renderContext = useMemo8(() => {
3189
+ const baseTheme = theme ?? DEFAULT_THEME2;
3190
+ const effectiveTheme = resolvedSurface ? applySurface(baseTheme, resolvedSurface) : baseTheme;
3191
+ return {
3192
+ theme: effectiveTheme,
3193
+ viewport: activeViewport,
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
3198
+ };
3199
+ }, [activeViewport, totalBlocks, theme, resolvedSurface]);
3200
+ const activeTheme = renderContext.theme;
3201
+ const bgColor = activeTheme.colors.background;
3202
+ const textColor = activeTheme.colors.text;
3203
+ const mutedColor = activeTheme.colors.textMuted;
3204
+ const primaryColor = activeTheme.colors.primary;
3205
+ const bodyFont = resolveFontFamily2(activeTheme.typography.bodyFont, "system-ui, sans-serif");
3206
+ const titleFont = resolveFontFamily2(activeTheme.typography.titleFont, "Georgia, serif");
3207
+ const lineHt = activeTheme.typography.lineHeight ?? 1.7;
3208
+ return /* @__PURE__ */ jsx19(
3209
+ "div",
3210
+ {
3211
+ className: `squisq-linear ${className || ""}`,
3212
+ style: {
3213
+ width: "100%",
3214
+ // Thin-margins mode is the "embedded in someone else's container"
3215
+ // signal (chat bubble, sidebar preview). Fit to content there so
3216
+ // the host's bubble doesn't render a tall empty box when the doc
3217
+ // is short. Standalone mode keeps height:100% for full-viewport
3218
+ // scrolling.
3219
+ height: thinMargins ? "auto" : "100%",
3220
+ overflowY: thinMargins ? "visible" : "auto",
3221
+ overflowX: "hidden",
3222
+ background: bgColor
3223
+ },
3224
+ children: /* @__PURE__ */ jsxs12(
3225
+ "div",
3226
+ {
3227
+ className: `squisq-linear-content squisq-md${thinMargins ? " squisq-linear-content--thin" : ""}${imageDisplayMode === "thumbnail" ? " squisq-linear-content--thumbnail-images" : ""}`,
3228
+ style: {
3229
+ // Thin-margins mode drops the 720px reading column + generous
3230
+ // page padding (right for standalone docs) in favor of a tight
3231
+ // layout that hugs its container (right for chat bubbles and
3232
+ // sidebar previews).
3233
+ maxWidth: thinMargins ? "none" : "720px",
3234
+ margin: thinMargins ? "0" : "0 auto",
3235
+ padding: thinMargins ? "0" : "24px 16px",
3236
+ lineHeight: lineHt,
3237
+ fontSize: "16px",
3238
+ fontFamily: bodyFont,
3239
+ color: textColor,
3240
+ // CSS custom properties for MarkdownRenderer / nested elements
3241
+ "--squisq-linear-title-font": titleFont,
3242
+ "--squisq-linear-body-font": bodyFont,
3243
+ "--squisq-linear-text": textColor,
3244
+ "--squisq-linear-muted": mutedColor,
3245
+ "--squisq-linear-primary": primaryColor,
3246
+ "--squisq-linear-bg": bgColor
3247
+ },
3248
+ children: [
3249
+ /* @__PURE__ */ jsx19("style", { children: `
3250
+ .squisq-linear-content h1,
3251
+ .squisq-linear-content h2,
3252
+ .squisq-linear-content h3,
3253
+ .squisq-linear-content h4,
3254
+ .squisq-linear-content h5,
3255
+ .squisq-linear-content h6 {
3256
+ font-family: var(--squisq-linear-title-font);
3257
+ color: var(--squisq-linear-text);
3258
+ margin-top: 1.2em;
3259
+ margin-bottom: 0.4em;
3260
+ }
3261
+ .squisq-linear-content h1 { font-size: 2em; }
3262
+ .squisq-linear-content h2 { font-size: 1.5em; }
3263
+ .squisq-linear-content h3 { font-size: 1.25em; }
3264
+ .squisq-linear-content p {
3265
+ margin-bottom: 0.75em;
3266
+ }
3267
+ .squisq-linear-content ul,
3268
+ .squisq-linear-content ol {
3269
+ padding-left: 2em;
3270
+ margin-bottom: 0.75em;
3271
+ }
3272
+ .squisq-linear-content li {
3273
+ margin-bottom: 0.3em;
3274
+ }
3275
+ .squisq-linear-content a {
3276
+ /* Blend the theme's primary toward the body text color so
3277
+ links stay theme-flavored but always have enough contrast
3278
+ against the background. Some themes (e.g. Gezellig) pick a
3279
+ mid-tone primary that's nearly invisible on a dark page
3280
+ without this lift. */
3281
+ color: color-mix(in srgb, var(--squisq-linear-primary) 65%, var(--squisq-linear-text));
3282
+ text-decoration: underline;
3283
+ text-decoration-thickness: 1px;
3284
+ text-underline-offset: 2px;
3285
+ }
3286
+ .squisq-linear-content a:hover {
3287
+ color: var(--squisq-linear-primary);
3288
+ }
3289
+ .squisq-linear-content code {
3290
+ color: var(--squisq-linear-primary);
3291
+ font-size: 0.9em;
3292
+ padding: 0.15em 0.3em;
3293
+ border-radius: 3px;
3294
+ background: rgba(128, 128, 128, 0.15);
3295
+ }
3296
+ .squisq-linear-content pre {
3297
+ padding: 1em;
3298
+ border-radius: 6px;
3299
+ background: rgba(0, 0, 0, 0.2);
3300
+ overflow-x: auto;
3301
+ margin-bottom: 0.75em;
3302
+ }
3303
+ .squisq-linear-content pre code {
3304
+ padding: 0;
3305
+ background: none;
3306
+ }
3307
+ .squisq-linear-content blockquote {
3308
+ border-left: 3px solid var(--squisq-linear-muted);
3309
+ color: var(--squisq-linear-muted);
3310
+ padding-left: 1em;
3311
+ margin-left: 0;
3312
+ margin-bottom: 0.75em;
3313
+ }
3314
+ .squisq-linear-content hr {
3315
+ border: none;
3316
+ border-top: 1px solid var(--squisq-linear-muted);
3317
+ margin: 1.5em 0;
3318
+ }
3319
+ .squisq-linear-content img {
3320
+ max-width: 100%;
3321
+ height: auto;
3322
+ border-radius: 6px;
3323
+ margin: 0.5em 0;
3324
+ }
3325
+ .squisq-linear-content--thumbnail-images img {
3326
+ max-width: 100px;
3327
+ max-height: 100px;
3328
+ width: auto;
3329
+ height: auto;
3330
+ object-fit: contain;
3331
+ display: block;
3332
+ }
3333
+ .squisq-linear-content strong {
3334
+ font-weight: 700;
3335
+ }
3336
+ .squisq-linear-content em {
3337
+ font-style: italic;
3338
+ }
3339
+ .squisq-linear-content table {
3340
+ width: 100%;
3341
+ border-collapse: collapse;
3342
+ margin: 1em 0;
3343
+ font-size: 0.95em;
3344
+ }
3345
+ .squisq-linear-content thead th {
3346
+ background: var(--squisq-linear-primary);
3347
+ color: var(--squisq-linear-bg);
3348
+ font-family: var(--squisq-linear-title-font);
3349
+ font-weight: 600;
3350
+ padding: 10px 14px;
3351
+ text-align: left;
3352
+ }
3353
+ .squisq-linear-content tbody td {
3354
+ padding: 8px 14px;
3355
+ border-bottom: 1px solid color-mix(in srgb, var(--squisq-linear-muted) 30%, transparent);
3356
+ }
3357
+ .squisq-linear-content tbody tr:hover {
3358
+ background: color-mix(in srgb, var(--squisq-linear-primary) 8%, transparent);
3359
+ }
3360
+ ` }),
3361
+ doc.blocks.map((block, i) => /* @__PURE__ */ jsx19(
3362
+ BlockSection,
3363
+ {
3364
+ block,
3365
+ basePath,
3366
+ viewport: activeViewport,
3367
+ renderContext,
3368
+ blockIndex: i
3369
+ },
3370
+ block.id
3371
+ ))
3372
+ ]
3373
+ }
3374
+ )
3375
+ }
3376
+ );
3377
+ }
3378
+
3379
+ // src/DocPlayer.tsx
3380
+ import { jsx as jsx20, jsxs as jsxs13 } from "react/jsx-runtime";
3381
+ var SMALL_WORDS = /* @__PURE__ */ new Set([
3382
+ "a",
3383
+ "an",
3384
+ "the",
3385
+ "and",
3386
+ "but",
3387
+ "or",
3388
+ "for",
3389
+ "nor",
3390
+ "on",
3391
+ "at",
3392
+ "to",
3393
+ "in",
3394
+ "of",
3395
+ "by",
3396
+ "is"
3397
+ ]);
3398
+ function buildSegmentTitleMap(script) {
3399
+ const map = /* @__PURE__ */ new Map();
3400
+ for (const block of script.blocks) {
3401
+ if (isTemplateBlock2(block) && block.template === "sectionHeader" && "title" in block) {
3402
+ const segIdx = block.audioSegment;
3403
+ if (!map.has(segIdx)) {
3404
+ map.set(segIdx, block.title);
3405
+ }
3406
+ }
3407
+ }
3408
+ for (let i = 0; i < script.audio.segments.length; i++) {
3409
+ if (!map.has(i)) {
3410
+ const name = script.audio.segments[i].name;
3411
+ if (name === "intro" || name.includes("intro")) {
3412
+ map.set(i, "Introduction");
3413
+ } else if (name === "flight-context" || name.includes("flight-context")) {
3414
+ map.set(i, "Flight Context");
3415
+ } else {
3416
+ const words = name.split("-");
3417
+ const titled = words.map(
3418
+ (w, idx) => idx === 0 || !SMALL_WORDS.has(w) ? w.charAt(0).toUpperCase() + w.slice(1) : w
3419
+ ).join(" ");
3420
+ map.set(i, titled);
3421
+ }
3422
+ }
3423
+ }
3424
+ return map;
3425
+ }
3426
+ function DocPlayer({
3427
+ script,
3428
+ basePath,
3429
+ renderMode = false,
3430
+ autoPlay = false,
3431
+ onEnded,
3432
+ onTimeUpdate,
3433
+ audioProvider: externalAudioProvider,
3434
+ showControls = true,
3435
+ showScrubber = false,
3436
+ muted = false,
3437
+ captionsEnabled: captionsEnabledProp,
3438
+ onCaptionsToggle,
3439
+ onPlaybackStateChange,
3440
+ onControlsReady,
3441
+ isFullscreen = false,
3442
+ onFullscreenToggle,
3443
+ onBlockMarkers,
3444
+ forceViewport,
3445
+ displayMode = "video",
3446
+ theme,
3447
+ surface,
3448
+ captionStyle = "standard"
3449
+ }) {
3450
+ const isSlideshowMode = displayMode === "slideshow";
3451
+ const isLinearMode = displayMode === "linear";
3452
+ const audioRef = useRef6(null);
3453
+ const containerRef = useRef6(null);
3454
+ const [tapFeedback, setTapFeedback] = useState6(null);
3455
+ const tapFeedbackTimer = useRef6();
3456
+ const { viewport, orientation } = useViewportOrientation();
3457
+ const activeViewport = forceViewport || (renderMode ? VIEWPORT_PRESETS4.landscape : viewport);
3458
+ const isDebugMode = useMemo9(() => {
3459
+ if (typeof window === "undefined") return false;
3460
+ const params = new URLSearchParams(window.location.search);
3461
+ return params.get("debug") === "true";
3462
+ }, []);
3463
+ const internalAudio = useAudioSync(audioRef, script.audio, basePath);
3464
+ const audio = externalAudioProvider || internalAudio;
3465
+ const {
3466
+ currentTime,
3467
+ isPlaying,
3468
+ currentSegment,
3469
+ totalDuration,
3470
+ isEnded,
3471
+ isReady: isAudioReady,
3472
+ isAvailable,
3473
+ unavailableMessage,
3474
+ play,
3475
+ pause,
3476
+ toggle,
3477
+ seekTo,
3478
+ skipToSegment: _skipToSegment,
3479
+ restart
3480
+ } = audio;
3481
+ const mediaSchedule = useMemo9(() => resolveMediaSchedule(script), [script]);
3482
+ const currentTimeRef = useRef6(currentTime);
3483
+ currentTimeRef.current = currentTime;
3484
+ const totalDurationRef = useRef6(totalDuration);
3485
+ totalDurationRef.current = totalDuration;
3486
+ const expandedBlocksLenRef = useRef6(0);
3487
+ const handleContainerClick = useCallback5(
3488
+ (e) => {
3489
+ if (renderMode || isSlideshowMode || isLinearMode) return;
3490
+ const target = e.target;
3491
+ if (target.closest(
3492
+ "button, a, input, .doc-player__controls, .doc-player__scrubber, .doc-controls-sidebar, .doc-controls-slideshow"
3493
+ ))
3494
+ return;
3495
+ toggle();
3496
+ const nextState = isPlaying ? "play" : "pause";
3497
+ setTapFeedback(nextState);
3498
+ clearTimeout(tapFeedbackTimer.current);
3499
+ tapFeedbackTimer.current = setTimeout(() => setTapFeedback(null), 600);
3500
+ },
3501
+ [renderMode, toggle, isPlaying, isSlideshowMode, isLinearMode]
3502
+ );
3503
+ const autoSurface = useAutoSurface(surface === "auto");
3504
+ const resolvedSurface = surface === "auto" ? autoSurface : surface;
3505
+ const effectiveTheme = useMemo9(() => {
3506
+ const base = theme ?? DEFAULT_THEME3;
3507
+ return resolvedSurface ? applySurface2(base, resolvedSurface) : base;
3508
+ }, [theme, resolvedSurface]);
3509
+ const {
3510
+ currentBlock,
3511
+ currentBlockIndex,
3512
+ previousBlock,
3513
+ isEntering,
3514
+ isExiting,
3515
+ blockTime,
3516
+ blockProgress: _blockProgress,
3517
+ docProgress,
3518
+ nextBlock: _nextBlock,
3519
+ prevBlock: _prevBlock,
3520
+ blocks: expandedBlocks
3521
+ } = useDocPlayback(script, currentTime, activeViewport, renderMode, effectiveTheme);
3522
+ const coverBlock = useMemo9(() => {
3523
+ const startBlockConfig = script.startBlock;
3524
+ if (!startBlockConfig) return null;
3525
+ const context = createTemplateContext(effectiveTheme, 0, 1, activeViewport);
3526
+ const layers = expandCoverBlock(startBlockConfig, context);
3527
+ return {
3528
+ id: "cover-block",
3529
+ startTime: -1,
3530
+ // Not part of timeline
3531
+ duration: 0,
3532
+ // Static
3533
+ audioSegment: -1,
3534
+ layers
3535
+ };
3536
+ }, [script.startBlock, activeViewport, effectiveTheme]);
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);
3542
+ const atRest = !!(coverBlock && !isPlaying && currentTime === 0 && !hasPlayedOnce.current && !renderMode && !autoPlay);
3543
+ if (atRest) coverWasShowing.current = true;
3544
+ useEffect7(() => {
3545
+ if (isPlaying && coverWasShowing.current && coverBlock && !renderMode) {
3546
+ coverWasShowing.current = false;
3547
+ hasPlayedOnce.current = true;
3548
+ setCoverGraceActive(true);
3549
+ coverGraceTimer.current = setTimeout(() => setCoverGraceActive(false), 3e3);
3550
+ }
3551
+ }, [isPlaying, coverBlock, renderMode]);
3552
+ useEffect7(() => () => clearTimeout(coverGraceTimer.current), []);
3553
+ const showCoverBlock = !isSlideshowMode && !isLinearMode && coverBlock && (coverForced || coverGraceActive || !isPlaying && currentTime === 0 && !hasPlayedOnce.current && !renderMode && !autoPlay);
3554
+ const hasAutoPlayed = useRef6(false);
3555
+ useEffect7(() => {
3556
+ if (isAudioReady && autoPlay && !hasAutoPlayed.current) {
3557
+ hasAutoPlayed.current = true;
3558
+ play();
3559
+ }
3560
+ }, [isAudioReady, autoPlay, play]);
3561
+ useEffect7(() => {
3562
+ onTimeUpdate?.(currentTime);
3563
+ }, [currentTime, onTimeUpdate]);
3564
+ useEffect7(() => {
3565
+ if (isEnded) {
3566
+ onEnded?.();
3567
+ }
3568
+ }, [isEnded, onEnded]);
3569
+ useEffect7(() => {
3570
+ if ((renderMode || isDebugMode) && typeof window !== "undefined") {
3571
+ const w = window;
3572
+ w.seekTo = (time) => {
3573
+ seekTo(time);
3574
+ return new Promise((resolve) => {
3575
+ requestAnimationFrame(() => {
3576
+ let blockStartTime = 0;
3577
+ for (let i = expandedBlocks.length - 1; i >= 0; i--) {
3578
+ if (time >= expandedBlocks[i].startTime) {
3579
+ blockStartTime = expandedBlocks[i].startTime;
3580
+ break;
3581
+ }
3582
+ }
3583
+ const elapsedMs = (time - blockStartTime) * 1e3;
3584
+ document.getAnimations().forEach((anim) => {
3585
+ const target = anim.effect?.target;
3586
+ if (!target) return;
3587
+ if (target.closest(".doc-player__block--active")) {
3588
+ anim.currentTime = Math.max(0, elapsedMs);
3589
+ } else if (target.closest(".doc-player__block--previous")) {
3590
+ anim.currentTime = Math.max(0, elapsedMs);
3591
+ }
3592
+ });
3593
+ const blockElapsed = time - blockStartTime;
3594
+ const videoSeekPromises = [];
3595
+ const activeBlockEl = document.querySelector(".doc-player__block--active");
3596
+ if (activeBlockEl) {
3597
+ const videos = activeBlockEl.querySelectorAll("video[data-clip-start]");
3598
+ videos.forEach((el) => {
3599
+ const video = el;
3600
+ const clipStart = parseFloat(video.dataset.clipStart || "0");
3601
+ const clipEnd = parseFloat(video.dataset.clipEnd || "0");
3602
+ const startAt = parseFloat(video.dataset.startAt || "0");
3603
+ const targetTime = Math.min(
3604
+ clipStart + Math.max(0, blockElapsed - startAt),
3605
+ clipEnd
3606
+ );
3607
+ video.pause();
3608
+ video.currentTime = targetTime;
3609
+ videoSeekPromises.push(
3610
+ new Promise((r) => {
3611
+ if (Math.abs(video.currentTime - targetTime) < 0.1) {
3612
+ r();
3613
+ } else {
3614
+ video.addEventListener("seeked", () => r(), { once: true });
3615
+ setTimeout(r, 200);
3616
+ }
3617
+ })
3618
+ );
3619
+ });
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
+ });
3641
+ Promise.all(videoSeekPromises).then(() => {
3642
+ requestAnimationFrame(() => resolve());
3643
+ });
3644
+ });
3645
+ });
3646
+ };
3647
+ w.getDuration = () => {
3648
+ const mediaDuration = getDocPlaybackDuration(script);
3649
+ if (totalDuration > 0) return Math.max(totalDuration, mediaDuration);
3650
+ return mediaDuration;
3651
+ };
3652
+ w.getBlocks = () => expandedBlocks.map((s) => ({
3653
+ id: s.id,
3654
+ template: s.template ?? "raw",
3655
+ startTime: s.startTime,
3656
+ duration: s.duration
3657
+ }));
3658
+ w.getAudioSegments = () => script.audio.segments.map((seg) => ({
3659
+ src: seg.src,
3660
+ name: seg.name,
3661
+ duration: seg.duration,
3662
+ startTime: seg.startTime
3663
+ }));
3664
+ w.getCaptions = () => script.captions?.phrases?.map((p) => ({
3665
+ text: p.text,
3666
+ startTime: p.startTime,
3667
+ endTime: p.endTime
3668
+ })) || [];
3669
+ w.getChapters = () => {
3670
+ const titleMap = buildSegmentTitleMap(script);
3671
+ return script.audio.segments.map((seg, i) => ({
3672
+ title: titleMap.get(i) || seg.name,
3673
+ startTime: seg.startTime,
3674
+ duration: seg.duration
3675
+ }));
3676
+ };
3677
+ w.showCover = () => {
3678
+ setCoverForced(true);
3679
+ return new Promise((resolve) => requestAnimationFrame(() => resolve()));
3680
+ };
3681
+ w.hideCover = () => {
3682
+ setCoverForced(false);
3683
+ return new Promise((resolve) => requestAnimationFrame(() => resolve()));
3684
+ };
3685
+ w.hasCoverBlock = () => !!coverBlock;
3686
+ }
3687
+ return () => {
3688
+ if (typeof window !== "undefined") {
3689
+ const w = window;
3690
+ delete w.seekTo;
3691
+ delete w.getDuration;
3692
+ delete w.getBlocks;
3693
+ delete w.getAudioSegments;
3694
+ delete w.getCaptions;
3695
+ delete w.getChapters;
3696
+ delete w.showCover;
3697
+ delete w.hideCover;
3698
+ delete w.hasCoverBlock;
3699
+ }
3700
+ };
3701
+ }, [renderMode, isDebugMode, seekTo, totalDuration, expandedBlocks, coverBlock]);
3702
+ const defaultMode = captionsEnabledProp === false ? "off" : captionStyle || "standard";
3703
+ const [captionMode, setCaptionMode] = useState6(defaultMode);
3704
+ const captionsEnabled = captionMode !== "off";
3705
+ const activeCaptionStyle = captionMode === "social" ? "social" : "standard";
3706
+ const setCaptionsEnabled = useCallback5(
3707
+ (enabled) => {
3708
+ setCaptionMode(enabled ? captionStyle || "standard" : "off");
3709
+ onCaptionsToggle?.(enabled);
3710
+ },
3711
+ [onCaptionsToggle, captionStyle]
3712
+ );
3713
+ const cycleCaptionMode = useCallback5(() => {
3714
+ setCaptionMode((prev) => {
3715
+ const next = prev === "off" ? "standard" : prev === "standard" ? "social" : "off";
3716
+ onCaptionsToggle?.(next !== "off");
3717
+ return next;
3718
+ });
3719
+ }, [onCaptionsToggle]);
3720
+ const hasCaptions = script.captions && script.captions.phrases.length > 0;
3721
+ const segmentTitleMap = useMemo9(() => buildSegmentTitleMap(script), [script]);
3722
+ const playbackState = useMemo9(
3723
+ () => ({
3724
+ isPlaying,
3725
+ currentTime,
3726
+ totalDuration,
3727
+ currentBlockIndex,
3728
+ totalBlocks: expandedBlocks.length,
3729
+ docProgress,
3730
+ hasCaptions: !!hasCaptions,
3731
+ captionsEnabled,
3732
+ captionMode,
3733
+ isFullscreen,
3734
+ currentSegmentIndex: currentSegment,
3735
+ currentSegmentName: segmentTitleMap.get(currentSegment) ?? script.audio.segments[currentSegment]?.name ?? null,
3736
+ currentBlock: currentBlock ?? null
3737
+ }),
3738
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- script.audio.segments is stable within a given script
3739
+ [
3740
+ isPlaying,
3741
+ currentTime,
3742
+ totalDuration,
3743
+ currentBlockIndex,
3744
+ expandedBlocks.length,
3745
+ docProgress,
3746
+ hasCaptions,
3747
+ captionsEnabled,
3748
+ captionMode,
3749
+ isFullscreen,
3750
+ currentSegment,
3751
+ segmentTitleMap,
3752
+ currentBlock
3753
+ ]
3754
+ );
3755
+ const playbackActions = useMemo9(
3756
+ () => ({
3757
+ toggle,
3758
+ restart,
3759
+ seekTo,
3760
+ setCaptionsEnabled,
3761
+ cycleCaptionMode,
3762
+ toggleFullscreen: onFullscreenToggle
3763
+ }),
3764
+ [toggle, restart, seekTo, setCaptionsEnabled, cycleCaptionMode, onFullscreenToggle]
3765
+ );
3766
+ const slideNavActions = useMemo9(
3767
+ () => ({
3768
+ nextSlide: () => {
3769
+ if (currentBlockIndex < expandedBlocks.length - 1) {
3770
+ const target = expandedBlocks[currentBlockIndex + 1];
3771
+ if (target) {
3772
+ seekTo(target.startTime);
3773
+ pause();
3774
+ }
3775
+ }
3776
+ },
3777
+ prevSlide: () => {
3778
+ if (currentBlockIndex > 0) {
3779
+ const target = expandedBlocks[currentBlockIndex - 1];
3780
+ if (target) {
3781
+ seekTo(target.startTime);
3782
+ pause();
3783
+ }
3784
+ }
3785
+ },
3786
+ goToSlide: (index) => {
3787
+ if (index >= 0 && index < expandedBlocks.length) {
3788
+ const target = expandedBlocks[index];
3789
+ if (target) {
3790
+ seekTo(target.startTime);
3791
+ pause();
3792
+ }
3793
+ }
3794
+ }
3795
+ }),
3796
+ [currentBlockIndex, expandedBlocks, seekTo, pause]
3797
+ );
3798
+ useEffect7(() => {
3799
+ onPlaybackStateChange?.(playbackState);
3800
+ }, [playbackState, onPlaybackStateChange]);
3801
+ useEffect7(() => {
3802
+ onControlsReady?.({ play, pause, ...playbackActions });
3803
+ }, [play, pause, playbackActions, onControlsReady]);
3804
+ const getBlockTitle = useCallback5((block) => {
3805
+ const docBlock = block;
3806
+ if (isTemplateBlock2(docBlock)) {
3807
+ const props = docBlock;
3808
+ if (typeof props.title === "string") return props.title;
3809
+ if (typeof props.stat === "string") return props.stat;
3810
+ if (typeof props.quote === "string") {
3811
+ const firstLine = props.quote.split("\n")[0];
3812
+ if (firstLine.length <= 30) return firstLine;
3813
+ return firstLine.slice(0, 27) + "...";
3814
+ }
3815
+ if (typeof props.date === "string") return props.date;
3816
+ if (typeof props.fact === "string") return props.fact;
3817
+ }
3818
+ if (block.layers && Array.isArray(block.layers)) {
3819
+ const textLayer = block.layers.find((l) => l.type === "text");
3820
+ if (textLayer?.content?.text) {
3821
+ const firstLine = textLayer.content.text.split("\n")[0];
3822
+ if (firstLine.length <= 30) return firstLine;
3823
+ return firstLine.slice(0, 27) + "...";
3824
+ }
3825
+ }
3826
+ return block.id.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
3827
+ }, []);
3828
+ const blockMarkers = useMemo9(() => {
3829
+ if (!totalDuration || !expandedBlocks.length) return [];
3830
+ let prevSegment = -1;
3831
+ return expandedBlocks.map((block, index) => {
3832
+ const isSectionStart = block.audioSegment !== prevSegment;
3833
+ prevSegment = block.audioSegment;
3834
+ return {
3835
+ block,
3836
+ index,
3837
+ position: block.startTime / totalDuration * 100,
3838
+ title: getBlockTitle(block),
3839
+ isSectionStart
3840
+ };
3841
+ });
3842
+ }, [expandedBlocks, totalDuration, getBlockTitle]);
3843
+ useEffect7(() => {
3844
+ if (blockMarkers.length > 0) {
3845
+ onBlockMarkers?.(blockMarkers);
3846
+ }
3847
+ }, [blockMarkers, onBlockMarkers]);
3848
+ expandedBlocksLenRef.current = expandedBlocks.length;
3849
+ const handleKeyDown = useCallback5(
3850
+ (e) => {
3851
+ const activeEl = document.activeElement;
3852
+ if (activeEl && (activeEl.tagName === "INPUT" || activeEl.tagName === "TEXTAREA" || activeEl.tagName === "SELECT")) {
3853
+ return;
3854
+ }
3855
+ if (isLinearMode) return;
3856
+ if (isSlideshowMode) {
3857
+ switch (e.key) {
3858
+ case "ArrowRight":
3859
+ case "ArrowDown":
3860
+ case " ":
3861
+ e.preventDefault();
3862
+ slideNavActions.nextSlide();
3863
+ break;
3864
+ case "ArrowLeft":
3865
+ case "ArrowUp":
3866
+ e.preventDefault();
3867
+ slideNavActions.prevSlide();
3868
+ break;
3869
+ case "Home":
3870
+ e.preventDefault();
3871
+ slideNavActions.goToSlide(0);
3872
+ break;
3873
+ case "End":
3874
+ e.preventDefault();
3875
+ slideNavActions.goToSlide(expandedBlocksLenRef.current - 1);
3876
+ break;
3877
+ }
3878
+ } else {
3879
+ switch (e.key) {
3880
+ case " ":
3881
+ e.preventDefault();
3882
+ toggle();
3883
+ break;
3884
+ case "ArrowRight":
3885
+ seekTo(Math.min(currentTimeRef.current + 10, totalDurationRef.current));
3886
+ break;
3887
+ case "ArrowLeft":
3888
+ seekTo(Math.max(currentTimeRef.current - 10, 0));
3889
+ break;
3890
+ }
3891
+ }
3892
+ },
3893
+ [isSlideshowMode, isLinearMode, toggle, seekTo, slideNavActions]
3894
+ );
3895
+ useEffect7(() => {
3896
+ if (renderMode) return;
3897
+ window.addEventListener("keydown", handleKeyDown);
3898
+ return () => window.removeEventListener("keydown", handleKeyDown);
3899
+ }, [handleKeyDown, renderMode]);
3900
+ if (isLinearMode) {
3901
+ return /* @__PURE__ */ jsx20(
3902
+ "div",
3903
+ {
3904
+ ref: containerRef,
3905
+ className: "doc-player doc-player--linear",
3906
+ style: {
3907
+ position: "relative",
3908
+ width: "100%",
3909
+ height: "100%",
3910
+ overflow: "hidden"
3911
+ },
3912
+ children: /* @__PURE__ */ jsx20(
3913
+ LinearDocView,
3914
+ {
3915
+ doc: script,
3916
+ basePath,
3917
+ viewport: activeViewport,
3918
+ theme,
3919
+ surface
3920
+ }
3921
+ )
3922
+ }
3923
+ );
3924
+ }
3925
+ return /* @__PURE__ */ jsxs13(
3926
+ "div",
3927
+ {
3928
+ ref: containerRef,
3929
+ className: "doc-player",
3930
+ onClick: handleContainerClick,
3931
+ style: {
3932
+ position: "relative",
3933
+ width: "100%",
3934
+ aspectRatio: `${activeViewport.width} / ${activeViewport.height}`,
3935
+ margin: "0 auto",
3936
+ overflow: "hidden",
3937
+ cursor: renderMode ? void 0 : "pointer"
3938
+ },
3939
+ children: [
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(
3953
+ BlockRenderer,
3954
+ {
3955
+ block: coverBlock,
3956
+ blockTime: 0,
3957
+ basePath,
3958
+ isEntering: false,
3959
+ viewport: activeViewport
3960
+ }
3961
+ ) }),
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(
3967
+ BlockRenderer,
3968
+ {
3969
+ block: previousBlock,
3970
+ blockTime,
3971
+ basePath,
3972
+ isExiting: true,
3973
+ transition: currentBlock?.transition,
3974
+ viewport: activeViewport
3975
+ }
3976
+ ) }, previousBlock.id),
3977
+ !showCoverBlock && currentBlock && /* @__PURE__ */ jsx20("div", { className: "doc-player__block doc-player__block--active", children: /* @__PURE__ */ jsx20(
3978
+ BlockRenderer,
3979
+ {
3980
+ block: currentBlock,
3981
+ blockTime,
3982
+ basePath,
3983
+ isEntering,
3984
+ viewport: activeViewport,
3985
+ isPlaying
3986
+ }
3987
+ ) }, currentBlock.id),
3988
+ hasCaptions && (renderMode ? captionsEnabled : true) && /* @__PURE__ */ jsx20(
3989
+ CaptionOverlay,
3990
+ {
3991
+ captions: script.captions,
3992
+ currentTime,
3993
+ enabled: captionsEnabled && (renderMode || isPlaying || currentTime > 0),
3994
+ fontSize: 16,
3995
+ captionStyle: activeCaptionStyle,
3996
+ theme: effectiveTheme,
3997
+ viewport: activeViewport
3998
+ }
3999
+ ),
4000
+ isDebugMode && /* @__PURE__ */ jsxs13(
4001
+ "div",
4002
+ {
4003
+ className: "doc-player__debug",
4004
+ style: {
4005
+ position: "absolute",
4006
+ top: "8px",
4007
+ right: "8px",
4008
+ padding: "8px 12px",
4009
+ background: "rgba(0, 0, 0, 0.85)",
4010
+ borderRadius: "6px",
4011
+ color: "#00ff00",
4012
+ fontFamily: "monospace",
4013
+ fontSize: "11px",
4014
+ lineHeight: "1.5",
4015
+ zIndex: 200,
4016
+ maxWidth: "280px",
4017
+ pointerEvents: "none",
4018
+ textAlign: "left"
4019
+ },
4020
+ children: [
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:" }),
4024
+ " ",
4025
+ /* @__PURE__ */ jsx20("span", { style: { color: "#ff6b6b" }, children: currentBlock?.template ?? "raw" })
4026
+ ] }),
4027
+ /* @__PURE__ */ jsxs13("div", { children: [
4028
+ /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "block:" }),
4029
+ " ",
4030
+ currentBlockIndex + 1,
4031
+ "/",
4032
+ expandedBlocks.length,
4033
+ " ",
4034
+ /* @__PURE__ */ jsxs13("span", { style: { color: "#666" }, children: [
4035
+ "(",
4036
+ currentBlock?.id || "none",
4037
+ ")"
4038
+ ] })
4039
+ ] }),
4040
+ /* @__PURE__ */ jsxs13("div", { children: [
4041
+ /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "time:" }),
4042
+ " ",
4043
+ currentTime.toFixed(2),
4044
+ "s /",
4045
+ " ",
4046
+ totalDuration.toFixed(1),
4047
+ "s",
4048
+ " ",
4049
+ /* @__PURE__ */ jsxs13("span", { style: { color: "#666" }, children: [
4050
+ "(progress: ",
4051
+ (docProgress * 100).toFixed(1),
4052
+ "%, scriptDur:",
4053
+ " ",
4054
+ script.duration.toFixed(1),
4055
+ ")"
4056
+ ] })
4057
+ ] }),
4058
+ /* @__PURE__ */ jsxs13("div", { children: [
4059
+ /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "blockTime:" }),
4060
+ " ",
4061
+ blockTime.toFixed(2),
4062
+ "s /",
4063
+ " ",
4064
+ (currentBlock?.duration || 0).toFixed(1),
4065
+ "s"
4066
+ ] }),
4067
+ /* @__PURE__ */ jsxs13("div", { children: [
4068
+ /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "segment:" }),
4069
+ " ",
4070
+ currentSegment,
4071
+ "/",
4072
+ script.audio.segments.length - 1,
4073
+ " ",
4074
+ /* @__PURE__ */ jsxs13("span", { style: { color: "#666" }, children: [
4075
+ "(",
4076
+ script.audio.segments[currentSegment]?.name || "none",
4077
+ ")"
4078
+ ] })
4079
+ ] }),
4080
+ /* @__PURE__ */ jsxs13("div", { children: [
4081
+ /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "viewport:" }),
4082
+ " ",
4083
+ activeViewport.name || `${activeViewport.width}x${activeViewport.height}`,
4084
+ " ",
4085
+ /* @__PURE__ */ jsxs13("span", { style: { color: "#666" }, children: [
4086
+ "(",
4087
+ orientation,
4088
+ ")"
4089
+ ] })
4090
+ ] }),
4091
+ /* @__PURE__ */ jsxs13("div", { children: [
4092
+ /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "playing:" }),
4093
+ " ",
4094
+ /* @__PURE__ */ jsx20("span", { style: { color: isPlaying ? "#4ade80" : "#f87171" }, children: isPlaying ? "yes" : "no" }),
4095
+ showCoverBlock && /* @__PURE__ */ jsx20("span", { style: { color: "#60a5fa" }, children: " (cover)" })
4096
+ ] }),
4097
+ hasCaptions && (() => {
4098
+ const debugPhrase = getCaptionAtTime2(script.captions, currentTime);
4099
+ const debugEnabled = captionsEnabled && (isPlaying || currentTime > 0);
4100
+ return /* @__PURE__ */ jsxs13(Fragment3, { children: [
4101
+ /* @__PURE__ */ jsxs13("div", { children: [
4102
+ /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "captions:" }),
4103
+ " ",
4104
+ script.captions?.phrases.length || 0,
4105
+ " phrases",
4106
+ " ",
4107
+ /* @__PURE__ */ jsxs13("span", { style: { color: captionsEnabled ? "#4ade80" : "#666" }, children: [
4108
+ "(",
4109
+ captionsEnabled ? "on" : "off",
4110
+ ")"
4111
+ ] })
4112
+ ] }),
4113
+ /* @__PURE__ */ jsxs13("div", { children: [
4114
+ /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "cc.enabled:" }),
4115
+ " ",
4116
+ /* @__PURE__ */ jsx20("span", { style: { color: debugEnabled ? "#4ade80" : "#f87171" }, children: String(debugEnabled) }),
4117
+ " ",
4118
+ /* @__PURE__ */ jsxs13("span", { style: { color: "#666" }, children: [
4119
+ "(playing=",
4120
+ String(isPlaying),
4121
+ " t>0=",
4122
+ String(currentTime > 0),
4123
+ ")"
4124
+ ] })
4125
+ ] }),
4126
+ /* @__PURE__ */ jsxs13("div", { children: [
4127
+ /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "cc.phrase:" }),
4128
+ " ",
4129
+ /* @__PURE__ */ jsx20("span", { style: { color: debugPhrase ? "#4ade80" : "#f87171" }, children: debugPhrase ? `"${debugPhrase.text.slice(0, 30)}..."` : "null" })
4130
+ ] }),
4131
+ debugPhrase && /* @__PURE__ */ jsxs13("div", { children: [
4132
+ /* @__PURE__ */ jsx20("span", { style: { color: "#888" }, children: "cc.range:" }),
4133
+ " ",
4134
+ /* @__PURE__ */ jsxs13("span", { style: { color: "#60a5fa" }, children: [
4135
+ debugPhrase.startTime.toFixed(2),
4136
+ "-",
4137
+ debugPhrase.endTime.toFixed(2)
4138
+ ] })
4139
+ ] })
4140
+ ] });
4141
+ })()
4142
+ ]
4143
+ }
4144
+ )
4145
+ ] }),
4146
+ !isAvailable && unavailableMessage && /* @__PURE__ */ jsxs13(
4147
+ "div",
4148
+ {
4149
+ className: "doc-player__unavailable",
4150
+ style: {
4151
+ position: "absolute",
4152
+ top: 0,
4153
+ left: 0,
4154
+ right: 0,
4155
+ bottom: 0,
4156
+ display: "flex",
4157
+ alignItems: "center",
4158
+ justifyContent: "center",
4159
+ flexDirection: "column",
4160
+ gap: "16px",
4161
+ background: "rgba(0, 0, 0, 0.7)",
4162
+ color: "rgba(255, 255, 255, 0.9)",
4163
+ fontSize: "14px",
4164
+ zIndex: 50
4165
+ },
4166
+ children: [
4167
+ /* @__PURE__ */ jsx20("span", { style: { fontSize: "32px" }, children: "\u{1F50A}" }),
4168
+ /* @__PURE__ */ jsx20("span", { children: unavailableMessage })
4169
+ ]
4170
+ }
4171
+ ),
4172
+ !renderMode && !isSlideshowMode && showControls && /* @__PURE__ */ jsx20(
4173
+ DocControlsOverlay,
4174
+ {
4175
+ state: playbackState,
4176
+ actions: playbackActions,
4177
+ blockMarkers,
4178
+ expandedBlocks,
4179
+ getBlockTitle
4180
+ }
4181
+ ),
4182
+ !renderMode && !isSlideshowMode && !showControls && showScrubber && /* @__PURE__ */ jsx20(
4183
+ "div",
4184
+ {
4185
+ className: "doc-player__scrubber",
4186
+ style: {
4187
+ position: "absolute",
4188
+ bottom: 0,
4189
+ left: 0,
4190
+ right: 0,
4191
+ padding: "12px 16px 8px",
4192
+ background: "linear-gradient(transparent, rgba(0,0,0,0.6))",
4193
+ display: "flex",
4194
+ alignItems: "center",
4195
+ zIndex: 100
4196
+ },
4197
+ children: /* @__PURE__ */ jsx20(
4198
+ DocProgressBar,
4199
+ {
4200
+ state: playbackState,
4201
+ actions: playbackActions,
4202
+ blockMarkers,
4203
+ expandedBlocks,
4204
+ getBlockTitle
4205
+ }
4206
+ )
4207
+ }
4208
+ ),
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())
4211
+ ]
4212
+ }
4213
+ );
4214
+ }
4215
+
4216
+ // src/DocControlsBottom.tsx
4217
+ import { jsx as jsx21, jsxs as jsxs14 } from "react/jsx-runtime";
4218
+ function DocControlsBottom({
4219
+ state,
4220
+ actions,
4221
+ blockMarkers,
4222
+ expandedBlocks,
4223
+ getBlockTitle
4224
+ }) {
4225
+ return /* @__PURE__ */ jsxs14("div", { className: "doc-controls-bottom", children: [
4226
+ /* @__PURE__ */ jsx21(
4227
+ "button",
4228
+ {
4229
+ className: "bottom-ctrl-btn",
4230
+ onClick: actions.restart,
4231
+ title: "Restart",
4232
+ "aria-label": "Restart from beginning",
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" }) })
4234
+ }
4235
+ ),
4236
+ /* @__PURE__ */ jsx21(
4237
+ "button",
4238
+ {
4239
+ className: "bottom-ctrl-btn bottom-play-btn",
4240
+ onClick: actions.toggle,
4241
+ "aria-label": state.isPlaying ? "Pause" : "Play",
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" }) })
4243
+ }
4244
+ ),
4245
+ /* @__PURE__ */ jsxs14("span", { className: "bottom-time", children: [
4246
+ formatTime(state.currentTime),
4247
+ " / ",
4248
+ formatTime(state.totalDuration)
4249
+ ] }),
4250
+ /* @__PURE__ */ jsx21(
4251
+ DocProgressBar,
4252
+ {
4253
+ state,
4254
+ actions,
4255
+ blockMarkers,
4256
+ expandedBlocks,
4257
+ getBlockTitle
4258
+ }
4259
+ ),
4260
+ /* @__PURE__ */ jsxs14("span", { className: "bottom-segment", children: [
4261
+ state.currentBlockIndex + 1,
4262
+ "/",
4263
+ state.totalBlocks
4264
+ ] }),
4265
+ state.hasCaptions && /* @__PURE__ */ jsx21(
4266
+ "button",
4267
+ {
4268
+ className: `bottom-ctrl-btn ${state.captionMode !== "off" ? "bottom-ctrl-btn--active" : ""}`,
4269
+ onClick: () => actions.cycleCaptionMode(),
4270
+ title: state.captionMode === "off" ? "Captions: Off" : state.captionMode === "standard" ? "Captions: Standard" : "Captions: Social",
4271
+ "aria-label": "Cycle caption style",
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" }) })
4273
+ }
4274
+ )
4275
+ ] });
4276
+ }
4277
+
4278
+ // src/DocControlsSidebar.tsx
4279
+ import { jsx as jsx22, jsxs as jsxs15 } from "react/jsx-runtime";
4280
+ function DocControlsSidebar({ state, actions }) {
4281
+ return /* @__PURE__ */ jsxs15("div", { className: "doc-controls-sidebar", children: [
4282
+ /* @__PURE__ */ jsx22(
4283
+ "button",
4284
+ {
4285
+ className: "sidebar-ctrl-btn",
4286
+ onClick: actions.restart,
4287
+ title: "Restart",
4288
+ "aria-label": "Restart from beginning",
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" }) })
4290
+ }
4291
+ ),
4292
+ /* @__PURE__ */ jsx22(
4293
+ "button",
4294
+ {
4295
+ className: "sidebar-ctrl-btn sidebar-play-btn",
4296
+ onClick: actions.toggle,
4297
+ "aria-label": state.isPlaying ? "Pause" : "Play",
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" }) })
4299
+ }
4300
+ ),
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) })
4304
+ ] }),
4305
+ /* @__PURE__ */ jsxs15("div", { className: "sidebar-segment", children: [
4306
+ state.currentBlockIndex + 1,
4307
+ "/",
4308
+ state.totalBlocks
4309
+ ] }),
4310
+ state.hasCaptions && /* @__PURE__ */ jsx22(
4311
+ "button",
4312
+ {
4313
+ className: `sidebar-ctrl-btn ${state.captionMode !== "off" ? "sidebar-ctrl-btn--active" : ""}`,
4314
+ onClick: () => actions.cycleCaptionMode(),
4315
+ title: state.captionMode === "off" ? "Captions: Off" : state.captionMode === "standard" ? "Captions: Standard" : "Captions: Social",
4316
+ "aria-label": "Cycle caption style",
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" }) })
4318
+ }
4319
+ ),
4320
+ actions.toggleFullscreen && /* @__PURE__ */ jsx22(
4321
+ "button",
4322
+ {
4323
+ className: `sidebar-ctrl-btn ${state.isFullscreen ? "sidebar-ctrl-btn--active" : ""}`,
4324
+ onClick: actions.toggleFullscreen,
4325
+ title: state.isFullscreen ? "Exit fullscreen (F)" : "Fullscreen (F)",
4326
+ "aria-label": state.isFullscreen ? "Exit fullscreen" : "Enter fullscreen",
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" }) })
4328
+ }
4329
+ )
4330
+ ] });
4331
+ }
4332
+
4333
+ // src/DocPlayerWithSidebar.tsx
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";
4336
+ var DEFAULT_STATE = {
4337
+ isPlaying: false,
4338
+ currentTime: 0,
4339
+ totalDuration: 0,
4340
+ currentBlockIndex: 0,
4341
+ totalBlocks: 0,
4342
+ docProgress: 0,
4343
+ hasCaptions: false,
4344
+ captionsEnabled: false,
4345
+ captionMode: "off",
4346
+ currentSegmentIndex: 0,
4347
+ currentSegmentName: null,
4348
+ currentBlock: null
4349
+ };
4350
+ function DocPlayerWithSidebar({
4351
+ script,
4352
+ basePath,
4353
+ autoPlay = false,
4354
+ onEnded,
4355
+ onTimeUpdate,
4356
+ audioProvider,
4357
+ muted,
4358
+ captionsEnabled,
4359
+ isFullscreen,
4360
+ onFullscreenToggle,
4361
+ forceViewport,
4362
+ onPlayingChange
4363
+ }) {
4364
+ const stateRef = useRef7(DEFAULT_STATE);
4365
+ const actionsRef = useRef7(null);
4366
+ const wasPlayingRef = useRef7(false);
4367
+ const [, setTick] = useState7(0);
4368
+ const handleStateChange = useCallback6(
4369
+ (state) => {
4370
+ stateRef.current = state;
4371
+ if (onPlayingChange && state.isPlaying !== wasPlayingRef.current) {
4372
+ wasPlayingRef.current = state.isPlaying;
4373
+ onPlayingChange(state.isPlaying);
4374
+ }
4375
+ },
4376
+ [onPlayingChange]
4377
+ );
4378
+ const handleControlsReady = useCallback6(
4379
+ (controls) => {
4380
+ const isFirst = !actionsRef.current;
4381
+ actionsRef.current = controls;
4382
+ if (isFirst) setTick((t) => t + 1);
4383
+ },
4384
+ []
4385
+ );
4386
+ useEffect8(() => {
4387
+ const interval = setInterval(() => {
4388
+ setTick((t) => t + 1);
4389
+ }, 250);
4390
+ return () => clearInterval(interval);
4391
+ }, []);
4392
+ return /* @__PURE__ */ jsxs16("div", { className: "doc-player-sidebar-layout", children: [
4393
+ /* @__PURE__ */ jsx23("div", { className: "doc-player-sidebar-layout__video", children: /* @__PURE__ */ jsx23(
4394
+ DocPlayer,
4395
+ {
4396
+ script,
4397
+ basePath,
4398
+ autoPlay,
4399
+ onEnded,
4400
+ onTimeUpdate,
4401
+ audioProvider,
4402
+ muted,
4403
+ captionsEnabled,
4404
+ showControls: isFullscreen,
4405
+ showScrubber: !isFullscreen,
4406
+ onPlaybackStateChange: handleStateChange,
4407
+ onControlsReady: handleControlsReady,
4408
+ isFullscreen,
4409
+ onFullscreenToggle,
4410
+ forceViewport
4411
+ }
4412
+ ) }),
4413
+ actionsRef.current && /* @__PURE__ */ jsx23(DocControlsSidebar, { state: stateRef.current, actions: actionsRef.current })
4414
+ ] });
4415
+ }
4416
+
4417
+ // src/jsonView/useJsonViewTokens.ts
4418
+ import { useMemo as useMemo10 } from "react";
4419
+ import {
4420
+ applySurface as applySurface3,
4421
+ resolveFontFamily as resolveFontFamily3
4422
+ } from "@bendyline/squisq/schemas";
4423
+ import { DEFAULT_THEME as DEFAULT_THEME4 } from "@bendyline/squisq/doc";
4424
+ function useJsonViewTokens(theme, surface) {
4425
+ const auto = useAutoSurface(surface === "auto");
4426
+ const effectiveSurface = surface === "auto" ? auto : surface ?? void 0;
4427
+ return useMemo10(() => {
4428
+ const baseTheme = theme ?? DEFAULT_THEME4;
4429
+ const finalTheme = effectiveSurface ? applySurface3(baseTheme, effectiveSurface) : baseTheme;
4430
+ const titleFont = resolveFontFamily3(finalTheme.typography.titleFont, "system-ui, sans-serif");
4431
+ const bodyFont = resolveFontFamily3(finalTheme.typography.bodyFont, "system-ui, sans-serif");
4432
+ const monoFont = resolveFontFamily3(
4433
+ finalTheme.typography.monoFont,
4434
+ "ui-monospace, Consolas, monospace"
4435
+ );
4436
+ const style = {
4437
+ ["--squisq-json-bg"]: finalTheme.colors.background,
4438
+ ["--squisq-json-text"]: finalTheme.colors.text,
4439
+ ["--squisq-json-muted"]: finalTheme.colors.textMuted,
4440
+ ["--squisq-json-primary"]: finalTheme.colors.primary,
4441
+ ["--squisq-json-accent"]: finalTheme.colors.secondary,
4442
+ ["--squisq-json-border"]: `color-mix(in srgb, ${finalTheme.colors.textMuted} 35%, transparent)`,
4443
+ ["--squisq-json-title-font"]: titleFont,
4444
+ ["--squisq-json-body-font"]: bodyFont,
4445
+ ["--squisq-json-mono-font"]: monoFont,
4446
+ ["--squisq-json-radius"]: `${finalTheme.style.borderRadius ?? 8}px`
4447
+ };
4448
+ return { style, theme: finalTheme };
4449
+ }, [theme, effectiveSurface]);
4450
+ }
4451
+
4452
+ // src/jsonView/RenderNode.tsx
4453
+ import {
4454
+ chooseControl,
4455
+ resolveFlag,
4456
+ resolveRef
4457
+ } from "@bendyline/squisq/jsonForm";
4458
+
4459
+ // src/jsonView/viewers.tsx
4460
+ import { Fragment as Fragment4, useMemo as useMemo11 } from "react";
4461
+ import {
4462
+ arrayItemKind
4463
+ } from "@bendyline/squisq/jsonForm";
4464
+ import { parseMarkdown } from "@bendyline/squisq/markdown";
4465
+ import { Fragment as Fragment5, jsx as jsx24, jsxs as jsxs17 } from "react/jsx-runtime";
4466
+ function TextViewer({ value }) {
4467
+ if (value === void 0 || value === null || value === "") {
4468
+ return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-empty", children: "\u2014" });
4469
+ }
4470
+ return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-value", children: String(value) });
4471
+ }
4472
+ function MultilineViewer({ value }) {
4473
+ if (value === void 0 || value === null || value === "") {
4474
+ return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-empty", children: "\u2014" });
4475
+ }
4476
+ return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-value squisq-jv-value--multiline", children: String(value) });
4477
+ }
4478
+ function RichTextViewer({ value }) {
4479
+ const nodes = useMemo11(() => {
4480
+ if (typeof value !== "string" || value === "") return null;
4481
+ try {
4482
+ const doc = parseMarkdown(value);
4483
+ return doc.children;
4484
+ } catch {
4485
+ return null;
4486
+ }
4487
+ }, [value]);
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 }) });
4490
+ }
4491
+ function NumberViewer({ value }) {
4492
+ if (value === void 0 || value === null) {
4493
+ return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-empty", children: "\u2014" });
4494
+ }
4495
+ return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-value", children: String(value) });
4496
+ }
4497
+ function BooleanViewer({ value }) {
4498
+ const on = Boolean(value);
4499
+ return /* @__PURE__ */ jsx24("span", { className: `squisq-jv-toggle squisq-jv-toggle--${on ? "on" : "off"}`, children: on ? "On" : "Off" });
4500
+ }
4501
+ function EnumViewer({ value, schema }) {
4502
+ if (value === void 0 || value === null || value === "") {
4503
+ return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-empty", children: "\u2014" });
4504
+ }
4505
+ const labels = schema.squisq?.enumLabels;
4506
+ const display = labels && typeof value === "string" ? labels[value] ?? value : String(value);
4507
+ return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-value", children: display });
4508
+ }
4509
+ function ColorViewer({ value }) {
4510
+ if (typeof value !== "string" || value === "") {
4511
+ return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-empty", children: "\u2014" });
4512
+ }
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 })
4516
+ ] });
4517
+ }
4518
+ function DateViewer({ value, schema }) {
4519
+ if (typeof value !== "string" || value === "") {
4520
+ return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-empty", children: "\u2014" });
4521
+ }
4522
+ const fmt = schema.format;
4523
+ let display = value;
4524
+ try {
4525
+ const date = new Date(value);
4526
+ if (!Number.isNaN(date.getTime())) {
4527
+ if (fmt === "time") {
4528
+ display = date.toLocaleTimeString();
4529
+ } else if (fmt === "date") {
4530
+ display = date.toLocaleDateString();
4531
+ } else {
4532
+ display = date.toLocaleString();
4533
+ }
4534
+ }
4535
+ } catch {
4536
+ }
4537
+ return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-value", children: display });
4538
+ }
4539
+ function ChipBinViewer({ value, schema }) {
4540
+ if (!Array.isArray(value) || value.length === 0) {
4541
+ return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-empty", children: "\u2014" });
4542
+ }
4543
+ const itemSchema = Array.isArray(schema.items) ? schema.items[0] : schema.items;
4544
+ const labels = itemSchema?.squisq?.enumLabels;
4545
+ return /* @__PURE__ */ jsx24("div", { className: "squisq-jv-chip-bin", children: value.map((item, i) => {
4546
+ const label = labels && typeof item === "string" ? labels[item] ?? String(item) : String(item);
4547
+ return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-chip", children: label }, i);
4548
+ }) });
4549
+ }
4550
+ function CardStackViewer(props) {
4551
+ const { value, schema, rootSchema, rootData, pointer, density } = props;
4552
+ if (!Array.isArray(value) || value.length === 0) {
4553
+ return /* @__PURE__ */ jsx24("span", { className: "squisq-jv-empty", children: "No items" });
4554
+ }
4555
+ const itemSchema = (Array.isArray(schema.items) ? schema.items[0] : schema.items) ?? {};
4556
+ const itemLabel = itemSchema.squisq?.itemLabel;
4557
+ return /* @__PURE__ */ jsx24("div", { className: "squisq-jv-card-stack", children: value.map((item, i) => {
4558
+ const title = resolveItemTitle(itemLabel, item, i);
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(
4562
+ RenderNode,
4563
+ {
4564
+ value: item,
4565
+ schema: itemSchema,
4566
+ rootSchema,
4567
+ rootData,
4568
+ pointer: `${pointer}/${i}`,
4569
+ density,
4570
+ suppressTopGroupTitle: true
4571
+ }
4572
+ )
4573
+ ] }, i);
4574
+ }) });
4575
+ }
4576
+ function resolveItemTitle(spec, item, index) {
4577
+ if (!spec) return `Item ${index + 1}`;
4578
+ if (typeof spec === "string") return spec;
4579
+ if (item && typeof item === "object") {
4580
+ const v = item[spec.fromField];
4581
+ if (typeof v === "string" && v !== "") return v;
4582
+ if (typeof v === "number") return String(v);
4583
+ }
4584
+ return `Item ${index + 1}`;
4585
+ }
4586
+ function GroupViewer(props) {
4587
+ const { value, schema, rootSchema, rootData, pointer, density, suppressTitle } = props;
4588
+ const title = !suppressTitle ? schema.squisq?.label ?? schema.title : void 0;
4589
+ const help = schema.squisq?.help ?? schema.description;
4590
+ const obj = (value && typeof value === "object" ? value : {}) ?? {};
4591
+ const propEntries = Object.entries(schema.properties ?? {});
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(
4596
+ RowOrSection,
4597
+ {
4598
+ label: propSchema.squisq?.label ?? propSchema.title ?? key,
4599
+ help: propSchema.squisq?.help ?? propSchema.description,
4600
+ kindHint: propSchema,
4601
+ children: /* @__PURE__ */ jsx24(
4602
+ RenderNode,
4603
+ {
4604
+ value: obj[key],
4605
+ schema: propSchema,
4606
+ rootSchema,
4607
+ rootData,
4608
+ pointer: `${pointer}/${key}`,
4609
+ density,
4610
+ suppressTopGroupTitle: true
4611
+ }
4612
+ )
4613
+ }
4614
+ ) }, key))
4615
+ ] });
4616
+ }
4617
+ function RowOrSection({
4618
+ label,
4619
+ help,
4620
+ kindHint,
4621
+ children
4622
+ }) {
4623
+ const composite = isCompositeKind(kindHint);
4624
+ if (composite) {
4625
+ return /* @__PURE__ */ jsx24(Fragment5, { children });
4626
+ }
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 })
4630
+ ] });
4631
+ }
4632
+ function isCompositeKind(schema) {
4633
+ const t = schema.type;
4634
+ const arrayLike = t === "array" || Array.isArray(t) && t.includes("array");
4635
+ if (arrayLike) {
4636
+ return arrayItemKind(schema) === "object";
4637
+ }
4638
+ const objectLike = t === "object" || Array.isArray(t) && t.includes("object");
4639
+ if (objectLike) return true;
4640
+ if (schema.oneOf || schema.anyOf) return true;
4641
+ if (schema.format === "markdown" || schema.squisq?.control === "richtext") return true;
4642
+ return false;
4643
+ }
4644
+ function TabsViewer(props) {
4645
+ const { value, schema, rootSchema, rootData, pointer, density } = props;
4646
+ const branches = (schema.oneOf ?? schema.anyOf ?? []).slice();
4647
+ const matchedIndex = pickMatchingBranch(branches, value);
4648
+ const branch = branches[matchedIndex];
4649
+ if (!branch) {
4650
+ return /* @__PURE__ */ jsx24(TextViewer, { ...props });
4651
+ }
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(
4655
+ RenderNode,
4656
+ {
4657
+ value,
4658
+ schema: branch,
4659
+ rootSchema,
4660
+ rootData,
4661
+ pointer,
4662
+ density
4663
+ }
4664
+ )
4665
+ ] });
4666
+ }
4667
+ function pickMatchingBranch(branches, value) {
4668
+ for (let i = 0; i < branches.length; i++) {
4669
+ if (matchesShape(branches[i], value)) return i;
4670
+ }
4671
+ return 0;
4672
+ }
4673
+ function matchesShape(schema, value) {
4674
+ const t = schema.type;
4675
+ const target = Array.isArray(t) ? t.find((x) => x !== "null") : t;
4676
+ if (!target) return true;
4677
+ switch (target) {
4678
+ case "string":
4679
+ return typeof value === "string";
4680
+ case "number":
4681
+ case "integer":
4682
+ return typeof value === "number";
4683
+ case "boolean":
4684
+ return typeof value === "boolean";
4685
+ case "null":
4686
+ return value === null;
4687
+ case "array":
4688
+ return Array.isArray(value);
4689
+ case "object":
4690
+ return value !== null && typeof value === "object" && !Array.isArray(value);
4691
+ default:
4692
+ return false;
4693
+ }
4694
+ }
4695
+ var VIEWERS = {
4696
+ text: TextViewer,
4697
+ multiline: MultilineViewer,
4698
+ richtext: RichTextViewer,
4699
+ color: ColorViewer,
4700
+ date: DateViewer,
4701
+ time: DateViewer,
4702
+ datetime: DateViewer,
4703
+ slider: NumberViewer,
4704
+ stepper: NumberViewer,
4705
+ segmented: EnumViewer,
4706
+ radio: EnumViewer,
4707
+ combobox: EnumViewer,
4708
+ toggle: BooleanViewer,
4709
+ checkbox: BooleanViewer,
4710
+ card: GroupViewer,
4711
+ group: GroupViewer,
4712
+ "card-stack": CardStackViewer,
4713
+ "chip-bin": ChipBinViewer,
4714
+ tabs: TabsViewer
4715
+ };
4716
+
4717
+ // src/jsonView/RenderNode.tsx
4718
+ import { jsx as jsx25 } from "react/jsx-runtime";
4719
+ function RenderNode(props) {
4720
+ const resolved = resolveRef(props.schema, props.rootSchema) ?? props.schema;
4721
+ if (resolveFlag(resolved.squisq?.hidden, props.rootData)) return null;
4722
+ const kind = chooseControl(resolved);
4723
+ const Viewer = VIEWERS[kind];
4724
+ const viewerProps = {
4725
+ value: props.value,
4726
+ schema: resolved,
4727
+ rootSchema: props.rootSchema,
4728
+ rootData: props.rootData,
4729
+ pointer: props.pointer,
4730
+ density: props.density
4731
+ };
4732
+ if (kind === "group" || kind === "card") {
4733
+ const Group = Viewer;
4734
+ return /* @__PURE__ */ jsx25(Group, { ...viewerProps, suppressTitle: props.suppressTopGroupTitle });
4735
+ }
4736
+ return /* @__PURE__ */ jsx25(Viewer, { ...viewerProps });
4737
+ }
4738
+
4739
+ // src/jsonView/JsonView.tsx
4740
+ import { jsx as jsx26 } from "react/jsx-runtime";
4741
+ function JsonView(props) {
4742
+ const { schema, value, theme, surface, density = "comfortable", className } = props;
4743
+ const { style } = useJsonViewTokens(theme, surface);
4744
+ const cls = "squisq-json-view" + (density === "compact" ? " squisq-json-view--compact" : "") + (className ? ` ${className}` : "");
4745
+ return /* @__PURE__ */ jsx26("div", { className: cls, style, children: /* @__PURE__ */ jsx26(
4746
+ RenderNode,
4747
+ {
4748
+ value,
4749
+ schema,
4750
+ rootSchema: schema,
4751
+ rootData: value,
4752
+ pointer: "",
4753
+ density
4754
+ }
4755
+ ) });
4756
+ }
4757
+ export {
4758
+ BlockRenderer,
4759
+ CaptionOverlay,
4760
+ DocControlsBottom,
4761
+ DocControlsOverlay,
4762
+ DocControlsSidebar,
4763
+ DocControlsSlideshow,
4764
+ DocPlayer,
4765
+ DocPlayerWithSidebar,
4766
+ DocProgressBar,
4767
+ ImageLayer,
4768
+ InlineAudioPlayer,
4769
+ InlineVideoPlayer,
4770
+ JsonView,
4771
+ LinearDocView,
4772
+ MapLayer,
4773
+ MarkdownRenderer,
4774
+ MediaClipLayer,
4775
+ MediaContext,
4776
+ PathLayer,
4777
+ ShapeLayer,
4778
+ SocialCaptionOverlay,
4779
+ TableLayer,
4780
+ TextLayer,
4781
+ VIEWPORT,
4782
+ VideoLayer,
4783
+ formatTime,
4784
+ getAnimationStyle,
4785
+ getTransitionClass,
4786
+ useAudioSync,
4787
+ useAutoSurface,
4788
+ useDocPlayback,
4789
+ useMediaProvider,
4790
+ useMediaSchedule,
4791
+ useMediaUrl,
4792
+ useViewportOrientation
4793
+ };
34
4794
  //# sourceMappingURL=index.js.map